From 5c76350fba98eeae272e281cddd43ca1aa615307 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 17 Apr 2008 22:03:51 +0000 Subject: [PATCH 01/86] --- CHANGELOG | 2 + LICENSE | 24 + README | 23 + _config.php | 3 + code/MultiForm.php | 440 ++++++++++++++++++ code/MultiFormObjectDecorator.php | 59 +++ code/MultiFormPurgeTask.php | 59 +++ code/MultiFormSession.php | 83 ++++ code/MultiFormStep.php | 245 ++++++++++ templates/Includes/MultiFormProgressList.ss | 9 + .../Includes/MultiFormProgressPercent.ss | 1 + 11 files changed, 948 insertions(+) create mode 100644 CHANGELOG create mode 100644 LICENSE create mode 100644 README create mode 100644 _config.php create mode 100644 code/MultiForm.php create mode 100644 code/MultiFormObjectDecorator.php create mode 100644 code/MultiFormPurgeTask.php create mode 100644 code/MultiFormSession.php create mode 100644 code/MultiFormStep.php create mode 100644 templates/Includes/MultiFormProgressList.ss create mode 100644 templates/Includes/MultiFormProgressPercent.ss diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..a10a399 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +0.1: +- initial release \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6bcfb1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +* Copyright (c) 2008, Silverstripe Ltd. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* * Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* * Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* * Neither the name of the nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY Silverstripe Ltd. ``AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL Silverstripe Ltd. BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..c9918a2 --- /dev/null +++ b/README @@ -0,0 +1,23 @@ +------------------------------ +Multistep Form Module +------------------------------ + +April 3rd, 2008 + +Managing multiple form steps ("wizard") with automatic session-saving +of data, and versatile start/finish customizations. + +REQUIREMENTS +------------ +SilverStripe 2.2.2 + +DOCUMENTATION +------------- +http://doc.silverstripe.com/doku.php?id=modules:multiform + +INSTALL +------- + +1) Copy the "multiform" directory into your main SilverStripe directory +2) run ?flush=1 +3) Use it in your forms, e.g. in DataObject->getCMSFields() \ No newline at end of file diff --git a/_config.php b/_config.php new file mode 100644 index 0000000..15c5adc --- /dev/null +++ b/_config.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/code/MultiForm.php b/code/MultiForm.php new file mode 100644 index 0000000..8f663db --- /dev/null +++ b/code/MultiForm.php @@ -0,0 +1,440 @@ + + * static $allowed_actions = array('next','prev'); + * + * + * @todo Deal with Form->securityID + * + * @package multiform + */ +abstract class MultiForm extends Form { + + /** + * A session object stored in the database, which might link + * to further temporary {@link DataObject}s. + * + * @var MultiFormSession + */ + protected $session; + + /** + * Defines which subclass of {@link MultiFormStep} starts the form - + * needs to be defined for the controller to work correctly + * + * @var string Classname of a {@link MultiFormStep} subclass + */ + protected static $start_step; + + static $casting = array( + 'CompletedStepCount' => 'Int', + 'TotalStepCount' => 'Int', + 'CompletedPercent' => 'Float' + ); + + /** + * These fields are ignored when saving the raw form data into session. + * This ensures only field data is saved, and nothing else that's useless + * or potentially dangerous. + * + * @var array + */ + static $ignored_fields = array( + 'url', + 'executeForm', + 'MultiFormSessionID', + 'SecurityID' + ); + + /** + * Perform actions when the multiform is first started. + * + * It does NOT work like a normal controller init()! It has to be explicity called when MultiForm + * is intanciated on your controller. + * + * It sets up the right form session, gets the form step and populates the fields, actions, + * and validation (if it's applicable). + * + * @TODO We've currently got some start up routines that probably need to be moved to their own method, + * like start() - like creating a new MultiFormSession instance. + * + * @TODO init() may not be an appropriate name, considering there's already an init() automatically called + * for controller classes. Perhaps we rename this? + */ + public function init() { + $startStepClass = $this->stat('start_step'); + if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep', E_USER_ERROR); + + // If there's a MultiFormSessionID variable set, find that, otherwise create a new session + if(isset($_GET['MultiFormSessionID'])) { + $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); + } else { + // @TODO fix the fact that you can continually refresh on the first step creating new records + $this->session = new MultiFormSession(); + $this->session->write(); + } + + // Determine whether we use the current step, or create one if it doesn't exist + if(isset($_GET['StepID'])) { + $stepID = (int)$_GET['StepID']; + $step = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); + if($step) { + $currentStep = $step; + $this->session->CurrentStepID = $currentStep->ID; + $this->session->write(); + } + } elseif($this->session->CurrentStepID) { + $currentStep = $this->session->CurrentStep(); + } else { + // @TODO fix the fact that you can continually refresh on the first step creating new records + // @TODO encapsulate this into it's own method - it's the same code as the next() method anyway + $currentStep = new $startStepClass(); + $currentStep->start(); + $currentStep->SessionID = $this->session->ID; + $currentStep->write(); + $this->session->CurrentStepID = $currentStep->ID; + $this->session->write(); + } + + // Set up the fields from the current step + $this->setFields($currentStep->getFields()); + + // Set up the actions from the current step + $this->setActions(); + + // Set a hidden field in the form to define what this form session ID is + $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->ID)); + + // Set up validator from the form step class + $this->validator = $currentStep->getValidator(); + + // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) + if($currentStep->loadData()) { + $this->loadDataFrom($currentStep->loadData()); + } + } + + /** + * Set the fields for this form. + * + * @param FieldSet $fields + */ + function setFields($fields) { + foreach($fields as $field) $field->setForm($this); + $this->fields = $fields; + } + + /** + * Set the actions for this form. + * @TODO is it appropriate to call it setActions? + * @TODO should we put this on MultiFormStep, so it's easy to override on a per-step basis? + */ + function setActions() { + // Create default multi step actions (next, prev), and merge with extra actions, if any + $this->actions = new FieldSet(); + + // If the form is at final step, create a submit button to perform final actions + // The last step doesn't have a next button, so add that action to any step that isn't the final one + if($this->session->CurrentStep()->isFinalStep()) { + $this->actions->push(new FormAction('finish', _t('MultiForm.SUBMIT', 'Submit'))); + } else { + $this->actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); + } + + // If there is a previous step defined, add the back button + if($this->session->CurrentStep()->getPreviousStep()) { + if($this->actions->fieldByName('action_next')) { + $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_next'); + } elseif($this->actions->fieldByName('action_finish')) { + $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_finish'); + } else { + $this->actions->push(new FormAction('prev', _t('MultiForm.BACK', 'Back'))); + } + } + + // Merge any extra action fields defined on the step + $this->actions->merge($this->session->CurrentStep()->getExtraActions()); + } + + /** + * Return a rendered version of this form, with a specific template. + * Looks through the step ancestory templates (MultiFormStep, current step + * subclass template) to see if one is available to render the form with. If + * any of those don't exist, look for a default Form template to render + * with instead. + */ + function forTemplate() { + return $this->renderWith(array( + $this->session->CurrentStep()->class, + 'MultiFormStep', + $this->class, + 'MultiForm', + 'Form' + )); + } + + /** + * This method saves the data on the final step, after submitting. + * It should always be overloaded with parent::finish($data, $form) + * so you can create your own functionality which handles saving + * of all the data collected through each step of the form. + * + * @param array $data The request data returned from the form + * @param object $form The form that the action was called on + */ + public function finish($data, $form) { + if(!$this->session->CurrentStep()->isFinalStep()) { + Director::redirectBack(); + return false; + } + + // Save the form data for the current step + $this->save($data); + } + + /** + * Determine what to do when the next action is called. + * + * Saves the current step session data to the database, creates the + * new step based on getNextStep() of the current step, resets the current + * step to the next step, then redirects to the step. + * + * @param array $data The request data returned from the form + * @param object $form The form that the action was called on + */ + public function next($data, $form) { + if(!$this->session->CurrentStep()->getNextStep()) { + Director::redirectBack(); + return false; + } + + // Switch the step to the next! + $nextStepClass = $this->session->CurrentStep()->getNextStep(); + + // Save the form data for the current step + $this->save($data); + + // Determine whether we can use a step already in the DB, or create a new one + if(!$nextStep = DataObject::get_one($nextStepClass, "SessionID = {$this->session->ID}")) { + $nextStep = new $nextStepClass(); + $nextStep->SessionID = $this->session->ID; + } + + $nextStep->finish(); + $nextStep->write(); + $this->session->CurrentStepID = $nextStep->ID; + $this->session->write(); + + // Redirect to the next step + Director::redirect($this->session->CurrentStep()->Link()); + return; + } + + /** + * Determine what to do when the previous action is called. + * + * Saves the current step session data to the database, retrieves the + * previous step instance based on the classname returned by getPreviousStep() + * on the current step instance, and resets the current step to the previous + * step found, then redirects to the step. + * + * @TODO handle loading the data back into the previous step, from session. + * + * @param array $data The request data returned from the form + * @param object $form The form that the action was called on + */ + public function prev($data, $form) { + if(!$this->session->CurrentStep()->getPreviousStep()) { + Director::redirectBack(); + return false; + } + + // Switch the step to the previous! + $prevStepClass = $this->session->CurrentStep()->getPreviousStep(); + + // Get the previous step of the class instance returned from $currentStep->getPreviousStep() + $prevStep = DataObject::get_one($prevStepClass, "SessionID = {$this->session->ID}"); + + // Set the current step as the previous step + $this->session->CurrentStepID = $prevStep->ID; + $this->session->write(); + + // Redirect to the previous step + Director::redirect($this->session->CurrentStep()->Link()); + return; + } + + /** + * Save the raw data given back from the form into session. + * + * Harmful values provided from the internal form system will be unset from + * the map as defined in self::$ignored_fields. It also unsets any fields + * that look be be form action values, since they aren't required either. + * + * @param array $data An array of data to save + */ + protected function save($data) { + $currentStep = $this->session->CurrentStep(); + if(is_array($data)) { + foreach($data as $field => $value) { + if(in_array($field, self::$ignored_fields) || self::isActionField($field)) { + unset($data[$field]); + } + } + $currentStep->saveData($data); + } + return; + } + + // ############ Misc ############ + + /** + * Add the MultiFormSessionID variable to the URL on form submission. + * We use this to determine what session the multiform is currently using. + * + * @return string + */ + function FormAction() { + $action = parent::FormAction(); + $action .= (strpos($action, '?')) ? '&' : '?'; + $action .= "MultiFormSessionID={$this->session->ID}"; + + return $action; + } + + /** + * Determine the steps to show in a linear fashion, starting from the + * first step. We run a recursive function passing the steps found + * by reference to get a listing of the steps. + * + * @return DataObjectSet + */ + public function getAllStepsLinear() { + $stepsFound = new DataObjectSet(); + + $firstStep = DataObject::get_one($this->stat('start_step'), "SessionID = {$this->session->ID}"); + $templateData = array( + 'ID' => $firstStep->ID, + 'ClassName' => $firstStep->class, + 'Title' => $firstStep->getTitle(), + 'SessionID' => $firstStep->SessionID, + 'LinkingMode' => ($firstStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' + ); + $stepsFound->push(new ArrayData($templateData)); + + $this->getAllStepsRecursive($firstStep, $stepsFound); + + return $stepsFound; + } + + /** + * Recursively run through steps using the getNextStep() method on each step + * to determine what the next step is, gathering each step along the way. + * We stop on the last step, and return the results. + * + * @TODO make use of $step->getNextStepFromDatabase() instead of doing a direct + * DataObject::get() which is doing the same thing. + * + * @param $step Subclass of MultiFormStep to find the next step of + * @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on + * @return DataObjectSet + */ + protected function getAllStepsRecursive($step, &$stepsFound) { + // Find the next step to the current step, the final step has no next step + if(!$step->isFinalStep()) { + if($step->getNextStep()) { + // Is this step in the DB? If it is, we use that + if($nextSteps = DataObject::get($step->getNextStep(), "SessionID = {$this->session->ID}", "LastEdited DESC")) { + $nextStep = $nextSteps->First(); + $templateData = array( + 'ID' => $nextStep->ID, + 'ClassName' => $nextStep->class, + 'Title' => $nextStep->getTitle(), + 'SessionID' => $nextStep->SessionID, + 'LinkingMode' => ($nextStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' + ); + $stepsFound->push(new ArrayData($templateData)); + } else { + // If it's not in the DB, we use a singleton instance of it instead - this step hasn't been accessed yet + $nextStep = singleton($step->getNextStep()); + $templateData = array( + 'ClassName' => $nextStep->class, + 'Title' => $nextStep->getTitle() + ); + $stepsFound->push(new ArrayData($templateData)); + } + // Call back so we can recursively step through + $this->getAllStepsRecursive($nextStep, $stepsFound); + } + // Once we've reached the final step, we just return what we've collected + } else { + return $stepsFound; + } + } + + /** + * Returns the current step in the form process. + * + * @return Instance of a MultiFormStep subclass + */ + public function getCurrentStep() { + return $this->session->CurrentStep(); + } + + /** + * Number of steps already completed (excluding currently started step). + * The way we determine a step is complete is to check if it has the Data + * field filled out with a serialized value, then we know that the user has + * clicked next on the given step, to proceed. + * + * @return int + */ + public function getCompletedStepCount() { + $steps = DataObject::get('MultiFormStep', "SessionID = {$this->session->ID} && Data IS NOT NULL"); + return $steps ? $steps->Count() : 0; + } + + /** + * Total number of steps in the shortest path (only counting straight path without any branching) + * The way we determine this is to check if each step has a next_step string variable set. If it's + * anything else (like an array, for defining multiple branches) then it gets counted as a single step. + * + * @return int + */ + public function getTotalStepCount() { + return $this->getAllStepsLinear() ? $this->getAllStepsLinear()->Count() : 0; + } + + /** + * Percentage of steps completed (excluding currently started step) + * + * @return float + */ + public function getCompletedPercent() { + return (float)$this->CompletedStepCount * 100 / $this->TotalStepCount; + } + + /** + * Determines whether the field is an action. This checks the string name of the + * field, and not the actual field object of one. The actual checking is done + * by doing a string check to see if "action_" is prefixed to the name of the + * field. For example, in the form system: FormAction('next', 'Next') field + * gives an ID of "action_next" + * + * @param string $fieldName The name of the field to check is an action + * @param string $prefix The prefix of the string to check for, default is "action_" + * @return boolean + */ + public static function isActionField($fieldName, $prefix = 'action_') { + if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; + } + +} + +?> \ No newline at end of file diff --git a/code/MultiFormObjectDecorator.php b/code/MultiFormObjectDecorator.php new file mode 100644 index 0000000..3bae337 --- /dev/null +++ b/code/MultiFormObjectDecorator.php @@ -0,0 +1,59 @@ +`.`MultiFormIsTemporary` = 1 + * + * @package multiform + */ +class MultiFormObjectDecorator extends DataObjectDecorator { + + public function updateDBFields() { + return array( + "db" => array( + 'MultiFormIsTemporary' => 'Boolean', + ), + "has_one" => array( + 'MultiFormSession' => 'MultiFormSession', + ) + ); + } + + public function augmentSQL(SQLQuery &$query) { + // If you're querying by ID, ignore the sub-site - this is a bit ugly... + if( + strpos($query->where[0], ".`ID` = ") === false + && strpos($query->where[0], ".ID = ") === false + && strpos($query->where[0], "ID = ") !== 0 + && !$this->wantsTemporary($query) + ) { + $query->where[] = "`{$query->from[0]}`.`MultiFormIsTemporary` = 0"; + } + } + + /** + * Determines if the current query is supposed + * to be exempt from the automatic filtering out + * of temporary records. + * + * @param SQLQuery $query + * @return boolean + */ + protected function wantsTemporary($query) { + foreach($query->where[] as $whereClause) { + if($whereClause == "`{$query->from[0]}`.`MultiFormIsTemporary` = 1") return true; + } + return false; + } + +} + +?> \ No newline at end of file diff --git a/code/MultiFormPurgeTask.php b/code/MultiFormPurgeTask.php new file mode 100644 index 0000000..5239c6d --- /dev/null +++ b/code/MultiFormPurgeTask.php @@ -0,0 +1,59 @@ +getExpiredSessions(); + $sessionDeleteCount = 0; + if($sessions) foreach($sessions as $session) { + $session->purgeStoredData(); + if($session->delete()) $sessionDeleteCount++; + } + } + } + + protected function getExpiredSessions() { + $sessions = new DataObjectSet(); + + $implementors = Object::implementors_for_extension('MultiFormObjectDecorator'); + if($implementors) foreach($implementors as $implementorClass) { + $sessions->merge( + DataObject::get( + $implementorClass, + "`{$implementorClass}`.`MultiFormIsTemporary` = 1 + AND DATEDIFF(NOW(), `{$implementorClass}`.`Created`) > " . self::$session_expiry_days + ) + ); + } + + return $sessions; + } + +} + +?> \ No newline at end of file diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php new file mode 100644 index 0000000..163b904 --- /dev/null +++ b/code/MultiFormSession.php @@ -0,0 +1,83 @@ + 'Text', // stores serialized maps with all session information + ); + + static $has_one = array( + 'Submitter' => 'Member', + 'CurrentStep' => 'MultiFormStep', + ); + + static $has_many = array( + 'FormSteps' => 'MultiFormStep', + ); + + public function onBeforeWrite() { + // save submitter if a Member is logged in + $currentMember = Member::currentMember(); + if(!$this->SubmitterID && $currentMember) $this->SubmitterID = $currentMember->ID; + + parent::onBeforeWrite(); + } + + public function onBeforeDelete() { + // delete dependent form steps + $steps = $this->FormSteps(); + if($steps) foreach($steps as $step) { + $step->delete(); + } + } + + /** + * Enter description here... + * + */ + public function markTemporaryDataObjectsFinished() { + $temporaryObjects = $this->getTemporaryDataObjects(); + if($temporaryObjects) foreach($temporaryObjects as $obj) { + $obj->MultiFormIsTemporary = 0; + $obj->write(); + } + } + + /** + * Enter description here... + * + * @return DataObjectSet + */ + public function getTemporaryDataObjects() { + $implementors = Object::get_implementors_for_extension('MultiFormObjectDecorator'); + $objs = new DataObjectSet(); + if($implementors) foreach($implementors as $implementorClass) { + $objs->merge( + DataObject::get($implementorClass, "MultiFormSessionID = {$this->ID}") + ); + } + + return $objs; + } + + /** + * Remove all related data, either serialized + * in $Data property, or in related stored + * DataObjects. + * + * @return boolean + */ + public function purgeStoredData() { + die('MultiFormSession->purgeStoredData(): Not implemented yet'); + } + +} + +?> \ No newline at end of file diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php new file mode 100644 index 0000000..0e9b5d2 --- /dev/null +++ b/code/MultiFormStep.php @@ -0,0 +1,245 @@ + 'Text' // stores serialized maps with all session information + ); + + static $has_one = array( + 'Session' => 'MultiFormSession' + ); + + /** + * Centerpiece of the flow control for the form. + * If set to a string, you pretty much have a linear + * form flow - if set to an array, you should + * use {@link getNextStep()} to enact flow control + * and branching to different form steps, + * most likely based on previously set session data + * (e.g. a checkbox field or a dropdown). + * + * @var array|string + */ + protected static $next_steps; + + /** + * Each {@link MultiForm} subclass + * needs at least one step which is marked as the "final" one + * and triggers the {@link MultiForm->finish()} + * method that wraps up the whole submission. + * + * @var boolean + */ + protected static $is_final_step = false; + + /** + * Title of this step, can be used by each step that sub-classes this. + * It's useful for creating a list of steps in your template. + * + * @var string + */ + protected $title; + + /** + * Formfields to be rendered with this step + * (Form object is created in {@link MultiForm}. + * This function needs to be implemented + * + * @return FieldSet + */ + public function getFields() { + user_error('Please implement getFields on your MultiFormStep subclass', E_USER_ERROR); + } + + /** + * + * @return FieldSet + */ + public function getExtraActions() { + return new FieldSet(); + } + + /** + * Get a validator specific to this form. + * + * @return Validator + */ + public function getValidator() { + return null; + } + + /** + * Accessor method for $this->title + */ + public function getTitle() { + return $this->title; + } + + /** + * Gets a direct link to this step (only works + * if you're allowed to skip steps, or this step + * has already been saved to the database + * for the current {@link MultiFormSession}). + * + * @return string Relative URL to this step + */ + public function Link() { + return Controller::curr()->Link() . '?MultiFormSessionID=' . $this->Session()->ID; + } + + /** + * Unserialize stored session data and return it. + * This should be called when the form is constructed, + * so the fields can be loaded with the values. + */ + public function loadData() { + return unserialize($this->Data); + } + + /** + * Save the data for this step into session, serializing it first. + * + * @TODO write a code snippet on how to overload this method! + * + * @param array $data The processed data from save() on MultiForm + */ + public function saveData($data) { + $this->Data = serialize($data); + $this->write(); + } + + /** + * @TODO what does this method do? What is it's responsibility? + * + */ + public function start() { + + } + + /** + * @TODO what does this method do, in relation to MultiForm->finish() ? + * I thought we were finalising the entire form on MultiForm, and not + * each step? + */ + public function finish() { + + } + + /** + * Returns the first value of $next_step + * + * @return String Classname of a {@link MultiFormStep} subclass + */ + public function getNextStep() { + $nextSteps = $this->stat('next_steps'); + + // Check if next_steps have been implemented properly if not the final step + if(!$this->isFinalStep()) { + if(!isset($nextSteps)) user_error('MultiFormStep->getNextStep(): Please define at least one $next_steps on ' . $this->class, E_USER_ERROR); + } + + if(is_string($nextSteps)) { + return $nextSteps; + } elseif(is_array($nextSteps) && count($nextSteps)) { + // custom flow control goes here + return $nextSteps[0]; + } else { + return false; + } + } + + /** + * Returns the next step to the current step in the database. + * + * This will only return something if you've previously visited + * the step ahead of the current step, so if you've gone to the + * step ahead, and then gone back a step. + * + * @return MultiFormStep|boolean + */ + public function getNextStepFromDatabase() { + $nextSteps = $this->stat('next_steps'); + if(is_string($nextSteps)) { + $step = DataObject::get($nextSteps, "SessionID = {$this->SessionID}", 'LastEdited DESC'); + if($step) return $step->First(); + } elseif(is_array($nextSteps)) { + $step = DataObject::get($nextSteps[0], "SessionID = {$this->SessionID}", 'LastEdited DESC'); + if($step) return $step->First(); + } else { + return false; + } + } + + /** + * Accessor method for self::$next_steps + */ + public function getNextSteps() { + return $this->stat('next_steps'); + } + + /** + * Returns the previous step, if there is one. + * + * To determine if there is a previous step, we check the database to see if there's + * a previous step for this multi form session ID. + * + * @return String Classname of a {@link MultiFormStep} subclass + */ + public function getPreviousStep() { + $steps = DataObject::get('MultiFormStep', "SessionID = {$this->SessionID}", 'LastEdited DESC'); + if($steps) { + foreach($steps as $step) { + if($step->getNextStep()) { + if($step->getNextStep() == $this->class) { + return $step->class; + } + } + } + } + } + + // ##################### Utility #################### + + /** + * @TODO Do we need this? Can't we just check for the return value of getPreviousStep, + * and do boolean logic from that? + */ + public function hasPreviousStep() { + die('MultiFormStep->hasPreviousStep(): Not implemented yet'); + } + + /** + * Determines whether this step is the final step in the multi-step process or not, + * based on the variable $is_final_step - to set the final step, create this variable + * on your form step class. + * + * @return boolean + */ + public function isFinalStep() { + return $this->stat('is_final_step'); + } + + /** + * Determines whether the currently viewed step is the current step set in the session. + * This assumes you are checking isCurrentStep() against a data record of a MultiFormStep + * subclass, otherwise it doesn't work. An example of this is using a singleton instance - it won't + * work because there's no data. + * + * @return boolean + */ + public function isCurrentStep() { + if($this->class == $this->Session()->CurrentStep()->class) return true; + } + +} + +?> \ No newline at end of file diff --git a/templates/Includes/MultiFormProgressList.ss b/templates/Includes/MultiFormProgressList.ss new file mode 100644 index 0000000..40b5a2f --- /dev/null +++ b/templates/Includes/MultiFormProgressList.ss @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/templates/Includes/MultiFormProgressPercent.ss b/templates/Includes/MultiFormProgressPercent.ss new file mode 100644 index 0000000..f2b064a --- /dev/null +++ b/templates/Includes/MultiFormProgressPercent.ss @@ -0,0 +1 @@ +

You've completed {$CompletedPercent.Nice}% ($CompletedStepCount/$TotalStepCount)

\ No newline at end of file From a48c1433ea3383bb362653b38aeab1c6b8977375 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 18 Apr 2008 23:25:41 +0000 Subject: [PATCH 02/86] Changed static function isActionField to is_action_field to adhere to coding standards --- code/MultiForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 8f663db..3765aa1 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -283,7 +283,7 @@ abstract class MultiForm extends Form { $currentStep = $this->session->CurrentStep(); if(is_array($data)) { foreach($data as $field => $value) { - if(in_array($field, self::$ignored_fields) || self::isActionField($field)) { + if(in_array($field, self::$ignored_fields) || self::is_action_field($field)) { unset($data[$field]); } } @@ -431,7 +431,7 @@ abstract class MultiForm extends Form { * @param string $prefix The prefix of the string to check for, default is "action_" * @return boolean */ - public static function isActionField($fieldName, $prefix = 'action_') { + public static function is_action_field($fieldName, $prefix = 'action_') { if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; } From 101771a096e145f9fd24b65ed7a5c0536cbcd7b5 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 18 Apr 2008 23:49:01 +0000 Subject: [PATCH 03/86] Added TODO for security --- code/MultiForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 3765aa1..a13dddb 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -66,6 +66,9 @@ abstract class MultiForm extends Form { * * @TODO init() may not be an appropriate name, considering there's already an init() automatically called * for controller classes. Perhaps we rename this? + * + * @TODO Security. Currently you're able to just change the ID of MultiFormSessionID in the URL. We need some + * sort of identification so you can't just change to another session by changing the ID. */ public function init() { $startStepClass = $this->stat('start_step'); From cb0a14e601ecb4eaad13c6e8c210c76035273d30 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 00:05:02 +0000 Subject: [PATCH 04/86] Added TODO about expiration --- code/MultiForm.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index a13dddb..44c8570 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -32,6 +32,16 @@ abstract class MultiForm extends Form { */ protected static $start_step; + /** + * Define what type of URL you want to use throughout the step process. + * + * The non-secure way is to go by ID, for example: http://mysite.com/my-form/?MultiFormSessionID=50 + * Alternatively, we store a hash, for example: http://mysite.com/my-form/?MultiFormSessionID=de9f2c7fd25e1b3afad3e850bd17d9b100db4b3 + * + * @var $url_type either "ID", or "Hash" + */ + protected static $url_type = 'Hash'; + static $casting = array( 'CompletedStepCount' => 'Int', 'TotalStepCount' => 'Int', @@ -69,6 +79,9 @@ abstract class MultiForm extends Form { * * @TODO Security. Currently you're able to just change the ID of MultiFormSessionID in the URL. We need some * sort of identification so you can't just change to another session by changing the ID. + * + * @TODO Expiration. We need to make sure that these sessions, making use of {@link MultiFormPurgeTask} and + * {@link MultiFormObjectDecorator} */ public function init() { $startStepClass = $this->stat('start_step'); From 07752155789d562bb2d85e23334dcef2195948f8 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 00:08:19 +0000 Subject: [PATCH 05/86] Removed url_type - this isn't done quite yet to be committed. --- code/MultiForm.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 44c8570..0e89cb1 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -32,16 +32,6 @@ abstract class MultiForm extends Form { */ protected static $start_step; - /** - * Define what type of URL you want to use throughout the step process. - * - * The non-secure way is to go by ID, for example: http://mysite.com/my-form/?MultiFormSessionID=50 - * Alternatively, we store a hash, for example: http://mysite.com/my-form/?MultiFormSessionID=de9f2c7fd25e1b3afad3e850bd17d9b100db4b3 - * - * @var $url_type either "ID", or "Hash" - */ - protected static $url_type = 'Hash'; - static $casting = array( 'CompletedStepCount' => 'Int', 'TotalStepCount' => 'Int', From 39005fdf84402bbfdce10523044d7fb9a09b32ba Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 01:14:16 +0000 Subject: [PATCH 06/86] Added support for Hash identification of a MultiFormSession - probably needs some encapsulation, as we're adding more code to init(), and encapsulate the checking of this->stat('url_type') into it's own method, so you don't need to do the ternary operator so much --- code/MultiForm.php | 40 +++++++++++++++++++++++++++++++++------ code/MultiFormSession.php | 5 +++-- code/MultiFormStep.php | 3 ++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 0e89cb1..461e639 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -32,6 +32,23 @@ abstract class MultiForm extends Form { */ protected static $start_step; + /** + * Define what type of URL you want to use throughout the step process. + * + * By default, we store a hash, for example: http://mysite.com/my-form/?MultiFormSessionID=de9f2c7fd25e1b3afad3e850bd17d9b100db4b3 + * Alternatively, if you set this variable to "ID", then you get ?MultiFormSessionID=20 + * + * The ID is not as secure as the hash, but it all depends on your set up. + * If you're going to add security, such as check the SubmitterID on init + * of the MultiForm and use "ID" for this parameter, then security should be fine. + * + * In any other case, where there's no Member tied to a MultiFormSession, using + * the Hash is the recommended approach. + * + * @var $url_type either "ID", or "Hash" + */ + protected static $url_type = 'Hash'; + static $casting = array( 'CompletedStepCount' => 'Int', 'TotalStepCount' => 'Int', @@ -75,15 +92,25 @@ abstract class MultiForm extends Form { */ public function init() { $startStepClass = $this->stat('start_step'); - if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep', E_USER_ERROR); + $urlType = $this->stat('url_type'); + + if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep on ' . $this->class, E_USER_ERROR); // If there's a MultiFormSessionID variable set, find that, otherwise create a new session if(isset($_GET['MultiFormSessionID'])) { - $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); + if($urlType == 'Hash') { + $hash = Convert::raw2sql($_GET['MultiFormSessionID']); + $this->session = DataObject::get_one('MultiFormSession', "Hash = '$hash'"); + } elseif($urlType == 'ID') { + $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); + } else { + user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); + } } else { // @TODO fix the fact that you can continually refresh on the first step creating new records $this->session = new MultiFormSession(); $this->session->write(); + if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID); } // Determine whether we use the current step, or create one if it doesn't exist @@ -115,7 +142,7 @@ abstract class MultiForm extends Form { $this->setActions(); // Set a hidden field in the form to define what this form session ID is - $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->ID)); + $this->fields->push(new HiddenField('MultiFormSessionID', false, ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash)); // Set up validator from the form step class $this->validator = $currentStep->getValidator(); @@ -307,9 +334,10 @@ abstract class MultiForm extends Form { * @return string */ function FormAction() { + $id = ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash; $action = parent::FormAction(); $action .= (strpos($action, '?')) ? '&' : '?'; - $action .= "MultiFormSessionID={$this->session->ID}"; + $action .= "MultiFormSessionID={$id}"; return $action; } @@ -329,7 +357,7 @@ abstract class MultiForm extends Form { 'ID' => $firstStep->ID, 'ClassName' => $firstStep->class, 'Title' => $firstStep->getTitle(), - 'SessionID' => $firstStep->SessionID, + 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, 'LinkingMode' => ($firstStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' ); $stepsFound->push(new ArrayData($templateData)); @@ -362,7 +390,7 @@ abstract class MultiForm extends Form { 'ID' => $nextStep->ID, 'ClassName' => $nextStep->class, 'Title' => $nextStep->getTitle(), - 'SessionID' => $nextStep->SessionID, + 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, 'LinkingMode' => ($nextStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' ); $stepsFound->push(new ArrayData($templateData)); diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index 163b904..69c4b95 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -11,15 +11,16 @@ class MultiFormSession extends DataObject { static $db = array( 'Data' => 'Text', // stores serialized maps with all session information + 'Hash' => 'Varchar(40)' // cryptographic hash identification to this session ); static $has_one = array( 'Submitter' => 'Member', - 'CurrentStep' => 'MultiFormStep', + 'CurrentStep' => 'MultiFormStep' ); static $has_many = array( - 'FormSteps' => 'MultiFormStep', + 'FormSteps' => 'MultiFormStep' ); public function onBeforeWrite() { diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 0e9b5d2..8ea6194 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -93,7 +93,8 @@ class MultiFormStep extends DataObject { * @return string Relative URL to this step */ public function Link() { - return Controller::curr()->Link() . '?MultiFormSessionID=' . $this->Session()->ID; + $id = $this->Session()->Hash ? $this->Session()->Hash : $this->Session()->ID; + return Controller::curr()->Link() . '?MultiFormSessionID=' . $id; } /** From 433d74b94dff995711bece3137ad137ac521c1fd Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 01:17:12 +0000 Subject: [PATCH 07/86] Added TODO --- code/MultiForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 461e639..977713f 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -122,6 +122,7 @@ abstract class MultiForm extends Form { $this->session->CurrentStepID = $currentStep->ID; $this->session->write(); } + // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. } elseif($this->session->CurrentStepID) { $currentStep = $this->session->CurrentStep(); } else { From 01fe23fe2ff890002fb942ade4deb8ad0f47a9da Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 01:19:47 +0000 Subject: [PATCH 08/86] Removed unused methods --- code/MultiFormStep.php | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 8ea6194..6591b93 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -118,23 +118,6 @@ class MultiFormStep extends DataObject { $this->write(); } - /** - * @TODO what does this method do? What is it's responsibility? - * - */ - public function start() { - - } - - /** - * @TODO what does this method do, in relation to MultiForm->finish() ? - * I thought we were finalising the entire form on MultiForm, and not - * each step? - */ - public function finish() { - - } - /** * Returns the first value of $next_step * @@ -210,14 +193,6 @@ class MultiFormStep extends DataObject { // ##################### Utility #################### - /** - * @TODO Do we need this? Can't we just check for the return value of getPreviousStep, - * and do boolean logic from that? - */ - public function hasPreviousStep() { - die('MultiFormStep->hasPreviousStep(): Not implemented yet'); - } - /** * Determines whether this step is the final step in the multi-step process or not, * based on the variable $is_final_step - to set the final step, create this variable From a47191c802fee86a0e36e4f5aa9ad904bafb2ac5 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 01:21:48 +0000 Subject: [PATCH 09/86] Removed calls to previously removed methods --- code/MultiForm.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 977713f..035ca58 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -129,7 +129,6 @@ abstract class MultiForm extends Form { // @TODO fix the fact that you can continually refresh on the first step creating new records // @TODO encapsulate this into it's own method - it's the same code as the next() method anyway $currentStep = new $startStepClass(); - $currentStep->start(); $currentStep->SessionID = $this->session->ID; $currentStep->write(); $this->session->CurrentStepID = $currentStep->ID; @@ -260,7 +259,6 @@ abstract class MultiForm extends Form { $nextStep->SessionID = $this->session->ID; } - $nextStep->finish(); $nextStep->write(); $this->session->CurrentStepID = $nextStep->ID; $this->session->write(); From 75ce791dcac81226839c6f141ac03aa7f628b797 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 01:23:28 +0000 Subject: [PATCH 10/86] Added documentation --- code/MultiForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 035ca58..9f5ce6e 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -94,6 +94,7 @@ abstract class MultiForm extends Form { $startStepClass = $this->stat('start_step'); $urlType = $this->stat('url_type'); + // Check if there was a start step defined on the subclass of MultiForm if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep on ' . $this->class, E_USER_ERROR); // If there's a MultiFormSessionID variable set, find that, otherwise create a new session From c4cb832e5712c2399928f3b024c21a25abee542d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 03:00:51 +0000 Subject: [PATCH 11/86] $LinkingMode isn't always available, like for a step that hasn't been reached yet --- templates/Includes/MultiFormProgressList.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Includes/MultiFormProgressList.ss b/templates/Includes/MultiFormProgressList.ss index 40b5a2f..ba46431 100644 --- a/templates/Includes/MultiFormProgressList.ss +++ b/templates/Includes/MultiFormProgressList.ss @@ -1,6 +1,6 @@
    <% control AllStepsLinear %> -
  • +
  • <% if LinkingMode = current %><% else %><% if ID %><% end_if %><% end_if %> <% if Title %>$Title<% else %>$ClassName<% end_if %> <% if LinkingMode = current %><% else %><% if ID %><% end_if %><% end_if %> From 75cee5851a4fa1b46306d0c8e7e107bba277d56a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 19 Apr 2008 03:42:17 +0000 Subject: [PATCH 12/86] Removed TODO that's been done! --- code/MultiForm.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 9f5ce6e..ab49432 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -84,9 +84,6 @@ abstract class MultiForm extends Form { * @TODO init() may not be an appropriate name, considering there's already an init() automatically called * for controller classes. Perhaps we rename this? * - * @TODO Security. Currently you're able to just change the ID of MultiFormSessionID in the URL. We need some - * sort of identification so you can't just change to another session by changing the ID. - * * @TODO Expiration. We need to make sure that these sessions, making use of {@link MultiFormPurgeTask} and * {@link MultiFormObjectDecorator} */ From a0dfd338fbb30f84321bb9d65818287c8d8d4098 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:01:27 +0000 Subject: [PATCH 13/86] Encapsulated procedures from init() into their own methods, specifically, setSession() which sets up the current multiform session, and getCurrentStep() is now smarter and setCurrentStep() does the step writing responsibilities --- code/MultiForm.php | 133 ++++++++++++++++++++++++--------------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index ab49432..0fda349 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -73,65 +73,18 @@ abstract class MultiForm extends Form { * Perform actions when the multiform is first started. * * It does NOT work like a normal controller init()! It has to be explicity called when MultiForm - * is intanciated on your controller. + * is intanciated on your controller. @TODO perhaps find a better name, that doesn't quite conflict. * * It sets up the right form session, gets the form step and populates the fields, actions, * and validation (if it's applicable). - * - * @TODO We've currently got some start up routines that probably need to be moved to their own method, - * like start() - like creating a new MultiFormSession instance. - * - * @TODO init() may not be an appropriate name, considering there's already an init() automatically called - * for controller classes. Perhaps we rename this? - * - * @TODO Expiration. We need to make sure that these sessions, making use of {@link MultiFormPurgeTask} and - * {@link MultiFormObjectDecorator} */ public function init() { - $startStepClass = $this->stat('start_step'); - $urlType = $this->stat('url_type'); + // Set up the session + $this->setSession(); - // Check if there was a start step defined on the subclass of MultiForm - if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep on ' . $this->class, E_USER_ERROR); - - // If there's a MultiFormSessionID variable set, find that, otherwise create a new session - if(isset($_GET['MultiFormSessionID'])) { - if($urlType == 'Hash') { - $hash = Convert::raw2sql($_GET['MultiFormSessionID']); - $this->session = DataObject::get_one('MultiFormSession', "Hash = '$hash'"); - } elseif($urlType == 'ID') { - $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); - } else { - user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); - } - } else { - // @TODO fix the fact that you can continually refresh on the first step creating new records - $this->session = new MultiFormSession(); - $this->session->write(); - if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID); - } - - // Determine whether we use the current step, or create one if it doesn't exist - if(isset($_GET['StepID'])) { - $stepID = (int)$_GET['StepID']; - $step = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); - if($step) { - $currentStep = $step; - $this->session->CurrentStepID = $currentStep->ID; - $this->session->write(); - } - // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. - } elseif($this->session->CurrentStepID) { - $currentStep = $this->session->CurrentStep(); - } else { - // @TODO fix the fact that you can continually refresh on the first step creating new records - // @TODO encapsulate this into it's own method - it's the same code as the next() method anyway - $currentStep = new $startStepClass(); - $currentStep->SessionID = $this->session->ID; - $currentStep->write(); - $this->session->CurrentStepID = $currentStep->ID; - $this->session->write(); - } + // Get the current step, and set it + $currentStep = $this->getCurrentStep(); + $this->setCurrentStep($currentStep); // Set up the fields from the current step $this->setFields($currentStep->getFields()); @@ -151,6 +104,71 @@ abstract class MultiForm extends Form { } } + /** + * Get the current step. + * @return MultiFormStep subclass + */ + public function getCurrentStep() { + $startStepClass = $this->stat('start_step'); + + // Check if there was a start step defined on the subclass of MultiForm + if(!isset($startStepClass)) user_error('MultiForm::init(): Please define a $startStep on ' . $this->class, E_USER_ERROR); + + // Determine whether we use the current step, or create one if it doesn't exist + if(isset($_GET['StepID'])) { + $stepID = (int)$_GET['StepID']; + $step = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); + if($step) { + $currentStep = $step; + } + // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. + } elseif($this->session->CurrentStepID) { + $currentStep = $this->session->CurrentStep(); + } else { + // @TODO fix the fact that you can continually refresh on the first step creating new records + // @TODO encapsulate this into it's own method - it's the same code as the next() method anyway + $currentStep = new $startStepClass(); + $currentStep->SessionID = $this->session->ID; + $currentStep->write(); + } + return $currentStep; + } + + /** + * Set the step passed in as the current step. + * @param MultiFormStep $step A subclass of MultiFormStep + */ + protected function setCurrentStep($step) { + $this->session->CurrentStepID = $step->ID; + $this->session->write(); + } + + /** + * Set up the session. There's a bit of logic to determine this, specifically + * checking if there's a GET variable for a current session set. + */ + protected function setSession() { + $urlType = $this->stat('url_type'); + + // If there's a MultiFormSessionID variable set, find that, otherwise create a new session + if(isset($_GET['MultiFormSessionID'])) { + if($urlType == 'Hash') { + $hash = Convert::raw2sql($_GET['MultiFormSessionID']); + $this->session = DataObject::get_one('MultiFormSession', "Hash = '$hash'"); + } elseif($urlType == 'ID') { + $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); + } else { + user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); + } + } else { + // @TODO fix the fact that you can continually refresh on the first step creating new records + $this->session = new MultiFormSession(); + $this->session->write(); + if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID); + $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... + } + } + /** * Set the fields for this form. * @@ -409,15 +427,6 @@ abstract class MultiForm extends Form { } } - /** - * Returns the current step in the form process. - * - * @return Instance of a MultiFormStep subclass - */ - public function getCurrentStep() { - return $this->session->CurrentStep(); - } - /** * Number of steps already completed (excluding currently started step). * The way we determine a step is complete is to check if it has the Data From eb5b57f4189eb9158476d80e654a10901d9049a3 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:28:18 +0000 Subject: [PATCH 14/86] encapsulated $this->session->CurrentStep() calls to getCurrentStep() --- code/MultiForm.php | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 0fda349..a3a30f2 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -75,8 +75,8 @@ abstract class MultiForm extends Form { * It does NOT work like a normal controller init()! It has to be explicity called when MultiForm * is intanciated on your controller. @TODO perhaps find a better name, that doesn't quite conflict. * - * It sets up the right form session, gets the form step and populates the fields, actions, - * and validation (if it's applicable). + * This method sets up the session, figures out the current step, sets the current step, and + * */ public function init() { // Set up the session @@ -190,14 +190,14 @@ abstract class MultiForm extends Form { // If the form is at final step, create a submit button to perform final actions // The last step doesn't have a next button, so add that action to any step that isn't the final one - if($this->session->CurrentStep()->isFinalStep()) { + if($this->getCurrentStep()->isFinalStep()) { $this->actions->push(new FormAction('finish', _t('MultiForm.SUBMIT', 'Submit'))); } else { $this->actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); } // If there is a previous step defined, add the back button - if($this->session->CurrentStep()->getPreviousStep()) { + if($this->getCurrentStep()->getPreviousStep()) { if($this->actions->fieldByName('action_next')) { $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_next'); } elseif($this->actions->fieldByName('action_finish')) { @@ -208,7 +208,7 @@ abstract class MultiForm extends Form { } // Merge any extra action fields defined on the step - $this->actions->merge($this->session->CurrentStep()->getExtraActions()); + $this->actions->merge($this->getCurrentStep()->getExtraActions()); } /** @@ -220,7 +220,7 @@ abstract class MultiForm extends Form { */ function forTemplate() { return $this->renderWith(array( - $this->session->CurrentStep()->class, + $this->getCurrentStep()->class, 'MultiFormStep', $this->class, 'MultiForm', @@ -238,7 +238,7 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function finish($data, $form) { - if(!$this->session->CurrentStep()->isFinalStep()) { + if(!$this->getCurrentStep->isFinalStep()) { Director::redirectBack(); return false; } @@ -258,13 +258,13 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function next($data, $form) { - if(!$this->session->CurrentStep()->getNextStep()) { + if(!$this->getCurrentStep()->getNextStep()) { Director::redirectBack(); return false; } // Switch the step to the next! - $nextStepClass = $this->session->CurrentStep()->getNextStep(); + $nextStepClass = $this->getCurrentStep()->getNextStep(); // Save the form data for the current step $this->save($data); @@ -273,14 +273,14 @@ abstract class MultiForm extends Form { if(!$nextStep = DataObject::get_one($nextStepClass, "SessionID = {$this->session->ID}")) { $nextStep = new $nextStepClass(); $nextStep->SessionID = $this->session->ID; + $nextStep->write(); } - $nextStep->write(); - $this->session->CurrentStepID = $nextStep->ID; - $this->session->write(); + // Set the next step to be the current step + $this->setCurrentStep($nextStep); // Redirect to the next step - Director::redirect($this->session->CurrentStep()->Link()); + Director::redirect($this->getCurrentStep()->Link()); return; } @@ -298,23 +298,22 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function prev($data, $form) { - if(!$this->session->CurrentStep()->getPreviousStep()) { + if(!$this->getCurrentStep()->getPreviousStep()) { Director::redirectBack(); return false; } // Switch the step to the previous! - $prevStepClass = $this->session->CurrentStep()->getPreviousStep(); + $prevStepClass = $this->getCurrentStep()->getPreviousStep(); // Get the previous step of the class instance returned from $currentStep->getPreviousStep() $prevStep = DataObject::get_one($prevStepClass, "SessionID = {$this->session->ID}"); // Set the current step as the previous step - $this->session->CurrentStepID = $prevStep->ID; - $this->session->write(); + $this->setCurrentStep($prevStep); // Redirect to the previous step - Director::redirect($this->session->CurrentStep()->Link()); + Director::redirect($this->getCurrentStep()->Link()); return; } @@ -328,7 +327,7 @@ abstract class MultiForm extends Form { * @param array $data An array of data to save */ protected function save($data) { - $currentStep = $this->session->CurrentStep(); + $currentStep = $this->getCurrentStep(); if(is_array($data)) { foreach($data as $field => $value) { if(in_array($field, self::$ignored_fields) || self::is_action_field($field)) { @@ -373,7 +372,7 @@ abstract class MultiForm extends Form { 'ClassName' => $firstStep->class, 'Title' => $firstStep->getTitle(), 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, - 'LinkingMode' => ($firstStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' + 'LinkingMode' => ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); $stepsFound->push(new ArrayData($templateData)); @@ -406,7 +405,7 @@ abstract class MultiForm extends Form { 'ClassName' => $nextStep->class, 'Title' => $nextStep->getTitle(), 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, - 'LinkingMode' => ($nextStep->ID == $this->session->CurrentStep()->ID) ? 'current' : 'link' + 'LinkingMode' => ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); $stepsFound->push(new ArrayData($templateData)); } else { From facf2e38335a8a92f7d94fd5a0d87b78328e565c Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:28:49 +0000 Subject: [PATCH 15/86] Docs, bug fixes in code --- code/MultiFormStep.php | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 6591b93..ee30136 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -32,8 +32,8 @@ class MultiFormStep extends DataObject { protected static $next_steps; /** - * Each {@link MultiForm} subclass - * needs at least one step which is marked as the "final" one + * Each {@link MultiForm} subclass needs at least + * one step which is marked as the "final" one * and triggers the {@link MultiForm->finish()} * method that wraps up the whole submission. * @@ -50,9 +50,11 @@ class MultiFormStep extends DataObject { protected $title; /** - * Formfields to be rendered with this step + * Form fields to be rendered with this step. * (Form object is created in {@link MultiForm}. - * This function needs to be implemented + * + * This function needs to be implemented on your + * subclasses of MultiFormStep * * @return FieldSet */ @@ -61,6 +63,11 @@ class MultiFormStep extends DataObject { } /** + * Additional form actions to be rendered with this step. + * (Form object is created in {@link MultiForm}. + * + * Note: This is optional, and is to be implemented + * on your subclasses of MultiFormStep * * @return FieldSet */ @@ -70,15 +77,17 @@ class MultiFormStep extends DataObject { /** * Get a validator specific to this form. - * + * * @return Validator */ public function getValidator() { - return null; + return false; } /** - * Accessor method for $this->title + * Accessor method for $this->title + * + * @return string Title of this step */ public function getTitle() { return $this->title; @@ -108,8 +117,6 @@ class MultiFormStep extends DataObject { /** * Save the data for this step into session, serializing it first. - * - * @TODO write a code snippet on how to overload this method! * * @param array $data The processed data from save() on MultiForm */ @@ -153,11 +160,11 @@ class MultiFormStep extends DataObject { public function getNextStepFromDatabase() { $nextSteps = $this->stat('next_steps'); if(is_string($nextSteps)) { - $step = DataObject::get($nextSteps, "SessionID = {$this->SessionID}", 'LastEdited DESC'); - if($step) return $step->First(); + $step = DataObject::get_one($nextSteps, "SessionID = {$this->SessionID}"); + if($step) return $step; } elseif(is_array($nextSteps)) { - $step = DataObject::get($nextSteps[0], "SessionID = {$this->SessionID}", 'LastEdited DESC'); - if($step) return $step->First(); + $step = DataObject::get_one($nextSteps[0], "SessionID = {$this->SessionID}"); + if($step) return $step; } else { return false; } From 0d3196607afb797a6703d4b564124beb4c85e415 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:39:01 +0000 Subject: [PATCH 16/86] Made use of MultiFormStep::getNextStepFromDatabase() instead of direct DO::get() call, and check for SessionID before attempting a DO::get() --- code/MultiForm.php | 3 +-- code/MultiFormStep.php | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index a3a30f2..f966431 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -398,8 +398,7 @@ abstract class MultiForm extends Form { if(!$step->isFinalStep()) { if($step->getNextStep()) { // Is this step in the DB? If it is, we use that - if($nextSteps = DataObject::get($step->getNextStep(), "SessionID = {$this->session->ID}", "LastEdited DESC")) { - $nextStep = $nextSteps->First(); + if($nextStep = $step->getNextStepFromDatabase()) { $templateData = array( 'ID' => $nextStep->ID, 'ClassName' => $nextStep->class, diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index ee30136..cbd62e7 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -152,21 +152,20 @@ class MultiFormStep extends DataObject { * Returns the next step to the current step in the database. * * This will only return something if you've previously visited - * the step ahead of the current step, so if you've gone to the - * step ahead, and then gone back a step. + * the step ahead of the current step, and then gone back a step. * * @return MultiFormStep|boolean */ public function getNextStepFromDatabase() { - $nextSteps = $this->stat('next_steps'); - if(is_string($nextSteps)) { - $step = DataObject::get_one($nextSteps, "SessionID = {$this->SessionID}"); - if($step) return $step; - } elseif(is_array($nextSteps)) { - $step = DataObject::get_one($nextSteps[0], "SessionID = {$this->SessionID}"); - if($step) return $step; - } else { - return false; + if($this->SessionID) { + $nextSteps = $this->stat('next_steps'); + if(is_string($nextSteps)) { + return DataObject::get_one($nextSteps, "SessionID = {$this->SessionID}"); + } elseif(is_array($nextSteps)) { + return DataObject::get_one($nextSteps[0], "SessionID = {$this->SessionID}"); + } else { + return false; + } } } From b298e83a0f35045fce68ebad4f678e493e3ff994 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:43:19 +0000 Subject: [PATCH 17/86] Removed two of the same calls, just use one outside the if-else block instead --- code/MultiForm.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index f966431..435422d 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -406,7 +406,6 @@ abstract class MultiForm extends Form { 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, 'LinkingMode' => ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); - $stepsFound->push(new ArrayData($templateData)); } else { // If it's not in the DB, we use a singleton instance of it instead - this step hasn't been accessed yet $nextStep = singleton($step->getNextStep()); @@ -414,9 +413,9 @@ abstract class MultiForm extends Form { 'ClassName' => $nextStep->class, 'Title' => $nextStep->getTitle() ); - $stepsFound->push(new ArrayData($templateData)); } - // Call back so we can recursively step through + // Add the array data, and do a callback + $stepsFound->push(new ArrayData($templateData)); $this->getAllStepsRecursive($nextStep, $stepsFound); } // Once we've reached the final step, we just return what we've collected From 985abc75578b12e219cd0cb7e74caab83822fcea Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 00:44:14 +0000 Subject: [PATCH 18/86] Variable name change --- code/MultiForm.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 435422d..d3b91ec 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -399,7 +399,7 @@ abstract class MultiForm extends Form { if($step->getNextStep()) { // Is this step in the DB? If it is, we use that if($nextStep = $step->getNextStepFromDatabase()) { - $templateData = array( + $record = array( 'ID' => $nextStep->ID, 'ClassName' => $nextStep->class, 'Title' => $nextStep->getTitle(), @@ -409,13 +409,13 @@ abstract class MultiForm extends Form { } else { // If it's not in the DB, we use a singleton instance of it instead - this step hasn't been accessed yet $nextStep = singleton($step->getNextStep()); - $templateData = array( + $record = array( 'ClassName' => $nextStep->class, 'Title' => $nextStep->getTitle() ); } // Add the array data, and do a callback - $stepsFound->push(new ArrayData($templateData)); + $stepsFound->push(new ArrayData($record)); $this->getAllStepsRecursive($nextStep, $stepsFound); } // Once we've reached the final step, we just return what we've collected From a4a78b82cbe09f54bae3fb91c35b01ab80fd894e Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 05:59:15 +0000 Subject: [PATCH 19/86] Use microtime() --- code/MultiForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index d3b91ec..ca973ef 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -164,7 +164,7 @@ abstract class MultiForm extends Form { // @TODO fix the fact that you can continually refresh on the first step creating new records $this->session = new MultiFormSession(); $this->session->write(); - if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID); + if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID . '-' . microtime()); $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... } } From d5e613a9fb940f4b634c7876d908e85ee14d85d4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 09:25:24 +0000 Subject: [PATCH 20/86] Fixed XML parsing error in template --- templates/Includes/MultiFormProgressList.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Includes/MultiFormProgressList.ss b/templates/Includes/MultiFormProgressList.ss index ba46431..ce98588 100644 --- a/templates/Includes/MultiFormProgressList.ss +++ b/templates/Includes/MultiFormProgressList.ss @@ -1,7 +1,7 @@
      <% control AllStepsLinear %>
    • - <% if LinkingMode = current %><% else %><% if ID %><% end_if %><% end_if %> + <% if LinkingMode = current %><% else %><% if ID %><% end_if %><% end_if %> <% if Title %>$Title<% else %>$ClassName<% end_if %> <% if LinkingMode = current %><% else %><% if ID %><% end_if %><% end_if %>
    • From 18fe36cdd9b150e3ef2bf5d45fae8db45bf9be8d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 09:28:02 +0000 Subject: [PATCH 21/86] Encapsulated getting session records, added notes --- code/MultiForm.php | 52 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index ca973ef..eeb5761 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -151,22 +151,50 @@ abstract class MultiForm extends Form { $urlType = $this->stat('url_type'); // If there's a MultiFormSessionID variable set, find that, otherwise create a new session - if(isset($_GET['MultiFormSessionID'])) { - if($urlType == 'Hash') { - $hash = Convert::raw2sql($_GET['MultiFormSessionID']); - $this->session = DataObject::get_one('MultiFormSession', "Hash = '$hash'"); - } elseif($urlType == 'ID') { - $this->session = DataObject::get_by_id('MultiFormSession', (int)$_GET['MultiFormSessionID']); - } else { - user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); + if(isset($_GET['MultiFormSessionID'])) { + switch($urlType) { + case 'Hash': + $this->session = $this->getSessionRecordByHash($_GET['MultiFormSessionID']); + break; + case 'ID': + $this->session = $this->getSessionRecordByID($_GET['MultiFormSessionID']); + break; + + default: + user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); + break; } } else { // @TODO fix the fact that you can continually refresh on the first step creating new records $this->session = new MultiFormSession(); - $this->session->write(); + $this->session->write(); + + // We have to have an ID, before we can hash the ID of the session. @TODO a better way here? if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID . '-' . microtime()); $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... } + } + + /** + * Return an instance of MultiFormSession from the database by a single + * record with the hash passed into this method. + * + * @param string $hash The Hash field of the record to retrieve + * @return MultiFormSession + */ + function getSessionRecordByHash($hash) { + $SQL_hash = Convert::raw2sql($hash); + return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash'"); + } + + /** + * Return an instance of MultiFormSession from the database by it's ID. + * + * @param int|string $id The ID of the record to retrieve + * @return MultiFormSession + */ + function getSessionRecordByID($id) { + return DataObject::get_by_id('MultiFormSession', $id); } /** @@ -347,11 +375,11 @@ abstract class MultiForm extends Form { * * @return string */ - function FormAction() { - $id = ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash; + function FormAction() { + $urlMethod = $this->stat('url_type'); $action = parent::FormAction(); $action .= (strpos($action, '?')) ? '&' : '?'; - $action .= "MultiFormSessionID={$id}"; + $action .= "MultiFormSessionID={$this->session->$urlMethod}"; return $action; } From 45fd3853c95a01d899b12bfd6c9659cdd594e097 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 22:06:02 +0000 Subject: [PATCH 22/86] Updated README file --- README | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README b/README index c9918a2..042e306 100644 --- a/README +++ b/README @@ -17,7 +17,11 @@ http://doc.silverstripe.com/doku.php?id=modules:multiform INSTALL ------- - 1) Copy the "multiform" directory into your main SilverStripe directory -2) run ?flush=1 -3) Use it in your forms, e.g. in DataObject->getCMSFields() \ No newline at end of file +2) run db/build?flush=1 +3) View documentation (link above) on how to model your forms into steps + +NOTES +----- +If you ever add a new step, you must run db/build?flush=1, as the step requires +a class name in the database to store it's session state, and data. \ No newline at end of file From 6baef78fd3525588ee7765fef409343e76d06004 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sun, 20 Apr 2008 22:07:18 +0000 Subject: [PATCH 23/86] Added missing doc on init() --- code/MultiForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index eeb5761..c5f6211 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -75,8 +75,8 @@ abstract class MultiForm extends Form { * It does NOT work like a normal controller init()! It has to be explicity called when MultiForm * is intanciated on your controller. @TODO perhaps find a better name, that doesn't quite conflict. * - * This method sets up the session, figures out the current step, sets the current step, and - * + * This method sets up the session, figures out the current step, sets the current step, then + * takes the fields, actions and validation (if any) for the step, setting up the form. */ public function init() { // Set up the session From 22d92410210870a8c3ed15573bbb745acc7802f8 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Apr 2008 05:06:47 +0000 Subject: [PATCH 24/86] Added getPreviousStepFromDatabase() to return the previous step class record from the database (if it exists!) --- code/MultiFormStep.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index cbd62e7..a5049ca 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -197,6 +197,17 @@ class MultiFormStep extends DataObject { } } + /** + * Retrieves the previous step record from the database. + * + * @return MultiFormStep subclass + */ + public function getPreviousStepFromDatabase() { + if($prevStepClass = $this->getPreviousStep()) { + return DataObject::get_one($prevStepClass, "SessionID = {$this->SessionID}"); + } + } + // ##################### Utility #################### /** From aa6236b4956bc3e96f62a15d763a2ecee7f38075 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Apr 2008 08:32:05 +0000 Subject: [PATCH 25/86] Added getController (an accessor method to $this->controller so we can use $Controller, or <% control Controller %> in the form templates) --- code/MultiForm.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index c5f6211..dfa7de9 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -104,6 +104,14 @@ abstract class MultiForm extends Form { } } + /** + * Accessor method to $this->controller + * Returns the controller this form was instanciated on. + */ + public function getController() { + return $this->controller; + } + /** * Get the current step. * @return MultiFormStep subclass From ec747bc6c622dc5936b7b92102b9d82356d7aae0 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Apr 2008 08:54:48 +0000 Subject: [PATCH 26/86] Documented loadData() and saveData() --- code/MultiFormStep.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index a5049ca..a46741c 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -108,8 +108,19 @@ class MultiFormStep extends DataObject { /** * Unserialize stored session data and return it. - * This should be called when the form is constructed, - * so the fields can be loaded with the values. + * This is used for loading data previously saved + * in session back into the form. + * + * You need to overload this method onto your own + * step if you require custom loading. An example + * would be selective loading specific fields, or + * filtering out fields that don't require loading. + * + * This method is called on {@link MultiForm} inside + * the init() method, to load the data by default (if + * it exists, back into the form). + * + * @return array */ public function loadData() { return unserialize($this->Data); @@ -117,8 +128,11 @@ class MultiFormStep extends DataObject { /** * Save the data for this step into session, serializing it first. + * + * To selectively save fields, instead of it all, this + * method would need to be overloaded on your step class. * - * @param array $data The processed data from save() on MultiForm + * @param array $data The processed data from save() on {@link MultiForm} */ public function saveData($data) { $this->Data = serialize($data); @@ -198,7 +212,7 @@ class MultiFormStep extends DataObject { } /** - * Retrieves the previous step record from the database. + * Retrieves the previous step class record from the database. * * @return MultiFormStep subclass */ From 6f07bf39b2faaeb276b3a59bfc552fd92fad9c83 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 22 Apr 2008 11:03:03 +0000 Subject: [PATCH 27/86] Added phpdoc to methods and classes --- code/MultiForm.php | 87 ++++++++++++++++++++++--------- code/MultiFormObjectDecorator.php | 4 +- code/MultiFormPurgeTask.php | 1 - code/MultiFormSession.php | 21 ++++++-- code/MultiFormStep.php | 35 +++++++------ 5 files changed, 98 insertions(+), 50 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index dfa7de9..61613ad 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -1,8 +1,9 @@ * - * @todo Deal with Form->securityID - * * @package multiform */ abstract class MultiForm extends Form { @@ -49,6 +48,11 @@ abstract class MultiForm extends Form { */ protected static $url_type = 'Hash'; + /** + * Set the casting for these fields. + * + * @var array + */ static $casting = array( 'CompletedStepCount' => 'Int', 'TotalStepCount' => 'Int', @@ -114,6 +118,12 @@ abstract class MultiForm extends Form { /** * Get the current step. + * + * If StepID has been set in the URL, we attempt to get that record + * by the ID. Otherwise, we check if there's a current step ID in + * our session record. Failing those cases, we assume that the form has + * just been started, and so we create the first step and return it. + * * @return MultiFormStep subclass */ public function getCurrentStep() { @@ -152,8 +162,18 @@ abstract class MultiForm extends Form { } /** - * Set up the session. There's a bit of logic to determine this, specifically - * checking if there's a GET variable for a current session set. + * Set up the session. + * + * First of all we check if MultiFormSessionID is set in the URL, + * then we determine what URL type has been set (default is "Hash"). + * Knowing this, we can retrieve the session record from the database + * by a particular method (getSessionRecordByHash, or getSessionRecordByID). + * + * If MultiFormSessionID isn't set, we assume that this is a new + * multiform that requires a new session record to be created. + * + * @TODO Fix the fact you can continually refresh and create new records + * if MultiFormSessionID isn't set. */ protected function setSession() { $urlType = $this->stat('url_type'); @@ -207,6 +227,9 @@ abstract class MultiForm extends Form { /** * Set the fields for this form. + * + * To ensure that each field knows what form it's related to, + * we call setForm($this) on each field. * * @param FieldSet $fields */ @@ -217,8 +240,19 @@ abstract class MultiForm extends Form { /** * Set the actions for this form. - * @TODO is it appropriate to call it setActions? - * @TODO should we put this on MultiFormStep, so it's easy to override on a per-step basis? + * + * If the current step is the final step, we push in a submit button, which + * calls the action {@link finish()} to finalise the submission. Otherwise, + * we push in a next button which calls the action {@link next()} to determine + * where to go next in our step process, and save any form data collected. + * + * If there's a previous step (a step that has the current step as it's next + * step class), then we allow a previous button, which calls the previous action + * to determine which step to go back to. + * + * If there are any extra actions defined in MultiFormStep->getExtraActions() + * then that set of actions is appended to the end of the actions FieldSet we + * have created in this method. */ function setActions() { // Create default multi step actions (next, prev), and merge with extra actions, if any @@ -287,8 +321,9 @@ abstract class MultiForm extends Form { * Determine what to do when the next action is called. * * Saves the current step session data to the database, creates the - * new step based on getNextStep() of the current step, resets the current - * step to the next step, then redirects to the step. + * new step based on getNextStep() of the current step (or fetches + * an existing one), resets the current step to the next step, + * then redirects to the newly set step. * * @param array $data The request data returned from the form * @param object $form The form that the action was called on @@ -299,20 +334,20 @@ abstract class MultiForm extends Form { return false; } - // Switch the step to the next! + // Get the next step class $nextStepClass = $this->getCurrentStep()->getNextStep(); // Save the form data for the current step $this->save($data); - // Determine whether we can use a step already in the DB, or create a new one + // Determine whether we can use a step already in the DB, or have to create a new one if(!$nextStep = DataObject::get_one($nextStepClass, "SessionID = {$this->session->ID}")) { $nextStep = new $nextStepClass(); $nextStep->SessionID = $this->session->ID; $nextStep->write(); } - // Set the next step to be the current step + // Set the next step found as the current step $this->setCurrentStep($nextStep); // Redirect to the next step @@ -323,12 +358,9 @@ abstract class MultiForm extends Form { /** * Determine what to do when the previous action is called. * - * Saves the current step session data to the database, retrieves the - * previous step instance based on the classname returned by getPreviousStep() - * on the current step instance, and resets the current step to the previous - * step found, then redirects to the step. - * - * @TODO handle loading the data back into the previous step, from session. + * Retrieves the previous step class, finds the record for that + * class in the DB, and sets the current step to that step found. + * Finally, it redirects to that step. * * @param array $data The request data returned from the form * @param object $form The form that the action was called on @@ -356,9 +388,9 @@ abstract class MultiForm extends Form { /** * Save the raw data given back from the form into session. * - * Harmful values provided from the internal form system will be unset from - * the map as defined in self::$ignored_fields. It also unsets any fields - * that look be be form action values, since they aren't required either. + * Take the submitted form data for the current step, removing + * any key => value pairs that shouldn't be saved, then saves + * the data into the session. * * @param array $data An array of data to save */ @@ -422,9 +454,6 @@ abstract class MultiForm extends Form { * to determine what the next step is, gathering each step along the way. * We stop on the last step, and return the results. * - * @TODO make use of $step->getNextStepFromDatabase() instead of doing a direct - * DataObject::get() which is doing the same thing. - * * @param $step Subclass of MultiFormStep to find the next step of * @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on * @return DataObjectSet @@ -466,6 +495,9 @@ abstract class MultiForm extends Form { * field filled out with a serialized value, then we know that the user has * clicked next on the given step, to proceed. * + * @TODO Not sure if it's entirely appropriate to check if Data is set as a + * way to determine a step is "completed". + * * @return int */ public function getCompletedStepCount() { @@ -498,7 +530,10 @@ abstract class MultiForm extends Form { * field, and not the actual field object of one. The actual checking is done * by doing a string check to see if "action_" is prefixed to the name of the * field. For example, in the form system: FormAction('next', 'Next') field - * gives an ID of "action_next" + * gives an ID of "action_next" + * + * The assumption here is the ID we're checking against has the prefix that we're + * looking for, otherwise this won't work. * * @param string $fieldName The name of the field to check is an action * @param string $prefix The prefix of the string to check for, default is "action_" diff --git a/code/MultiFormObjectDecorator.php b/code/MultiFormObjectDecorator.php index 3bae337..fd4d718 100644 --- a/code/MultiFormObjectDecorator.php +++ b/code/MultiFormObjectDecorator.php @@ -18,10 +18,10 @@ class MultiFormObjectDecorator extends DataObjectDecorator { public function updateDBFields() { return array( - "db" => array( + 'db' => array( 'MultiFormIsTemporary' => 'Boolean', ), - "has_one" => array( + 'has_one' => array( 'MultiFormSession' => 'MultiFormSession', ) ); diff --git a/code/MultiFormPurgeTask.php b/code/MultiFormPurgeTask.php index 5239c6d..b78d53e 100644 --- a/code/MultiFormPurgeTask.php +++ b/code/MultiFormPurgeTask.php @@ -22,7 +22,6 @@ class MultiFormPurgeTask extends DailyTask { */ public static $session_expiry_days = 7; - public function run() { $controllers = ClassInfo::subclassesFor('MultiForm'); diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index 69c4b95..bd07208 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -4,8 +4,11 @@ * Serializes one or more {@link MultiFormStep}s into * a database object. * + * MultiFormSession also stores the current step, so that + * the {@link MultiForm} and {@link MultiFormStep} classes + * know what the current step is. + * * @package multiform - * */ class MultiFormSession extends DataObject { @@ -23,6 +26,9 @@ class MultiFormSession extends DataObject { 'FormSteps' => 'MultiFormStep' ); + /** + * These actions are performed when write() is called on this object. + */ public function onBeforeWrite() { // save submitter if a Member is logged in $currentMember = Member::currentMember(); @@ -30,7 +36,10 @@ class MultiFormSession extends DataObject { parent::onBeforeWrite(); } - + + /** + * These actions are performed when delete() is called on this object. + */ public function onBeforeDelete() { // delete dependent form steps $steps = $this->FormSteps(); @@ -40,8 +49,8 @@ class MultiFormSession extends DataObject { } /** - * Enter description here... - * + * Get all the temporary objects, and set them as temporary, writing + * them back to the database. */ public function markTemporaryDataObjectsFinished() { $temporaryObjects = $this->getTemporaryDataObjects(); @@ -52,7 +61,9 @@ class MultiFormSession extends DataObject { } /** - * Enter description here... + * Get all classes that implement the MultiFormObjectDecorator, + * find the records for each and merge them together into a + * DataObjectSet. * * @return DataObjectSet */ diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index a46741c..34acc69 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -1,10 +1,11 @@ stat('next_steps'); @@ -226,8 +230,7 @@ class MultiFormStep extends DataObject { /** * Determines whether this step is the final step in the multi-step process or not, - * based on the variable $is_final_step - to set the final step, create this variable - * on your form step class. + * based on the variable $is_final_step - which must be defined on at least one step. * * @return boolean */ From dac113fe3d9805baf94d8483ef25020ef09e2ce2 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 23 Apr 2008 10:46:11 +0000 Subject: [PATCH 28/86] Made the hidden field work the same way as the FormAction() --- code/MultiForm.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 61613ad..7e6c529 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -97,7 +97,8 @@ abstract class MultiForm extends Form { $this->setActions(); // Set a hidden field in the form to define what this form session ID is - $this->fields->push(new HiddenField('MultiFormSessionID', false, ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash)); + $urlMethod = $this->stat('url_type'); + $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); // Set up validator from the form step class $this->validator = $currentStep->getValidator(); From 20207a8deea27991c823555228b92120a0eca728 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 28 Apr 2008 03:10:13 +0000 Subject: [PATCH 29/86] Disable default form security token - our sessions are identified by URL, so this won't work! (Although we could possibly find a better way than simply disabling this in the future) --- code/MultiForm.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 7e6c529..03ea739 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -83,6 +83,10 @@ abstract class MultiForm extends Form { * takes the fields, actions and validation (if any) for the step, setting up the form. */ public function init() { + // Disable security token. We tie a form to a session by URL. + // @TODO Is there a better way than simply disabling this? + $this->disableSecurityToken(); + // Set up the session $this->setSession(); From 9475fabee7f5a2a36c7bdaa3d5b59a72c9c79a58 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 28 Apr 2008 03:13:03 +0000 Subject: [PATCH 30/86] Fixed old documentation --- code/MultiForm.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 03ea739..bcdf2d1 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -100,7 +100,9 @@ abstract class MultiForm extends Form { // Set up the actions from the current step $this->setActions(); - // Set a hidden field in the form to define what this form session ID is + // Set a hidden field in the form to identify this session. + // Depending on what has been configured for $url_type, we + // find an encrypted hash to identify the session. $urlMethod = $this->stat('url_type'); $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); From d878a45645d53b6897823d9ebd22bc42925ec8eb Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 28 Apr 2008 03:13:29 +0000 Subject: [PATCH 31/86] tweaked doc --- code/MultiForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index bcdf2d1..c2852e1 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -102,7 +102,7 @@ abstract class MultiForm extends Form { // Set a hidden field in the form to identify this session. // Depending on what has been configured for $url_type, we - // find an encrypted hash to identify the session. + // find an encrypted hash by default to identify the session. $urlMethod = $this->stat('url_type'); $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); From ac09ddfa8666201337652c51d2ed67ca7ce53f82 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 28 Apr 2008 06:42:32 +0000 Subject: [PATCH 32/86] Removed the disabling of SecurityID --- code/MultiForm.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index c2852e1..a08e574 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -83,10 +83,6 @@ abstract class MultiForm extends Form { * takes the fields, actions and validation (if any) for the step, setting up the form. */ public function init() { - // Disable security token. We tie a form to a session by URL. - // @TODO Is there a better way than simply disabling this? - $this->disableSecurityToken(); - // Set up the session $this->setSession(); From 75a4fb78d526a329c99e2f94c56e97a3d71e3771 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 29 Apr 2008 09:50:06 +0000 Subject: [PATCH 33/86] Added TODO --- code/MultiFormStep.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 34acc69..5eec4f7 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -99,7 +99,11 @@ class MultiFormStep extends DataObject { * Gets a direct link to this step (only works * if you're allowed to skip steps, or this step * has already been saved to the database - * for the current {@link MultiFormSession}). + * for the current {@link MultiFormSession}). + * + * @TODO We check for the Hash field directly on session + * to determine how to identify the session. This isn't + * a very good way of doing it! * * @return string Relative URL to this step */ From 3872ca004c1366126c53516f2f41a3db67f41caa Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 30 Apr 2008 03:46:35 +0000 Subject: [PATCH 34/86] Don't need to check if data exists first --- code/MultiForm.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index a08e574..9f5e54c 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -105,10 +105,8 @@ abstract class MultiForm extends Form { // Set up validator from the form step class $this->validator = $currentStep->getValidator(); - // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) - if($currentStep->loadData()) { - $this->loadDataFrom($currentStep->loadData()); - } + // Load existing data into the form (CAUTION: loadData() MUST unserialize first!) + $this->loadDataFrom($currentStep->loadData()); } /** From ff9662f9f544706768917640bd24637541887e4f Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 30 Apr 2008 03:47:33 +0000 Subject: [PATCH 35/86] Revert last change --- code/MultiForm.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 9f5e54c..a08e574 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -105,8 +105,10 @@ abstract class MultiForm extends Form { // Set up validator from the form step class $this->validator = $currentStep->getValidator(); - // Load existing data into the form (CAUTION: loadData() MUST unserialize first!) - $this->loadDataFrom($currentStep->loadData()); + // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) + if($currentStep->loadData()) { + $this->loadDataFrom($currentStep->loadData()); + } } /** From 652c0af4c2b3de2d12c80e2a637a7427ac16fd4d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 06:54:53 +0000 Subject: [PATCH 36/86] Added parent::onBeforeDelete() - without this, it breaks! --- code/MultiFormSession.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index bd07208..13a49ff 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -46,6 +46,8 @@ class MultiFormSession extends DataObject { if($steps) foreach($steps as $step) { $step->delete(); } + + parent::onBeforeDelete(); } /** From 626c272b6f28c51b66f3f04df9ec680c39d3df08 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 07:16:38 +0000 Subject: [PATCH 37/86] Remove the relation as well as delete the object --- code/MultiFormSession.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index 13a49ff..a94b0fa 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -41,9 +41,10 @@ class MultiFormSession extends DataObject { * These actions are performed when delete() is called on this object. */ public function onBeforeDelete() { - // delete dependent form steps + // delete dependent form steps and relation $steps = $this->FormSteps(); if($steps) foreach($steps as $step) { + $steps->remove($step); $step->delete(); } From f5f93fb10e3c28d838fe80b15c1095b78bfb0b27 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 07:18:12 +0000 Subject: [PATCH 38/86] Added basic test to MultiForm --- tests/MultiFormSessionTest.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/MultiFormSessionTest.php diff --git a/tests/MultiFormSessionTest.php b/tests/MultiFormSessionTest.php new file mode 100644 index 0000000..6360037 --- /dev/null +++ b/tests/MultiFormSessionTest.php @@ -0,0 +1,33 @@ +write(); + + $this->assertTrue($session->ID != 0); + $this->assertTrue($session->ID > 0); + + $session->delete(); + } + + /** + * Test that a MemberID was set on MultiFormSession if + * a member is logged in. + */ + function testMemberLogging() { + $session = new MultiFormSession(); + $session->write(); + + if($memberID = Member::currentUserID()) { + $this->assertTrue($memberID == $session->SubmitterID); + } + } + +} + +?> \ No newline at end of file From 56325faf8f34cc45103d5facb0580ef19ed6fdd8 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 10:22:58 +0000 Subject: [PATCH 39/86] added an accessor method for $this->session --- code/MultiForm.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index a08e574..5581669 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -164,6 +164,14 @@ abstract class MultiForm extends Form { $this->session->write(); } + /** + * Accessor method to $this->session. + * @return MultiFormSession + */ + function getSession() { + return $this->session; + } + /** * Set up the session. * From 8d93736482606aede2fcde4b9d5640680fc96a94 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 10:40:19 +0000 Subject: [PATCH 40/86] Fall back to using the class name if there is no title property set --- code/MultiForm.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 5581669..f26c931 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -449,7 +449,7 @@ abstract class MultiForm extends Form { $templateData = array( 'ID' => $firstStep->ID, 'ClassName' => $firstStep->class, - 'Title' => $firstStep->getTitle(), + 'Title' => $firstStep->title ? $firstStep->title : $firstStep->class, 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, 'LinkingMode' => ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); @@ -478,7 +478,7 @@ abstract class MultiForm extends Form { $record = array( 'ID' => $nextStep->ID, 'ClassName' => $nextStep->class, - 'Title' => $nextStep->getTitle(), + 'Title' => $nextStep->title ? $nextStep->title : $nextStep->class, 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, 'LinkingMode' => ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); @@ -487,7 +487,7 @@ abstract class MultiForm extends Form { $nextStep = singleton($step->getNextStep()); $record = array( 'ClassName' => $nextStep->class, - 'Title' => $nextStep->getTitle() + 'Title' => $nextStep->title ? $nextStep->title : $nextStep->class ); } // Add the array data, and do a callback From 927e974724aa69888543233966a32ae61a3f1444 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 10:45:19 +0000 Subject: [PATCH 41/86] Added TODO about session being "baked" into MultiForm directly --- code/MultiForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index f26c931..55a703f 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -185,6 +185,9 @@ abstract class MultiForm extends Form { * * @TODO Fix the fact you can continually refresh and create new records * if MultiFormSessionID isn't set. + * + * @TODO Not sure if we should bake the session stuff directly into MultiForm. + * Perhaps it would be best dealt with on a separate class? */ protected function setSession() { $urlType = $this->stat('url_type'); From 6cfe0680c0fa39a3e975674ea16ec5f820d00df9 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 10:57:25 +0000 Subject: [PATCH 42/86] Added TODO items to fix later --- code/MultiForm.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 55a703f..bf219e0 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -138,12 +138,10 @@ abstract class MultiForm extends Form { // Determine whether we use the current step, or create one if it doesn't exist if(isset($_GET['StepID'])) { $stepID = (int)$_GET['StepID']; - $step = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); - if($step) { - $currentStep = $step; - } - // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. + $currentStep = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); + // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. } elseif($this->session->CurrentStepID) { + // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. $currentStep = $this->session->CurrentStep(); } else { // @TODO fix the fact that you can continually refresh on the first step creating new records From 54164a129f3a22d0d86dcf4c391d40a143351357 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 10:57:41 +0000 Subject: [PATCH 43/86] Whitespace --- code/MultiForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index bf219e0..e999251 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -150,6 +150,7 @@ abstract class MultiForm extends Form { $currentStep->SessionID = $this->session->ID; $currentStep->write(); } + return $currentStep; } From e4f51e07c28aa20ca21749008870cafe6cd2aa8c Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 11:09:47 +0000 Subject: [PATCH 44/86] Added basics for testing, and some test classes for future test operations --- tests/MultiFormSessionTest.php | 28 +++++++++--- tests/MultiFormTest.php | 64 +++++++++++++++++++++++++++ tests/code/MultiFormTestClass.php | 17 +++++++ tests/code/MultiFormTestStepOne.php | 17 +++++++ tests/code/MultiFormTestStepThree.php | 15 +++++++ tests/code/MultiFormTestStepTwo.php | 15 +++++++ 6 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 tests/MultiFormTest.php create mode 100644 tests/code/MultiFormTestClass.php create mode 100644 tests/code/MultiFormTestStepOne.php create mode 100644 tests/code/MultiFormTestStepThree.php create mode 100644 tests/code/MultiFormTestStepTwo.php diff --git a/tests/MultiFormSessionTest.php b/tests/MultiFormSessionTest.php index 6360037..0cc3efc 100644 --- a/tests/MultiFormSessionTest.php +++ b/tests/MultiFormSessionTest.php @@ -2,17 +2,24 @@ class MultiFormSessionTest extends SapphireTest { + /** + * Set up the instance of MultiFormSession, writing + * a record to the database for this test. We persist + * the object in our tests by assigning $this->session + */ + function setUp() { + $this->session = new MultiFormSession(); + $this->session->write(); + } + /** * Test generation of a new session. + * + * @TODO Write some more advanced tests for MultiFormSession. */ function testSessionGeneration() { - $session = new MultiFormSession(); - $session->write(); - - $this->assertTrue($session->ID != 0); - $this->assertTrue($session->ID > 0); - - $session->delete(); + $this->assertTrue($this->session->ID != 0); + $this->assertTrue($this->session->ID > 0); } /** @@ -28,6 +35,13 @@ class MultiFormSessionTest extends SapphireTest { } } + /** + * Delete the MultiFormSession record that we created. + */ + function tearDown() { + $this->session->delete(); + } + } ?> \ No newline at end of file diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php new file mode 100644 index 0000000..0f7f005 --- /dev/null +++ b/tests/MultiFormTest.php @@ -0,0 +1,64 @@ +session + */ + function setUp() { + $this->form = new MultiFormTestClass(new Controller(), 'Form', new FieldSet(), new FieldSet()); + $this->form->init(); + } + + /** + * Tests initialising a new instance of a test class. + * + * @TODO Write some decent tests! The current assertions are very basic, and are + * nowhere near touching on the more advanced concepts of MultiForm, such + * as the form actions (prev/next), session handling, and step handling + * through {@link MultiFormStep->getPreviousStep()} and + * {@link MultiFormStep->getNextStep()} for example. + */ + function testInitialisingForm() { + $this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0)); + $this->assertTrue(is_numeric($this->form->session->ID) && ($this->form->session->ID > 0)); + $this->assertTrue($this->form->getStartStep() == 'MultiFormTestStepOne'); + } + + /** + * Remove the session data that was created. Note: This should delete all the + * dependencies such as MultiFormStep instances that are related directly to + * this session. These directives can be found on {@link MultiFormSession->onBeforeWrite()} + */ + function tearDown() { + $this->form->session->delete(); + } + +} + +?> \ No newline at end of file diff --git a/tests/code/MultiFormTestClass.php b/tests/code/MultiFormTestClass.php new file mode 100644 index 0000000..b0d1e56 --- /dev/null +++ b/tests/code/MultiFormTestClass.php @@ -0,0 +1,17 @@ +stat('start_step'); + } + +} + +?> \ No newline at end of file diff --git a/tests/code/MultiFormTestStepOne.php b/tests/code/MultiFormTestStepOne.php new file mode 100644 index 0000000..3da22ae --- /dev/null +++ b/tests/code/MultiFormTestStepOne.php @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/tests/code/MultiFormTestStepThree.php b/tests/code/MultiFormTestStepThree.php new file mode 100644 index 0000000..7576ac9 --- /dev/null +++ b/tests/code/MultiFormTestStepThree.php @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/tests/code/MultiFormTestStepTwo.php b/tests/code/MultiFormTestStepTwo.php new file mode 100644 index 0000000..4d7f901 --- /dev/null +++ b/tests/code/MultiFormTestStepTwo.php @@ -0,0 +1,15 @@ + \ No newline at end of file From 39229e19f45fa533dce80ea61d73f2b558628682 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 11:12:52 +0000 Subject: [PATCH 45/86] Added note in _config.php for what this is for --- _config.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_config.php b/_config.php index 15c5adc..db1f70d 100644 --- a/_config.php +++ b/_config.php @@ -1,3 +1,5 @@ \ No newline at end of file From edcdbf893578957fa4ce209e5dea00d97cd15caa Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 14 May 2008 11:30:35 +0000 Subject: [PATCH 46/86] Fixed re-creating of a new Hash field even if one exists, also fixed non-object errors through better structured setSession() method --- code/MultiForm.php | 25 ++++++++++++++----------- code/MultiFormSession.php | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index e999251..a5deb11 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -139,13 +139,9 @@ abstract class MultiForm extends Form { if(isset($_GET['StepID'])) { $stepID = (int)$_GET['StepID']; $currentStep = DataObject::get_one('MultiFormStep', "SessionID = {$this->session->ID} AND ID = {$stepID}"); - // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. } elseif($this->session->CurrentStepID) { - // @TODO if you set a wrong ID, then it ends up at this point with a non-object error. $currentStep = $this->session->CurrentStep(); } else { - // @TODO fix the fact that you can continually refresh on the first step creating new records - // @TODO encapsulate this into it's own method - it's the same code as the next() method anyway $currentStep = new $startStepClass(); $currentStep->SessionID = $this->session->ID; $currentStep->write(); @@ -205,14 +201,21 @@ abstract class MultiForm extends Form { user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); break; } - } else { + } + + // If there was no session found, create a new one instead + if(!$this->session) { // @TODO fix the fact that you can continually refresh on the first step creating new records $this->session = new MultiFormSession(); - $this->session->write(); + $this->session->write(); + } - // We have to have an ID, before we can hash the ID of the session. @TODO a better way here? - if($urlType == 'Hash') $this->session->Hash = sha1($this->session->ID . '-' . microtime()); - $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... + // We have to have an ID, before we can hash the ID of the session. @TODO a better way here? + if($urlType == 'Hash') { + if(!$this->session->Hash) { + $this->session->Hash = sha1($this->session->ID . '-' . microtime()); + $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... + } } } @@ -225,7 +228,7 @@ abstract class MultiForm extends Form { */ function getSessionRecordByHash($hash) { $SQL_hash = Convert::raw2sql($hash); - return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash'"); + return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); } /** @@ -235,7 +238,7 @@ abstract class MultiForm extends Form { * @return MultiFormSession */ function getSessionRecordByID($id) { - return DataObject::get_by_id('MultiFormSession', $id); + return DataObject::get_one('MultiFormSession', "MultiFormSession.ID = $id AND IsComplete = 0"); } /** diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index a94b0fa..b85297d 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -13,8 +13,9 @@ class MultiFormSession extends DataObject { static $db = array( - 'Data' => 'Text', // stores serialized maps with all session information - 'Hash' => 'Varchar(40)' // cryptographic hash identification to this session + 'Data' => 'Text', // stores serialized maps with all session information + 'Hash' => 'Varchar(40)', // cryptographic hash identification to this session + 'IsComplete' => 'Boolean' // flag to determine if this session is marked completed ); static $has_one = array( @@ -26,6 +27,17 @@ class MultiFormSession extends DataObject { 'FormSteps' => 'MultiFormStep' ); + /** + * Mark this session as completed. + * + * This sets the flag "IsComplete" to true, + * and writes the session back. + */ + public function markCompleted() { + $this->IsComplete = 1; + $this->write(); + } + /** * These actions are performed when write() is called on this object. */ From 2f4e8de921fd28627ccf20527ec56f7464c7e561 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 23 May 2008 05:47:07 +0000 Subject: [PATCH 47/86] Added tests, and used assertEquals which makes better use of PHPUnit --- tests/MultiFormSessionTest.php | 2 +- tests/MultiFormTest.php | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/MultiFormSessionTest.php b/tests/MultiFormSessionTest.php index 0cc3efc..c11d4d9 100644 --- a/tests/MultiFormSessionTest.php +++ b/tests/MultiFormSessionTest.php @@ -31,7 +31,7 @@ class MultiFormSessionTest extends SapphireTest { $session->write(); if($memberID = Member::currentUserID()) { - $this->assertTrue($memberID == $session->SubmitterID); + $this->assertEquals($memberID, $session->SubmitterID); } } diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index 0f7f005..517321e 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -47,7 +47,21 @@ class MultiFormTest extends SapphireTest { function testInitialisingForm() { $this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0)); $this->assertTrue(is_numeric($this->form->session->ID) && ($this->form->session->ID > 0)); - $this->assertTrue($this->form->getStartStep() == 'MultiFormTestStepOne'); + $this->assertEquals('MultiFormTestStepOne', $this->form->getStartStep()); + } + + /** + * Test that the 2nd step is correct to what we expect it to be. + */ + function testSecondStep() { + $this->assertEquals('MultiFormTestStepTwo', $this->form->getCurrentStep()->getNextStep()); + } + + /** + * Test that the amount of steps we have has been calculated correctly. + */ + function testTotalStepCount() { + $this->assertEquals(3, $this->form->getAllStepsLinear()->Count()); } /** From b0f45b505b8830206bacf0c17d4309f027faa02a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 24 May 2008 23:49:49 +0000 Subject: [PATCH 48/86] BUGFIX validator couldn't tie itself to a form correctly, encapsulated validator setup on MultiForm->setValidator() --- code/MultiForm.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index a5deb11..3fdd57b 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -103,7 +103,7 @@ abstract class MultiForm extends Form { $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); // Set up validator from the form step class - $this->validator = $currentStep->getValidator(); + $this->setValidator(); // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) if($currentStep->loadData()) { @@ -254,6 +254,19 @@ abstract class MultiForm extends Form { $this->fields = $fields; } + /** + * Set up the validation for this form. + * + * We check if the validator exists first, as validation on + * each step is optional. + */ + function setValidator() { + if($validator = $this->getCurrentStep()->getValidator()) { + $this->validator = $validator; + $this->validator->setForm($this); + } + } + /** * Set the actions for this form. * From 2385c23d6305af56a2b71b4895b33c62d81b14f3 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 26 May 2008 04:06:09 +0000 Subject: [PATCH 49/86] BUGFIX getCurrentStep should be called as a method, not a property --- code/MultiForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 3fdd57b..3c4fbfd 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -337,7 +337,7 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function finish($data, $form) { - if(!$this->getCurrentStep->isFinalStep()) { + if(!$this->getCurrentStep()->isFinalStep()) { Director::redirectBack(); return false; } From 4f758064d9dd6962105860710217473af9407e8f Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 27 May 2008 04:49:16 +0000 Subject: [PATCH 50/86] FEATURE Allow setting a flag (true, by default) so that you can't use the "back" form action for a step. --- code/MultiForm.php | 35 ++++++++++++++++++++--------------- code/MultiFormStep.php | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 3c4fbfd..4c5ab72 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -83,18 +83,21 @@ abstract class MultiForm extends Form { * takes the fields, actions and validation (if any) for the step, setting up the form. */ public function init() { - // Set up the session + // Set up the session for this MultiForm instance $this->setSession(); - // Get the current step, and set it + // Get the current step available (Note: either returns an existing + // step or creates a new one if none available) $currentStep = $this->getCurrentStep(); + + // Set the step returned above as the current step $this->setCurrentStep($currentStep); - // Set up the fields from the current step + // Set up the fields for the current step $this->setFields($currentStep->getFields()); - // Set up the actions from the current step - $this->setActions(); + // Set up the actions for the current step + $this->setActions($currentStep); // Set a hidden field in the form to identify this session. // Depending on what has been configured for $url_type, we @@ -102,7 +105,7 @@ abstract class MultiForm extends Form { $urlMethod = $this->stat('url_type'); $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); - // Set up validator from the form step class + // Set up validator for the step $this->setValidator(); // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) @@ -282,32 +285,34 @@ abstract class MultiForm extends Form { * If there are any extra actions defined in MultiFormStep->getExtraActions() * then that set of actions is appended to the end of the actions FieldSet we * have created in this method. + * + * @param $currentStep Subclass of MultiFormStep to set actions for */ - function setActions() { + function setActions($currentStep) { // Create default multi step actions (next, prev), and merge with extra actions, if any $this->actions = new FieldSet(); // If the form is at final step, create a submit button to perform final actions // The last step doesn't have a next button, so add that action to any step that isn't the final one - if($this->getCurrentStep()->isFinalStep()) { + if($currentStep->isFinalStep()) { $this->actions->push(new FormAction('finish', _t('MultiForm.SUBMIT', 'Submit'))); } else { $this->actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); } // If there is a previous step defined, add the back button - if($this->getCurrentStep()->getPreviousStep()) { - if($this->actions->fieldByName('action_next')) { + if($currentStep->getPreviousStep() && $currentStep->canGoBack()) { + // If there is a next step, insert the action before the next action + if($currentStep->getNextStep()) { $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_next'); - } elseif($this->actions->fieldByName('action_finish')) { - $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_finish'); + // Assume that this is the last step, insert the action before the finish action } else { - $this->actions->push(new FormAction('prev', _t('MultiForm.BACK', 'Back'))); + $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_finish'); } } // Merge any extra action fields defined on the step - $this->actions->merge($this->getCurrentStep()->getExtraActions()); + $this->actions->merge($currentStep->getExtraActions()); } /** @@ -395,7 +400,7 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function prev($data, $form) { - if(!$this->getCurrentStep()->getPreviousStep()) { + if(!$this->getCurrentStep()->getPreviousStep() && !$this->getCurrentStep()->canGoBack()) { Director::redirectBack(); return false; } diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 5eec4f7..ffb2303 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -42,6 +42,14 @@ class MultiFormStep extends DataObject { */ protected static $is_final_step = false; + /** + * This variable determines whether a user can use + * the "back" action from this step. + * + * @var boolean + */ + protected static $can_go_back = true; + /** * Title of this step. * @@ -232,6 +240,16 @@ class MultiFormStep extends DataObject { // ##################### Utility #################### + /** + * Determines whether the user is able to go back using the "action_back" + * form action, based on the boolean value of $can_go_back. + * + * @return boolean + */ + public function canGoBack() { + return $this->stat('can_go_back'); + } + /** * Determines whether this step is the final step in the multi-step process or not, * based on the variable $is_final_step - which must be defined on at least one step. From 2bb5983f19bc08a1fde89348b58a46171baa480a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 27 May 2008 04:54:24 +0000 Subject: [PATCH 51/86] Added TODO for what $can_go_back doesn't do yet! --- code/MultiFormStep.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index ffb2303..d179c99 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -45,6 +45,12 @@ class MultiFormStep extends DataObject { /** * This variable determines whether a user can use * the "back" action from this step. + * + * @TODO This does not check if the arbitrarily chosen step + * using the step indicator is actually a previous step, so + * unless you remove the link from the indicator template, or + * type in StepID=23 to the address bar you can still go back + * using the step indicator. * * @var boolean */ From 4d0e9491ccfa3ee0874bfb38a59eac398e1b742f Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 5 Jun 2008 01:12:08 +0000 Subject: [PATCH 52/86] ENHANCEMENT Don't validate if hitting the "prev" action --- code/MultiForm.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 4c5ab72..5dec3d9 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -105,8 +105,11 @@ abstract class MultiForm extends Form { $urlMethod = $this->stat('url_type'); $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); - // Set up validator for the step - $this->setValidator(); + // Set up validator for the step + // @TODO Fix this hack to stop validation if hitting the "prev" action + if(empty($_REQUEST['action_prev'])) { + $this->setValidator(); + } // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) if($currentStep->loadData()) { From e4e43ee4afb3c8fc9988c27f193a14622c0ab44e Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 5 Jun 2008 01:13:55 +0000 Subject: [PATCH 53/86] ENHANCEMENT prev() now saves the current step data, exactly as next() does --- code/MultiForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 5dec3d9..bd002e5 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -411,6 +411,9 @@ abstract class MultiForm extends Form { // Switch the step to the previous! $prevStepClass = $this->getCurrentStep()->getPreviousStep(); + // Save the form data for the current step + $this->save($data); + // Get the previous step of the class instance returned from $currentStep->getPreviousStep() $prevStep = DataObject::get_one($prevStepClass, "SessionID = {$this->session->ID}"); From fe8b3f57ef0c8dcc2e83709e54e3d039fd7861f1 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 9 Jun 2008 02:49:06 +0000 Subject: [PATCH 54/86] Added some useful information to README --- README | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README b/README index 042e306..6f77782 100644 --- a/README +++ b/README @@ -1,15 +1,15 @@ ------------------------------- -Multistep Form Module ------------------------------- +----------------------------- +SilverStripe MultiForm module +----------------------------- April 3rd, 2008 -Managing multiple form steps ("wizard") with automatic session-saving -of data, and versatile start/finish customizations. +This module acts as a "wizard" for multiple form steps with automatic +session-saving of data, and versatile start/finish customizations. REQUIREMENTS ------------ -SilverStripe 2.2.2 +SilverStripe 2.2.2 or higher is required. DOCUMENTATION ------------- @@ -23,5 +23,8 @@ INSTALL NOTES ----- +Please read the documentation (link above), for instructions on using +this module. + If you ever add a new step, you must run db/build?flush=1, as the step requires -a class name in the database to store it's session state, and data. \ No newline at end of file +a class name in the database to store it's session state, and data. From 35f2de5de332ee16b34fa6b657add5bad71d4bda Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 18 Jun 2008 08:07:11 +0000 Subject: [PATCH 55/86] ENHANCEMENT MultiFormStep->form can now be accessed to return the form that the step was created on --- code/MultiForm.php | 5 +++++ code/MultiFormStep.php | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index bd002e5..583c159 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -92,6 +92,9 @@ abstract class MultiForm extends Form { // Set the step returned above as the current step $this->setCurrentStep($currentStep); + + // Set the form of the step to this form instance + $currentStep->form = $this; // Set up the fields for the current step $this->setFields($currentStep->getFields()); @@ -158,6 +161,7 @@ abstract class MultiForm extends Form { /** * Set the step passed in as the current step. + * * @param MultiFormStep $step A subclass of MultiFormStep */ protected function setCurrentStep($step) { @@ -167,6 +171,7 @@ abstract class MultiForm extends Form { /** * Accessor method to $this->session. + * * @return MultiFormSession */ function getSession() { diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index d179c99..9b951d5 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -65,6 +65,13 @@ class MultiFormStep extends DataObject { */ protected $title; + /** + * Form class that this step is directly related to. + * + * @var MultiForm subclass + */ + protected $form; + /** * Form fields to be rendered with this step. * (Form object is created in {@link MultiForm}. @@ -244,6 +251,15 @@ class MultiFormStep extends DataObject { } } + /** + * Sets the form that this step is directly related to. + * + * @param MultiForm subclass $form + */ + public function setForm($form) { + $this->form = $form; + } + // ##################### Utility #################### /** From 295cd5077e707ca0b7cd47738c833667fcbacb1a Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 19 Jun 2008 10:29:47 +0000 Subject: [PATCH 56/86] API CHANGE Ticket #2562 - Cleaner instanciation of MultiForm subclass, init() is removed with the move of session/step initialization to __construct() --- code/MultiForm.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 583c159..58a747b 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -74,15 +74,21 @@ abstract class MultiForm extends Form { ); /** - * Perform actions when the multiform is first started. - * - * It does NOT work like a normal controller init()! It has to be explicity called when MultiForm - * is intanciated on your controller. @TODO perhaps find a better name, that doesn't quite conflict. - * - * This method sets up the session, figures out the current step, sets the current step, then - * takes the fields, actions and validation (if any) for the step, setting up the form. + * We start the MultiForm here. Before we start the MultiForm process, + * like setting up the session, we must pass in empty FieldSet objects + * to the Form->__construct(), as it requires them to be present. + * + * @param ContentController instance $controller Controller this form is created on + * @param string $name The form name, typically the same as the method name */ - public function init() { + public function __construct($controller, $name) { + + // We don't have any fields directly on here, they're on the step classes + $fields = new FieldSet(); + $actions = new FieldSet(); + + parent::__construct($controller, $name, $fields, $actions); + // Set up the session for this MultiForm instance $this->setSession(); From 0f2bfb2cc5268777c0af9991924e6424e6a4c587 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 19 Jun 2008 10:32:15 +0000 Subject: [PATCH 57/86] MINOR tests updated to reflect API change in r56597 --- tests/MultiFormTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index 517321e..fc574c8 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -31,8 +31,7 @@ class MultiFormTest extends SapphireTest { * in our tests by assigning $this->session */ function setUp() { - $this->form = new MultiFormTestClass(new Controller(), 'Form', new FieldSet(), new FieldSet()); - $this->form->init(); + $this->form = new MultiFormTestClass(new Controller(), 'Form'); } /** From 3b9f092ac152dc63cdf7712d551134dc14d67e9b Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 23 Jun 2008 05:06:36 +0000 Subject: [PATCH 58/86] BUGFIX We now call $nextStep->Link and $prevStep->Link() instead of $this->getCurrentStep()->Link() on MultiForm->next() and MultiForm->prev() which is confusing, and doesn't always work --- code/MultiForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 58a747b..66db91f 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -399,7 +399,7 @@ abstract class MultiForm extends Form { $this->setCurrentStep($nextStep); // Redirect to the next step - Director::redirect($this->getCurrentStep()->Link()); + Director::redirect($nextStep->Link()); return; } @@ -432,7 +432,7 @@ abstract class MultiForm extends Form { $this->setCurrentStep($prevStep); // Redirect to the previous step - Director::redirect($this->getCurrentStep()->Link()); + Director::redirect($prevStep->Link()); return; } From fce12220b26e9a06f7aa7a5b784d979840228592 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Tue, 1 Jul 2008 23:57:07 +0000 Subject: [PATCH 59/86] ENHANCEMENT Allowed static $ignored_fields to be overloaded on subclass of MultiForm, so specific fields can be ignored --- code/MultiForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 66db91f..5bb4f6e 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -449,7 +449,7 @@ abstract class MultiForm extends Form { $currentStep = $this->getCurrentStep(); if(is_array($data)) { foreach($data as $field => $value) { - if(in_array($field, self::$ignored_fields) || self::is_action_field($field)) { + if(in_array($field, $this->stat('ignored_fields')) || self::is_action_field($field)) { unset($data[$field]); } } From 9da9a611a0b5e40886ef417fb6336e66e5ce3363 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 2 Jul 2008 00:39:40 +0000 Subject: [PATCH 60/86] BUGFIX Made MultiFormPurgeTask greatly simplified, and workable - it simply deletes all session data after days specified in $session_expiry_days --- code/MultiFormPurgeTask.php | 52 ++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/code/MultiFormPurgeTask.php b/code/MultiFormPurgeTask.php index b78d53e..c406c97 100644 --- a/code/MultiFormPurgeTask.php +++ b/code/MultiFormPurgeTask.php @@ -13,44 +13,38 @@ class MultiFormPurgeTask extends DailyTask { /** - * Days after which unfinished sessions - * expire and are automatically deleted - * by a cronjob/ScheduledTask. + * Days after which sessions expire and + * are automatically deleted. * * @usedby {@link MultiFormPurgeTask} * @var int */ public static $session_expiry_days = 7; - + + /** + * Run this cron task. + * + * Go through all MultiFormSession records that + * are older than the days specified in $session_expiry_days + * and delete them. + */ public function run() { - $controllers = ClassInfo::subclassesFor('MultiForm'); - - if($controllers) foreach($controllers as $controllerClass) { - $controller = new $controllerClass(); - $sessions = $controller->getExpiredSessions(); - $sessionDeleteCount = 0; - if($sessions) foreach($sessions as $session) { - $session->purgeStoredData(); - if($session->delete()) $sessionDeleteCount++; - } + $sessions = $this->getExpiredSessions(); + if($sessions) foreach($sessions as $session) { + $session->delete(); } } - + + /** + * Return all MultiFormSession database records that are older than + * the days specified in $session_expiry_days + * + * @return DataObjectSet + */ protected function getExpiredSessions() { - $sessions = new DataObjectSet(); - - $implementors = Object::implementors_for_extension('MultiFormObjectDecorator'); - if($implementors) foreach($implementors as $implementorClass) { - $sessions->merge( - DataObject::get( - $implementorClass, - "`{$implementorClass}`.`MultiFormIsTemporary` = 1 - AND DATEDIFF(NOW(), `{$implementorClass}`.`Created`) > " . self::$session_expiry_days - ) - ); - } - - return $sessions; + return DataObject::get( + 'MultiFormSession', + "DATEDIFF(NOW(), `MultiFormSession`.`Created`) > " . self::$session_expiry_days); } } From 58b7bb2210a51e60a09f0573aa4a4be05a5bfa58 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 2 Jul 2008 00:54:22 +0000 Subject: [PATCH 61/86] MINOR echo out the amount of session records that were deleted on MultiFormPurgeTask --- code/MultiFormPurgeTask.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/MultiFormPurgeTask.php b/code/MultiFormPurgeTask.php index c406c97..5fecd7f 100644 --- a/code/MultiFormPurgeTask.php +++ b/code/MultiFormPurgeTask.php @@ -30,9 +30,11 @@ class MultiFormPurgeTask extends DailyTask { */ public function run() { $sessions = $this->getExpiredSessions(); + $delCount = 0; if($sessions) foreach($sessions as $session) { - $session->delete(); + if($session->delete()) $delCount++; } + echo $delCount . ' session records deleted that were older than ' . self::$session_expiry_days . ' days.'; } /** From f362ed07be2aa1b9bacdc097aad8f9b9ac4b6994 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 4 Jul 2008 01:07:33 +0000 Subject: [PATCH 62/86] BUGFIX Disable security token inherited from Form. We do not require this as we tie a MultiForm instance to a MultiFormSession via a URL parameter --- code/MultiForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/MultiForm.php b/code/MultiForm.php index 5bb4f6e..2a3c4b9 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -124,6 +124,9 @@ abstract class MultiForm extends Form { if($currentStep->loadData()) { $this->loadDataFrom($currentStep->loadData()); } + + // Disable security token - we tie a form to a session ID so this is not required + $this->disableSecurityToken(); } /** From 80e71b5ccf77c6a8f2f69b412f54dd7395929c49 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 9 Jul 2008 06:34:28 +0000 Subject: [PATCH 63/86] BUGFIX SQL injection possibility fix on MultiForm->getSessionRecordByID() --- code/MultiForm.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 2a3c4b9..2ba1e0a 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -257,8 +257,9 @@ abstract class MultiForm extends Form { * @param int|string $id The ID of the record to retrieve * @return MultiFormSession */ - function getSessionRecordByID($id) { - return DataObject::get_one('MultiFormSession', "MultiFormSession.ID = $id AND IsComplete = 0"); + function getSessionRecordByID($id) { + $SQL_id = (int)$id; + return DataObject::get_one('MultiFormSession', "MultiFormSession.ID = {$SQL_id} AND IsComplete = 0"); } /** From 78080c6238c295cb66ce5f72a030a819824e0d97 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 03:21:51 +0000 Subject: [PATCH 64/86] ENHANCEMENT Correct use of parent::__construct() so that fields, actions and validation are correctly set without extra code to set them on MultiForm --- code/MultiForm.php | 88 ++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 53 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 2ba1e0a..6182b08 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -87,14 +87,31 @@ abstract class MultiForm extends Form { $fields = new FieldSet(); $actions = new FieldSet(); - parent::__construct($controller, $name, $fields, $actions); - // Set up the session for this MultiForm instance $this->setSession(); - + // Get the current step available (Note: either returns an existing // step or creates a new one if none available) $currentStep = $this->getCurrentStep(); + + // Set up the fields for the current step + $fields = $currentStep->getFields(); + + // Set up the actions for the current step + $actions = $this->actionsFor($currentStep); + + // Set up validation (if necessary) {@TODO find a better way instead + // of hardcoding a check for action_prev in order to prevent validation + // when hitting the back button + $validator = new Validator(); + if(empty($_REQUEST['action_prev'])) { + if($this->getCurrentStep()->getValidator()) { + $validator = $this->getCurrentStep()->getValidator(); + } + } + + // Give the fields, actions, and validation for the current step back to the parent Form class + parent::__construct($controller, $name, $fields, $actions, $validator); // Set the step returned above as the current step $this->setCurrentStep($currentStep); @@ -102,24 +119,12 @@ abstract class MultiForm extends Form { // Set the form of the step to this form instance $currentStep->form = $this; - // Set up the fields for the current step - $this->setFields($currentStep->getFields()); - - // Set up the actions for the current step - $this->setActions($currentStep); - // Set a hidden field in the form to identify this session. // Depending on what has been configured for $url_type, we // find an encrypted hash by default to identify the session. $urlMethod = $this->stat('url_type'); $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); - // Set up validator for the step - // @TODO Fix this hack to stop validation if hitting the "prev" action - if(empty($_REQUEST['action_prev'])) { - $this->setValidator(); - } - // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) if($currentStep->loadData()) { $this->loadDataFrom($currentStep->loadData()); @@ -261,35 +266,9 @@ abstract class MultiForm extends Form { $SQL_id = (int)$id; return DataObject::get_one('MultiFormSession', "MultiFormSession.ID = {$SQL_id} AND IsComplete = 0"); } - - /** - * Set the fields for this form. - * - * To ensure that each field knows what form it's related to, - * we call setForm($this) on each field. - * - * @param FieldSet $fields - */ - function setFields($fields) { - foreach($fields as $field) $field->setForm($this); - $this->fields = $fields; - } /** - * Set up the validation for this form. - * - * We check if the validator exists first, as validation on - * each step is optional. - */ - function setValidator() { - if($validator = $this->getCurrentStep()->getValidator()) { - $this->validator = $validator; - $this->validator->setForm($this); - } - } - - /** - * Set the actions for this form. + * Build a FieldSet of the FormAction fields used for the given step. * * If the current step is the final step, we push in a submit button, which * calls the action {@link finish()} to finalise the submission. Otherwise, @@ -304,33 +283,36 @@ abstract class MultiForm extends Form { * then that set of actions is appended to the end of the actions FieldSet we * have created in this method. * - * @param $currentStep Subclass of MultiFormStep to set actions for + * @param $currentStep Subclass of MultiFormStep + * @return FieldSet of FormAction objects */ - function setActions($currentStep) { + function actionsFor($step) { // Create default multi step actions (next, prev), and merge with extra actions, if any - $this->actions = new FieldSet(); + $actions = new FieldSet(); // If the form is at final step, create a submit button to perform final actions // The last step doesn't have a next button, so add that action to any step that isn't the final one - if($currentStep->isFinalStep()) { - $this->actions->push(new FormAction('finish', _t('MultiForm.SUBMIT', 'Submit'))); + if($step->isFinalStep()) { + $actions->push(new FormAction('finish', _t('MultiForm.SUBMIT', 'Submit'))); } else { - $this->actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); + $actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); } // If there is a previous step defined, add the back button - if($currentStep->getPreviousStep() && $currentStep->canGoBack()) { + if($step->getPreviousStep() && $step->canGoBack()) { // If there is a next step, insert the action before the next action - if($currentStep->getNextStep()) { - $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_next'); + if($step->getNextStep()) { + $actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_next'); // Assume that this is the last step, insert the action before the finish action } else { - $this->actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_finish'); + $actions->insertBefore(new FormAction('prev', _t('MultiForm.BACK', 'Back')), 'action_finish'); } } // Merge any extra action fields defined on the step - $this->actions->merge($currentStep->getExtraActions()); + $actions->merge($step->getExtraActions()); + + return $actions; } /** From e843930e6945c41a33bbd3c642e56611f52b1832 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 03:27:44 +0000 Subject: [PATCH 65/86] MINOR added @TODO note for MultiFormTest --- tests/MultiFormTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index fc574c8..c390659 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -20,6 +20,8 @@ * is a subclass of DataObject. This is a bit of a pain, but it's required for * the database to store the step data for each step, which is very important! * + * @TODO make use of .yml file to populate test db data instead? + * * So, if you're going to create some new tests, and want to use some test classes, * make sure to use the ones mentioned above. */ From 8d138f4059b426d0bcdd226a66e1ea597931592e Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 03:34:27 +0000 Subject: [PATCH 66/86] MINOR Removed 3 useless lines of code --- code/MultiForm.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 6182b08..1eeb595 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -83,10 +83,6 @@ abstract class MultiForm extends Form { */ public function __construct($controller, $name) { - // We don't have any fields directly on here, they're on the step classes - $fields = new FieldSet(); - $actions = new FieldSet(); - // Set up the session for this MultiForm instance $this->setSession(); From 6abbe5b44fbb208d1cbbd91773d4f237664e30fc Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 04:40:18 +0000 Subject: [PATCH 67/86] MINOR where[] can't be used for reading --- code/MultiFormObjectDecorator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/MultiFormObjectDecorator.php b/code/MultiFormObjectDecorator.php index fd4d718..8168d95 100644 --- a/code/MultiFormObjectDecorator.php +++ b/code/MultiFormObjectDecorator.php @@ -48,7 +48,7 @@ class MultiFormObjectDecorator extends DataObjectDecorator { * @return boolean */ protected function wantsTemporary($query) { - foreach($query->where[] as $whereClause) { + foreach($query->where as $whereClause) { if($whereClause == "`{$query->from[0]}`.`MultiFormIsTemporary` = 1") return true; } return false; From 69b5cd6686844d6f331c2109b8e79b2a3a4ce34d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 05:14:04 +0000 Subject: [PATCH 68/86] Don't validate if not necessary - merged from branches/kiwiselect --- code/MultiForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 1eeb595..6ca10bf 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -99,7 +99,7 @@ abstract class MultiForm extends Form { // Set up validation (if necessary) {@TODO find a better way instead // of hardcoding a check for action_prev in order to prevent validation // when hitting the back button - $validator = new Validator(); + $validator = ''; if(empty($_REQUEST['action_prev'])) { if($this->getCurrentStep()->getValidator()) { $validator = $this->getCurrentStep()->getValidator(); @@ -107,7 +107,7 @@ abstract class MultiForm extends Form { } // Give the fields, actions, and validation for the current step back to the parent Form class - parent::__construct($controller, $name, $fields, $actions, $validator); + parent::__construct($controller, $name, $fields, $actions, $validator ? $validator : null); // Set the step returned above as the current step $this->setCurrentStep($currentStep); From 1b662ff9e1f536cbe6e43c88abacaf3a05e67df0 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 05:22:06 +0000 Subject: [PATCH 69/86] BUGFIX $this->form wasn't accessible on MultiFormStep because it wasn't being set by MultiForm->__construct() early enough in the piece --- code/MultiForm.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 6ca10bf..473627e 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -90,6 +90,12 @@ abstract class MultiForm extends Form { // step or creates a new one if none available) $currentStep = $this->getCurrentStep(); + // Set the step returned above as the current step + $this->setCurrentStep($currentStep); + + // Set the form of the step to this form instance + $currentStep->form = $this; + // Set up the fields for the current step $fields = $currentStep->getFields(); @@ -108,13 +114,7 @@ abstract class MultiForm extends Form { // Give the fields, actions, and validation for the current step back to the parent Form class parent::__construct($controller, $name, $fields, $actions, $validator ? $validator : null); - - // Set the step returned above as the current step - $this->setCurrentStep($currentStep); - // Set the form of the step to this form instance - $currentStep->form = $this; - // Set a hidden field in the form to identify this session. // Depending on what has been configured for $url_type, we // find an encrypted hash by default to identify the session. From a42bb6103b710e2fe5c8f21792026983590095d0 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 21:06:59 +0000 Subject: [PATCH 70/86] BUGFIX Removing url_type which isnt very useful MINOR documentation updates ENHANCEMENT return value on MultiForm->setCurrentStep() MINOR code cleanup in general --- code/MultiForm.php | 122 ++++++++++++-------------------------- code/MultiFormSession.php | 46 +------------- code/MultiFormStep.php | 23 +++---- 3 files changed, 47 insertions(+), 144 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 473627e..ad2b8c7 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -16,44 +16,27 @@ abstract class MultiForm extends Form { /** - * A session object stored in the database, which might link - * to further temporary {@link DataObject}s. + * A session object stored in the database, to identify and store + * data for this MultiForm instance. * * @var MultiFormSession */ protected $session; /** - * Defines which subclass of {@link MultiFormStep} starts the form - - * needs to be defined for the controller to work correctly + * Defines which subclass of {@link MultiFormStep} should be the first + * step in the multi-step process. * * @var string Classname of a {@link MultiFormStep} subclass */ - protected static $start_step; - - /** - * Define what type of URL you want to use throughout the step process. - * - * By default, we store a hash, for example: http://mysite.com/my-form/?MultiFormSessionID=de9f2c7fd25e1b3afad3e850bd17d9b100db4b3 - * Alternatively, if you set this variable to "ID", then you get ?MultiFormSessionID=20 - * - * The ID is not as secure as the hash, but it all depends on your set up. - * If you're going to add security, such as check the SubmitterID on init - * of the MultiForm and use "ID" for this parameter, then security should be fine. - * - * In any other case, where there's no Member tied to a MultiFormSession, using - * the Hash is the recommended approach. - * - * @var $url_type either "ID", or "Hash" - */ - protected static $url_type = 'Hash'; + protected static $start_step; /** * Set the casting for these fields. * * @var array */ - static $casting = array( + public static $casting = array( 'CompletedStepCount' => 'Int', 'TotalStepCount' => 'Int', 'CompletedPercent' => 'Float' @@ -66,7 +49,7 @@ abstract class MultiForm extends Form { * * @var array */ - static $ignored_fields = array( + public static $ignored_fields = array( 'url', 'executeForm', 'MultiFormSessionID', @@ -105,7 +88,7 @@ abstract class MultiForm extends Form { // Set up validation (if necessary) {@TODO find a better way instead // of hardcoding a check for action_prev in order to prevent validation // when hitting the back button - $validator = ''; + $validator = null; if(empty($_REQUEST['action_prev'])) { if($this->getCurrentStep()->getValidator()) { $validator = $this->getCurrentStep()->getValidator(); @@ -113,26 +96,25 @@ abstract class MultiForm extends Form { } // Give the fields, actions, and validation for the current step back to the parent Form class - parent::__construct($controller, $name, $fields, $actions, $validator ? $validator : null); + parent::__construct($controller, $name, $fields, $actions, $validator); - // Set a hidden field in the form to identify this session. - // Depending on what has been configured for $url_type, we - // find an encrypted hash by default to identify the session. - $urlMethod = $this->stat('url_type'); - $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->$urlMethod)); + // Set a hidden field in our form with an encrypted hash to identify this session. + $this->fields->push(new HiddenField('MultiFormSessionID', false, $this->session->Hash)); - // If there is form data, we populate it here (CAUTION: loadData() MUST unserialize first!) + // If there is saved data for the current step, we load it into the form it here + //(CAUTION: loadData() MUST unserialize first!) if($currentStep->loadData()) { $this->loadDataFrom($currentStep->loadData()); } - // Disable security token - we tie a form to a session ID so this is not required + // Disable security token - we tie a form to a session ID instead $this->disableSecurityToken(); } /** - * Accessor method to $this->controller - * Returns the controller this form was instanciated on. + * Accessor method to $this->controller. + * + * @return Controller this MultiForm was instanciated on. */ public function getController() { return $this->controller; @@ -173,10 +155,11 @@ abstract class MultiForm extends Form { * Set the step passed in as the current step. * * @param MultiFormStep $step A subclass of MultiFormStep + * @return boolean The return value of write() */ protected function setCurrentStep($step) { $this->session->CurrentStepID = $step->ID; - $this->session->write(); + return $this->session->write(); } /** @@ -191,11 +174,6 @@ abstract class MultiForm extends Form { /** * Set up the session. * - * First of all we check if MultiFormSessionID is set in the URL, - * then we determine what URL type has been set (default is "Hash"). - * Knowing this, we can retrieve the session record from the database - * by a particular method (getSessionRecordByHash, or getSessionRecordByID). - * * If MultiFormSessionID isn't set, we assume that this is a new * multiform that requires a new session record to be created. * @@ -206,65 +184,37 @@ abstract class MultiForm extends Form { * Perhaps it would be best dealt with on a separate class? */ protected function setSession() { - $urlType = $this->stat('url_type'); - // If there's a MultiFormSessionID variable set, find that, otherwise create a new session if(isset($_GET['MultiFormSessionID'])) { - switch($urlType) { - case 'Hash': - $this->session = $this->getSessionRecordByHash($_GET['MultiFormSessionID']); - break; - case 'ID': - $this->session = $this->getSessionRecordByID($_GET['MultiFormSessionID']); - break; - - default: - user_error('MultiForm::init(): Please define a correct value for $url_type on ' . $this->class, E_USER_ERROR); - break; - } + $this->session = $this->getSessionRecord($_GET['MultiFormSessionID']); } // If there was no session found, create a new one instead if(!$this->session) { - // @TODO fix the fact that you can continually refresh on the first step creating new records $this->session = new MultiFormSession(); $this->session->write(); } - // We have to have an ID, before we can hash the ID of the session. @TODO a better way here? - if($urlType == 'Hash') { - if(!$this->session->Hash) { - $this->session->Hash = sha1($this->session->ID . '-' . microtime()); - $this->session->write(); // I guess we could hash something else than the ID, this is a bit ugly... - } + // Create encrypted identification to the session instance if it doesn't exist + if(!$this->session->Hash) { + $this->session->Hash = sha1($this->session->ID . '-' . microtime()); + $this->session->write(); } } /** - * Return an instance of MultiFormSession from the database by a single - * record with the hash passed into this method. + * Return an instance of MultiFormSession. * - * @param string $hash The Hash field of the record to retrieve + * @param string $hash The unique, encrypted hash to identify the session * @return MultiFormSession */ - function getSessionRecordByHash($hash) { + function getSessionRecord($hash) { $SQL_hash = Convert::raw2sql($hash); return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); } - - /** - * Return an instance of MultiFormSession from the database by it's ID. - * - * @param int|string $id The ID of the record to retrieve - * @return MultiFormSession - */ - function getSessionRecordByID($id) { - $SQL_id = (int)$id; - return DataObject::get_one('MultiFormSession', "MultiFormSession.ID = {$SQL_id} AND IsComplete = 0"); - } - + /** - * Build a FieldSet of the FormAction fields used for the given step. + * Build a FieldSet of the FormAction fields for the given step. * * If the current step is the final step, we push in a submit button, which * calls the action {@link finish()} to finalise the submission. Otherwise, @@ -317,6 +267,8 @@ abstract class MultiForm extends Form { * subclass template) to see if one is available to render the form with. If * any of those don't exist, look for a default Form template to render * with instead. + * + * @return SSViewer object to render the template with */ function forTemplate() { return $this->renderWith(array( @@ -444,15 +396,15 @@ abstract class MultiForm extends Form { /** * Add the MultiFormSessionID variable to the URL on form submission. - * We use this to determine what session the multiform is currently using. + * This is a means to persist the session, by adding it's identification + * to the URL, which ties it back to this MultiForm instance. * * @return string */ - function FormAction() { - $urlMethod = $this->stat('url_type'); + function FormAction() { $action = parent::FormAction(); $action .= (strpos($action, '?')) ? '&' : '?'; - $action .= "MultiFormSessionID={$this->session->$urlMethod}"; + $action .= "MultiFormSessionID={$this->session->Hash}"; return $action; } @@ -472,7 +424,7 @@ abstract class MultiForm extends Form { 'ID' => $firstStep->ID, 'ClassName' => $firstStep->class, 'Title' => $firstStep->title ? $firstStep->title : $firstStep->class, - 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, + 'SessionID' => $this->session->Hash, 'LinkingMode' => ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); $stepsFound->push(new ArrayData($templateData)); @@ -501,7 +453,7 @@ abstract class MultiForm extends Form { 'ID' => $nextStep->ID, 'ClassName' => $nextStep->class, 'Title' => $nextStep->title ? $nextStep->title : $nextStep->class, - 'SessionID' => ($this->stat('url_type') == 'ID') ? $this->session->ID : $this->session->Hash, + 'SessionID' => $this->session->Hash, 'LinkingMode' => ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link' ); } else { diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index b85297d..ff0a8e9 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -56,55 +56,13 @@ class MultiFormSession extends DataObject { // delete dependent form steps and relation $steps = $this->FormSteps(); if($steps) foreach($steps as $step) { - $steps->remove($step); + $steps->remove($step); // @TODO not sure if this is required (does delete() remove the relation too?) $step->delete(); } parent::onBeforeDelete(); } - - /** - * Get all the temporary objects, and set them as temporary, writing - * them back to the database. - */ - public function markTemporaryDataObjectsFinished() { - $temporaryObjects = $this->getTemporaryDataObjects(); - if($temporaryObjects) foreach($temporaryObjects as $obj) { - $obj->MultiFormIsTemporary = 0; - $obj->write(); - } - } - - /** - * Get all classes that implement the MultiFormObjectDecorator, - * find the records for each and merge them together into a - * DataObjectSet. - * - * @return DataObjectSet - */ - public function getTemporaryDataObjects() { - $implementors = Object::get_implementors_for_extension('MultiFormObjectDecorator'); - $objs = new DataObjectSet(); - if($implementors) foreach($implementors as $implementorClass) { - $objs->merge( - DataObject::get($implementorClass, "MultiFormSessionID = {$this->ID}") - ); - } - - return $objs; - } - - /** - * Remove all related data, either serialized - * in $Data property, or in related stored - * DataObjects. - * - * @return boolean - */ - public function purgeStoredData() { - die('MultiFormSession->purgeStoredData(): Not implemented yet'); - } - + } ?> \ No newline at end of file diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 9b951d5..7729fe0 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -1,7 +1,7 @@ Session()->Hash ? $this->Session()->Hash : $this->Session()->ID; - return Controller::curr()->Link() . '?MultiFormSessionID=' . $id; + return Controller::curr()->Link() . '?MultiFormSessionID=' . $this->Session()->Hash; } /** @@ -140,12 +135,8 @@ class MultiFormStep extends DataObject { * * You need to overload this method onto your own * step if you require custom loading. An example - * would be selective loading specific fields, or - * filtering out fields that don't require loading. - * - * This method is called on {@link MultiForm} inside - * the init() method, to load the data by default (if - * it exists, back into the form). + * would be selective loading specific fields, leaving + * others that are not required. * * @return array */ @@ -243,6 +234,8 @@ class MultiFormStep extends DataObject { /** * Retrieves the previous step class record from the database. * + * This will only return a record if you've previously been on the step. + * * @return MultiFormStep subclass */ public function getPreviousStepFromDatabase() { @@ -291,7 +284,7 @@ class MultiFormStep extends DataObject { * @return boolean */ public function isCurrentStep() { - if($this->class == $this->Session()->CurrentStep()->class) return true; + return ($this->class == $this->Session()->CurrentStep()->class) ? true : false; } } From 1ba27d9c7d4623da20b465ab0743743aeedd48b1 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Mon, 21 Jul 2008 21:07:48 +0000 Subject: [PATCH 71/86] MINOR Removed MultiFormSession::[Data] which is never used --- code/MultiFormSession.php | 1 - 1 file changed, 1 deletion(-) diff --git a/code/MultiFormSession.php b/code/MultiFormSession.php index ff0a8e9..9adafcd 100644 --- a/code/MultiFormSession.php +++ b/code/MultiFormSession.php @@ -13,7 +13,6 @@ class MultiFormSession extends DataObject { static $db = array( - 'Data' => 'Text', // stores serialized maps with all session information 'Hash' => 'Varchar(40)', // cryptographic hash identification to this session 'IsComplete' => 'Boolean' // flag to determine if this session is marked completed ); From e2d8ace25010c31f779e4cec34dfa51ebcbe807d Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 21 Aug 2008 08:17:04 +0000 Subject: [PATCH 72/86] Fixed multi form test --- tests/MultiFormTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index c390659..584eca9 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -30,7 +30,7 @@ class MultiFormTest extends SapphireTest { /** * Set up the instance of MultiForm, writing a record * to the database for this test. We persist the object - * in our tests by assigning $this->session + * in our tests by assigning $this->getSession() */ function setUp() { $this->form = new MultiFormTestClass(new Controller(), 'Form'); @@ -47,7 +47,7 @@ class MultiFormTest extends SapphireTest { */ function testInitialisingForm() { $this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0)); - $this->assertTrue(is_numeric($this->form->session->ID) && ($this->form->session->ID > 0)); + $this->assertTrue(is_numeric($this->form->getSession()->ID) && ($this->form->getSession()->ID > 0)); $this->assertEquals('MultiFormTestStepOne', $this->form->getStartStep()); } @@ -71,7 +71,7 @@ class MultiFormTest extends SapphireTest { * this session. These directives can be found on {@link MultiFormSession->onBeforeWrite()} */ function tearDown() { - $this->form->session->delete(); + $this->form->getSession()->delete(); } } From 2e9d07cde26b154882c2528ff4f98bc58f769279 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 17 Sep 2008 00:06:17 +0000 Subject: [PATCH 73/86] MINOR Added or edited README files, added LICENSE and CHANGELOG files --- README | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/README b/README index 6f77782..ee97c33 100644 --- a/README +++ b/README @@ -1,30 +1,35 @@ ------------------------------ +############################################### SilverStripe MultiForm module ------------------------------ +############################################### April 3rd, 2008 This module acts as a "wizard" for multiple form steps with automatic session-saving of data, and versatile start/finish customizations. -REQUIREMENTS ------------- +Maintainer Contact +----------------------------------------------- +Matt Peel (Nickname: mpeel, mattSS) + + +Requirements +----------------------------------------------- SilverStripe 2.2.2 or higher is required. -DOCUMENTATION -------------- +Documentation +----------------------------------------------- http://doc.silverstripe.com/doku.php?id=modules:multiform -INSTALL -------- +Installation Instructions +----------------------------------------------- 1) Copy the "multiform" directory into your main SilverStripe directory 2) run db/build?flush=1 3) View documentation (link above) on how to model your forms into steps -NOTES ------ -Please read the documentation (link above), for instructions on using -this module. - +Usage Overview +----------------------------------------------- If you ever add a new step, you must run db/build?flush=1, as the step requires a class name in the database to store it's session state, and data. + +Known issues: +----------------------------------------------- \ No newline at end of file From 6ac64b13a6b71602268351f6e7f9871052ad2dc4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 17 Sep 2008 23:48:39 +0000 Subject: [PATCH 74/86] Updated README to reflect more accurate information --- README | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README b/README index ee97c33..3412089 100644 --- a/README +++ b/README @@ -9,8 +9,8 @@ session-saving of data, and versatile start/finish customizations. Maintainer Contact ----------------------------------------------- -Matt Peel (Nickname: mpeel, mattSS) - +Sean Harvey (Nickname: sharvey, halkyon) + Requirements ----------------------------------------------- @@ -32,4 +32,5 @@ If you ever add a new step, you must run db/build?flush=1, as the step requires a class name in the database to store it's session state, and data. Known issues: ------------------------------------------------ \ No newline at end of file +----------------------------------------------- +Please check http://open.silverstripe.com for known issues. \ No newline at end of file From c5916ffaf0c3dbe25235522aa152a9ee7e371a95 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 20 Sep 2008 09:22:01 +0000 Subject: [PATCH 75/86] MINOR Made MultiForm test classes implement TestOnly so they're only used for testing purposes --- tests/code/MultiFormTestClass.php | 2 +- tests/code/MultiFormTestStepOne.php | 2 +- tests/code/MultiFormTestStepThree.php | 2 +- tests/code/MultiFormTestStepTwo.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/code/MultiFormTestClass.php b/tests/code/MultiFormTestClass.php index b0d1e56..220295f 100644 --- a/tests/code/MultiFormTestClass.php +++ b/tests/code/MultiFormTestClass.php @@ -1,6 +1,6 @@ Date: Sat, 20 Sep 2008 09:27:38 +0000 Subject: [PATCH 76/86] MINOR Updated MultiFormTest class comments --- tests/MultiFormTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/MultiFormTest.php b/tests/MultiFormTest.php index 584eca9..c6eb1b5 100644 --- a/tests/MultiFormTest.php +++ b/tests/MultiFormTest.php @@ -19,11 +19,6 @@ * of MultiFormStep, which is every step in a form, requires a db/build, as it * is a subclass of DataObject. This is a bit of a pain, but it's required for * the database to store the step data for each step, which is very important! - * - * @TODO make use of .yml file to populate test db data instead? - * - * So, if you're going to create some new tests, and want to use some test classes, - * make sure to use the ones mentioned above. */ class MultiFormTest extends SapphireTest { From c97022314f4de1f51d4616ac7453bb55c09c5ad5 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 20 Sep 2008 09:31:44 +0000 Subject: [PATCH 77/86] MINOR Code syntax formatting and removal of useless comment --- code/MultiFormPurgeTask.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/MultiFormPurgeTask.php b/code/MultiFormPurgeTask.php index 5fecd7f..0eca20e 100644 --- a/code/MultiFormPurgeTask.php +++ b/code/MultiFormPurgeTask.php @@ -16,7 +16,6 @@ class MultiFormPurgeTask extends DailyTask { * Days after which sessions expire and * are automatically deleted. * - * @usedby {@link MultiFormPurgeTask} * @var int */ public static $session_expiry_days = 7; @@ -46,7 +45,8 @@ class MultiFormPurgeTask extends DailyTask { protected function getExpiredSessions() { return DataObject::get( 'MultiFormSession', - "DATEDIFF(NOW(), `MultiFormSession`.`Created`) > " . self::$session_expiry_days); + "DATEDIFF(NOW(), `MultiFormSession`.`Created`) > " . self::$session_expiry_days + ); } } From 228d03592e02699075979c29bdb5f0b5a28ab2e0 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Sat, 20 Sep 2008 09:36:54 +0000 Subject: [PATCH 78/86] MINOR Removed old MultiForm::__construct() comments --- code/MultiForm.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index ad2b8c7..bc883e1 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -57,11 +57,9 @@ abstract class MultiForm extends Form { ); /** - * We start the MultiForm here. Before we start the MultiForm process, - * like setting up the session, we must pass in empty FieldSet objects - * to the Form->__construct(), as it requires them to be present. + * Start the MultiForm instance. * - * @param ContentController instance $controller Controller this form is created on + * @param Controller instance $controller Controller this form is created on * @param string $name The form name, typically the same as the method name */ public function __construct($controller, $name) { From 7968b09fea0df0d969dd0d828046da9509fd78fb Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 1 Oct 2008 18:36:52 +0000 Subject: [PATCH 79/86] ENHANCEMENT Added MultiForm->getSavedSteps() and MultiForm->getSavedStepByClass() ENHANCEMENT Added MultiFormStep->validateStep() for custom validation routines ENHANCEMENT Changed MultiForm->save() to use $form->getData() instead of $data to ensure that all fields are saveable into a DataObject (had trouble with ConfirmedPasswordField returning array instead of string) --- code/MultiForm.php | 173 +++++++++++++++++++++++++---------------- code/MultiFormStep.php | 63 +++++++++------ 2 files changed, 149 insertions(+), 87 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index bc883e1..4a9b524 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -54,7 +54,7 @@ abstract class MultiForm extends Form { 'executeForm', 'MultiFormSessionID', 'SecurityID' - ); + ); /** * Start the MultiForm instance. @@ -83,9 +83,8 @@ abstract class MultiForm extends Form { // Set up the actions for the current step $actions = $this->actionsFor($currentStep); - // Set up validation (if necessary) {@TODO find a better way instead - // of hardcoding a check for action_prev in order to prevent validation - // when hitting the back button + // Set up validation (if necessary) + // @todo find a better way instead of hardcoding a check for action_prev in order to prevent validation when hitting the back button $validator = null; if(empty($_REQUEST['action_prev'])) { if($this->getCurrentStep()->getValidator()) { @@ -183,8 +182,8 @@ abstract class MultiForm extends Form { */ protected function setSession() { // If there's a MultiFormSessionID variable set, find that, otherwise create a new session - if(isset($_GET['MultiFormSessionID'])) { - $this->session = $this->getSessionRecord($_GET['MultiFormSessionID']); + if(isset($_GET['MultiFormSessionID'])) { + $this->session = $this->getSessionRecord($_GET['MultiFormSessionID']); } // If there was no session found, create a new one instead @@ -192,24 +191,59 @@ abstract class MultiForm extends Form { $this->session = new MultiFormSession(); $this->session->write(); } - + // Create encrypted identification to the session instance if it doesn't exist if(!$this->session->Hash) { $this->session->Hash = sha1($this->session->ID . '-' . microtime()); $this->session->write(); } - } - - /** - * Return an instance of MultiFormSession. - * - * @param string $hash The unique, encrypted hash to identify the session - * @return MultiFormSession - */ - function getSessionRecord($hash) { - $SQL_hash = Convert::raw2sql($hash); - return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); - } + } + + /** + * Return an instance of MultiFormSession. + * + * @param string $hash The unique, encrypted hash to identify the session + * @return MultiFormSession + */ + function getSessionRecord($hash) { + $SQL_hash = Convert::raw2sql($hash); + return DataObject::get_one('MultiFormSession', "Hash = '$SQL_hash' AND IsComplete = 0"); + } + + /** + * Get all steps saved in the database for the currently active session, + * in the order they were saved, oldest to newest (automatically ordered by ID). + * If you want a full chain of steps regardless if they've already been saved + * to the database, use {@link getAllStepsLinear()}. + * + * @return DataObjectSet|boolean A set of MultiFormStep subclasses + */ + function getSavedSteps() { + return DataObject::get( + 'MultiFormStep', + sprintf("SessionID = '%s'", + $this->session->ID + ) + ); + } + + /** + * Get a step which was previously saved to the database in the current session. + * Caution: This might cause unexpected behaviour if you have multiple steps + * in your chain with the same classname. + * + * @param string $className Classname of a {@link MultiFormStep} subclass + * @return MultiFormStep + */ + function getSavedStepByClass($className) { + return DataObject::get_one( + 'MultiFormStep', + sprintf("SessionID = '%s' AND ClassName = '%s'", + $this->session->ID, + Convert::raw2sql($className) + ) + ); + } /** * Build a FieldSet of the FormAction fields for the given step. @@ -241,7 +275,7 @@ abstract class MultiForm extends Form { } else { $actions->push(new FormAction('next', _t('MultiForm.NEXT', 'Next'))); } - + // If there is a previous step defined, add the back button if($step->getPreviousStep() && $step->canGoBack()) { // If there is a next step, insert the action before the next action @@ -284,16 +318,16 @@ abstract class MultiForm extends Form { * so you can create your own functionality which handles saving * of all the data collected through each step of the form. * - * @param array $data The request data returned from the form + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ - public function finish($data, $form) { - if(!$this->getCurrentStep()->isFinalStep()) { - Director::redirectBack(); - return false; - } - - // Save the form data for the current step + public function finish($data, $form) { + if(!$this->getCurrentStep()->isFinalStep()) { + Director::redirectBack(); + return false; + } + + // Save the form data for the current step $this->save($data); } @@ -303,24 +337,30 @@ abstract class MultiForm extends Form { * Saves the current step session data to the database, creates the * new step based on getNextStep() of the current step (or fetches * an existing one), resets the current step to the next step, - * then redirects to the newly set step. - * - * @param array $data The request data returned from the form + * then redirects to the newly set step. + * + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ public function next($data, $form) { - if(!$this->getCurrentStep()->getNextStep()) { + // Get the next step class + $nextStepClass = $this->getCurrentStep()->getNextStep(); + + if(!$nextStepClass) { + Director::redirectBack(); + return false; + } + + // custom validation (use MultiFormStep->getValidator() for built-in functionality) + if(!$this->getCurrentStep()->validateStep($data, $form)) { Director::redirectBack(); return false; } - // Get the next step class - $nextStepClass = $this->getCurrentStep()->getNextStep(); - // Save the form data for the current step - $this->save($data); + $this->save($form->getData()); - // Determine whether we can use a step already in the DB, or have to create a new one + // Determine whether we can use a step already in the DB, or have to create a new one if(!$nextStep = DataObject::get_one($nextStepClass, "SessionID = {$this->session->ID}")) { $nextStep = new $nextStepClass(); $nextStep->SessionID = $this->session->ID; @@ -340,9 +380,9 @@ abstract class MultiForm extends Form { * * Retrieves the previous step class, finds the record for that * class in the DB, and sets the current step to that step found. - * Finally, it redirects to that step. - * - * @param array $data The request data returned from the form + * Finally, it redirects to that step. + * + * @param array $data The request data returned from the form * @param object $form The form that the action was called on */ public function prev($data, $form) { @@ -381,7 +421,7 @@ abstract class MultiForm extends Form { $currentStep = $this->getCurrentStep(); if(is_array($data)) { foreach($data as $field => $value) { - if(in_array($field, $this->stat('ignored_fields')) || self::is_action_field($field)) { + if(in_array($field, $this->stat('ignored_fields'))) { unset($data[$field]); } } @@ -395,8 +435,8 @@ abstract class MultiForm extends Form { /** * Add the MultiFormSessionID variable to the URL on form submission. * This is a means to persist the session, by adding it's identification - * to the URL, which ties it back to this MultiForm instance. - * + * to the URL, which ties it back to this MultiForm instance. + * * @return string */ function FormAction() { @@ -407,13 +447,13 @@ abstract class MultiForm extends Form { return $action; } - /** + /** * Determine the steps to show in a linear fashion, starting from the - * first step. We run a recursive function passing the steps found - * by reference to get a listing of the steps. + * first step. We run {@link getAllStepsRecursive} passing the steps found + * by reference to get a listing of the steps. * - * @return DataObjectSet - */ + * @return DataObjectSet + */ public function getAllStepsLinear() { $stepsFound = new DataObjectSet(); @@ -436,6 +476,9 @@ abstract class MultiForm extends Form { * Recursively run through steps using the getNextStep() method on each step * to determine what the next step is, gathering each step along the way. * We stop on the last step, and return the results. + * If a step in the chain was already saved to the database in the current + * session, its used - otherwise a singleton of this step is used. + * Caution: Doesn't consider branching for steps which aren't in the database yet. * * @param $step Subclass of MultiFormStep to find the next step of * @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on @@ -470,7 +513,7 @@ abstract class MultiForm extends Form { } else { return $stepsFound; } - } + } /** * Number of steps already completed (excluding currently started step). @@ -506,26 +549,26 @@ abstract class MultiForm extends Form { */ public function getCompletedPercent() { return (float)$this->CompletedStepCount * 100 / $this->TotalStepCount; - } + } - /** - * Determines whether the field is an action. This checks the string name of the - * field, and not the actual field object of one. The actual checking is done - * by doing a string check to see if "action_" is prefixed to the name of the - * field. For example, in the form system: FormAction('next', 'Next') field + /** + * Determines whether the field is an action. This checks the string name of the + * field, and not the actual field object of one. The actual checking is done + * by doing a string check to see if "action_" is prefixed to the name of the + * field. For example, in the form system: FormAction('next', 'Next') field * gives an ID of "action_next" * * The assumption here is the ID we're checking against has the prefix that we're - * looking for, otherwise this won't work. - * - * @param string $fieldName The name of the field to check is an action - * @param string $prefix The prefix of the string to check for, default is "action_" - * @return boolean - */ - public static function is_action_field($fieldName, $prefix = 'action_') { - if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; - } + * looking for, otherwise this won't work. + * + * @param string $fieldName The name of the field to check is an action + * @param string $prefix The prefix of the string to check for, default is "action_" + * @return boolean + */ + public static function is_action_field($fieldName, $prefix = 'action_') { + if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; + } -} +} ?> \ No newline at end of file diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 7729fe0..58bc91e 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -1,7 +1,7 @@ -httpSubmission()}. * * @return Validator */ public function getValidator() { return false; } - - /** + + /** * Accessor method for $this->title * - * @return string Title of this step - */ - public function getTitle() { - return $this->title; - } + * @return string Title of this step + */ + public function getTitle() { + return $this->title; + } /** * Gets a direct link to this step (only works @@ -157,6 +158,24 @@ class MultiFormStep extends DataObject { $this->write(); } + /** + * Custom validation for a step. In most cases, it should be sufficient + * to have built-in validation through the {@link Validator} class + * on the {@link getValidator()} method. + * Use {@link Form->sessionMessage()} to feed back validation messages + * to the user. Please don't redirect from this method, + * this is taken care of in {@link next()}. + * + * @usedby next() + * + * @param array $data Request data + * @param Form $form + * @return boolean Validation success + */ + public function validateStep($data, $form) { + return true; + } + /** * Returns the first value of $next_step * @@ -274,19 +293,19 @@ class MultiFormStep extends DataObject { public function isFinalStep() { return $this->stat('is_final_step'); } - - /** - * Determines whether the currently viewed step is the current step set in the session. - * This assumes you are checking isCurrentStep() against a data record of a MultiFormStep - * subclass, otherwise it doesn't work. An example of this is using a singleton instance - it won't - * work because there's no data. - * - * @return boolean - */ - public function isCurrentStep() { - return ($this->class == $this->Session()->CurrentStep()->class) ? true : false; - } -} + /** + * Determines whether the currently viewed step is the current step set in the session. + * This assumes you are checking isCurrentStep() against a data record of a MultiFormStep + * subclass, otherwise it doesn't work. An example of this is using a singleton instance - it won't + * work because there's no data. + * + * @return boolean + */ + public function isCurrentStep() { + return ($this->class == $this->Session()->CurrentStep()->class) ? true : false; + } + +} ?> \ No newline at end of file From 053ac04387f20a88f74698f401fe3204cbc7b081 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 1 Oct 2008 21:06:56 +0000 Subject: [PATCH 80/86] MINOR Removed @usedby, it's not a valid phpdoc token --- code/MultiFormStep.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 58bc91e..8e9506e 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -162,12 +162,11 @@ class MultiFormStep extends DataObject { * Custom validation for a step. In most cases, it should be sufficient * to have built-in validation through the {@link Validator} class * on the {@link getValidator()} method. + * * Use {@link Form->sessionMessage()} to feed back validation messages * to the user. Please don't redirect from this method, * this is taken care of in {@link next()}. * - * @usedby next() - * * @param array $data Request data * @param Form $form * @return boolean Validation success From 647a5257e37e0f225655d43fa58f444215774de7 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 1 Oct 2008 21:10:20 +0000 Subject: [PATCH 81/86] MINOR Removed unused static function is_action_field from MultiForm --- code/MultiForm.php | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index 4a9b524..fdf0a61 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -551,24 +551,6 @@ abstract class MultiForm extends Form { return (float)$this->CompletedStepCount * 100 / $this->TotalStepCount; } - /** - * Determines whether the field is an action. This checks the string name of the - * field, and not the actual field object of one. The actual checking is done - * by doing a string check to see if "action_" is prefixed to the name of the - * field. For example, in the form system: FormAction('next', 'Next') field - * gives an ID of "action_next" - * - * The assumption here is the ID we're checking against has the prefix that we're - * looking for, otherwise this won't work. - * - * @param string $fieldName The name of the field to check is an action - * @param string $prefix The prefix of the string to check for, default is "action_" - * @return boolean - */ - public static function is_action_field($fieldName, $prefix = 'action_') { - if(substr((string)$fieldName, 0, strlen($prefix)) == $prefix) return true; - } - } ?> \ No newline at end of file From a8877b780f7a8a2e4fd83e7bd7905efb5fd35e1f Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 1 Oct 2008 22:18:44 +0000 Subject: [PATCH 82/86] ENHANCEMENT Made MultiForm->prev() do the same behaviour for saving data as MultiForm->next() --- code/MultiForm.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/code/MultiForm.php b/code/MultiForm.php index fdf0a61..72cdc5b 100644 --- a/code/MultiForm.php +++ b/code/MultiForm.php @@ -386,16 +386,16 @@ abstract class MultiForm extends Form { * @param object $form The form that the action was called on */ public function prev($data, $form) { - if(!$this->getCurrentStep()->getPreviousStep() && !$this->getCurrentStep()->canGoBack()) { + // Get the previous step class + $prevStepClass = $this->getCurrentStep()->getPreviousStep(); + + if(!$prevStepClass && !$this->getCurrentStep()->canGoBack()) { Director::redirectBack(); return false; } - - // Switch the step to the previous! - $prevStepClass = $this->getCurrentStep()->getPreviousStep(); // Save the form data for the current step - $this->save($data); + $this->save($form->getData()); // Get the previous step of the class instance returned from $currentStep->getPreviousStep() $prevStep = DataObject::get_one($prevStepClass, "SessionID = {$this->session->ID}"); From a867db5fe8d34a63d4755d9143888673b48e08ca Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 9 Oct 2008 00:36:46 +0000 Subject: [PATCH 83/86] ENHANCEMENT Added MultiFormStep->saveInto() to simulate Form->saveInto() behaviour with a stub form. This is preferred method of saving step data into a DataObject, instead of using $myObj->update($myStep->loadData()), is update() doesn't trigger custom formfield saving behaviour (e.g. to save relations on an existing dataobject for CheckboxSetField) --- code/MultiFormStep.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/code/MultiFormStep.php b/code/MultiFormStep.php index 8e9506e..1a8652d 100644 --- a/code/MultiFormStep.php +++ b/code/MultiFormStep.php @@ -158,6 +158,29 @@ class MultiFormStep extends DataObject { $this->write(); } + /** + * Save the data on this step into an object, + * similiar to {@link Form->saveInto()} - by building + * a stub form from {@link getFields()}. This is necessary + * to trigger each {@link FormField->saveInto()} method + * individually, rather than assuming that all data + * serialized through {@link saveData()} can be saved + * as a simple value outside of the original FormField context. + * + * @param DataObject $obj + */ + public function saveInto($obj) { + $form = new Form( + new Controller(), + 'Form', + $this->getFields(), + new FieldSet() + ); + $form->loadDataFrom($this->loadData()); + $form->saveInto($obj); + return $obj; + } + /** * Custom validation for a step. In most cases, it should be sufficient * to have built-in validation through the {@link Validator} class @@ -271,6 +294,13 @@ class MultiFormStep extends DataObject { $this->form = $form; } + /** + * @return Form + */ + public function getForm() { + return $this->form; + } + // ##################### Utility #################### /** From 4da79449f863377358a26b0895b6e8bf9cd49449 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 14 Oct 2008 00:26:57 +0000 Subject: [PATCH 84/86] ENHANCEMENT Making multiform module translatable --- lang/_manifest_exclude | 0 lang/en_US.php | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 lang/_manifest_exclude create mode 100644 lang/en_US.php diff --git a/lang/_manifest_exclude b/lang/_manifest_exclude new file mode 100644 index 0000000..e69de29 diff --git a/lang/en_US.php b/lang/en_US.php new file mode 100644 index 0000000..f99f6c2 --- /dev/null +++ b/lang/en_US.php @@ -0,0 +1,9 @@ + \ No newline at end of file From 2bd8f5a7b5e82a00fd49566c34ad6583c480a5fd Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 17 Oct 2008 22:43:02 +0000 Subject: [PATCH 85/86] ENHANCEMENT Updated entities and added german translation for multiform --- lang/de_DE.php | 31 +++++++++++++++++++++++++++++++ lang/en_US.php | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 lang/de_DE.php diff --git a/lang/de_DE.php b/lang/de_DE.php new file mode 100644 index 0000000..73232b4 --- /dev/null +++ b/lang/de_DE.php @@ -0,0 +1,31 @@ + \ No newline at end of file diff --git a/lang/en_US.php b/lang/en_US.php index f99f6c2..8adde85 100644 --- a/lang/en_US.php +++ b/lang/en_US.php @@ -2,8 +2,48 @@ global $lang; -$lang['en_US']['MultiForm']['SUBMIT'] = 'Submit'; -$lang['en_US']['MultiForm']['NEXT'] = 'Next'; $lang['en_US']['MultiForm']['BACK'] = 'Back'; +$lang['en_US']['MultiForm']['NEXT'] = 'Next'; +$lang['en_US']['MultiForm']['SUBMIT'] = 'Submit'; +$lang['en_US']['MultiFormSession']['db_Hash'] = array( + 'Hash', + 50, + 'Name of the object property, e.g. used for automatically generating forms' +); +$lang['en_US']['MultiFormSession']['db_IsComplete'] = array( + 'IsComplete', + 50, + 'Name of the object property, e.g. used for automatically generating forms' +); +$lang['en_US']['MultiFormSession']['has_many_FormSteps'] = array( + 'FormSteps', + 50, + 'Name of an object relation, e.g. used for automatically generating forms' +); +$lang['en_US']['MultiFormSession']['plural_name'] = array( + '', + 50, + 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' +); +$lang['en_US']['MultiFormSession']['singular_name'] = array( + '', + 50, + 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' +); +$lang['en_US']['MultiFormStep']['db_Data'] = array( + 'Data', + 50, + 'Name of the object property, e.g. used for automatically generating forms' +); +$lang['en_US']['MultiFormStep']['plural_name'] = array( + '', + 50, + 'Pural name of the object, used in dropdowns and to generally identify a collection of this object in the interface' +); +$lang['en_US']['MultiFormStep']['singular_name'] = array( + '', + 50, + 'Singular name of the object, used in dropdowns and to generally identify a single object in the interface' +); ?> \ No newline at end of file From f72761356b375b160c1c593ca9f8ae19e8b0611d Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 5 Dec 2008 03:22:35 +0000 Subject: [PATCH 86/86] Added 0.2 release notes --- CHANGELOG | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index a10a399..3f84682 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,2 +1,18 @@ +0.2: + - ENHANCEMENT Updated entities and added german translation for multiform + - ENHANCEMENT Making multiform module translatable + - ENHANCEMENT Added MultiFormStep->saveInto() to simulate Form->saveInto() + - ENHANCEMENT Made MultiForm->prev() do the same behaviour for saving data + - ENHANCEMENT Added MultiForm->getSavedSteps() + - BUGFIX Removing url_type which isnt very useful + - BUGFIX $this->form wasn't accessible on MultiFormStep + - ENHANCEMENT Correct use of parent::construct() so that fields, actions + - BUGFIX SQL injection possibility fix on MultiForm->getSessionRecordByID() + - BUGFIX Disable security token inherited from Form, which isn't required + - BUGFIX Made MultiFormPurgeTask greatly simplified, and workable + - ENHANCEMENT Allowed static $ignored_fields to be overloaded on subclass of MultiForm + - BUGFIX Use $nextStep->Link and $prevStep->Link() for prev() and next() on MultiForm + - API CHANGE Ticket #2562 - Cleaner instanciation of MultiForm subclass without having to call ->init() + 0.1: - initial release \ No newline at end of file