mirror of
https://github.com/silverstripe/silverstripe-multiform
synced 2024-10-22 11:05:49 +02:00
This commit is contained in:
commit
5c76350fba
24
LICENSE
Normal file
24
LICENSE
Normal file
@ -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 <organization> 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.
|
23
README
Normal file
23
README
Normal file
@ -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()
|
3
_config.php
Normal file
3
_config.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
?>
|
440
code/MultiForm.php
Normal file
440
code/MultiForm.php
Normal file
@ -0,0 +1,440 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Manages the loading of single form steps, and acts as a state machine
|
||||
* that connects to a {@link MultiFormSession} object as a persistence layer.
|
||||
*
|
||||
* CAUTION: If you're using controller permission control,
|
||||
* you have to allow the following methods:
|
||||
* <code>
|
||||
* static $allowed_actions = array('next','prev');
|
||||
* </code>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
59
code/MultiFormObjectDecorator.php
Normal file
59
code/MultiFormObjectDecorator.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Decorate {@link DataObject}s which are required to be saved
|
||||
* to the database directly by a {@link MultiFormStep}.
|
||||
* Only needed for objects which aren't stored in the session,
|
||||
* which is the default.
|
||||
*
|
||||
* This decorator also augments get() requests to the datalayer
|
||||
* by automatically filtering out temporary objects.
|
||||
* You can override this filter by putting the following statement
|
||||
* in your WHERE clause:
|
||||
* `<MyDataObjectClass>`.`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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
59
code/MultiFormPurgeTask.php
Normal file
59
code/MultiFormPurgeTask.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Task to clean out all {@link MultiFormSession} objects from the database.
|
||||
*
|
||||
* Setup Instructions:
|
||||
* You need to create an automated task for your system (cronjobs on unix)
|
||||
* which triggers the run() method through cli-script.php:
|
||||
* /your/path/sapphire/cli-script.php MultiFormPurgeTask/run
|
||||
*
|
||||
* @package multiform
|
||||
*/
|
||||
class MultiFormPurgeTask extends DailyTask {
|
||||
|
||||
/**
|
||||
* Days after which unfinished sessions
|
||||
* expire and are automatically deleted
|
||||
* by a cronjob/ScheduledTask.
|
||||
*
|
||||
* @usedby {@link MultiFormPurgeTask}
|
||||
* @var int
|
||||
*/
|
||||
public static $session_expiry_days = 7;
|
||||
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
83
code/MultiFormSession.php
Normal file
83
code/MultiFormSession.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Serializes one or more {@link MultiFormStep}s into
|
||||
* a database object.
|
||||
*
|
||||
* @package multiform
|
||||
*
|
||||
*/
|
||||
class MultiFormSession extends DataObject {
|
||||
|
||||
static $db = array(
|
||||
'Data' => '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');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
245
code/MultiFormStep.php
Normal file
245
code/MultiFormStep.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* MultiFormStep controls the behaviour of a single from step in the multi-form
|
||||
* process. All form steps should be subclasses of this class, as it encapsulates
|
||||
* the functionality required for the step to be aware of itself in the form step
|
||||
* process.
|
||||
*
|
||||
* @package multiform
|
||||
*/
|
||||
class MultiFormStep extends DataObject {
|
||||
|
||||
static $db = array(
|
||||
'Data' => '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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
9
templates/Includes/MultiFormProgressList.ss
Normal file
9
templates/Includes/MultiFormProgressList.ss
Normal file
@ -0,0 +1,9 @@
|
||||
<ul class="stepIndicator current-$CurrentStep.class">
|
||||
<% control AllStepsLinear %>
|
||||
<li class="$ClassName $LinkingMode<% if FirstLast %> $FirstLast<% end_if %>">
|
||||
<% if LinkingMode = current %><% else %><% if ID %><a href="{$Top.URLSegment}/?MultiFormSessionID={$SessionID}&StepID={$ID}"><% end_if %><% end_if %>
|
||||
<% if Title %>$Title<% else %>$ClassName<% end_if %>
|
||||
<% if LinkingMode = current %><% else %><% if ID %></a><% end_if %><% end_if %>
|
||||
</li>
|
||||
<% end_control %>
|
||||
</ul>
|
1
templates/Includes/MultiFormProgressPercent.ss
Normal file
1
templates/Includes/MultiFormProgressPercent.ss
Normal file
@ -0,0 +1 @@
|
||||
<p>You've completed {$CompletedPercent.Nice}% ($CompletedStepCount/$TotalStepCount)</p>
|
Loading…
Reference in New Issue
Block a user