commit 5c76350fba98eeae272e281cddd43ca1aa615307 Author: Sean Harvey Date: Thu Apr 17 22:03:51 2008 +0000 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