Merge pull request #63 from creative-commoners/pulls/2.0/ss4-compatibility

Pulls/2.0/ss4 compatibility
This commit is contained in:
Robbie Averill 2017-09-08 17:32:09 +12:00 committed by GitHub
commit a3e7422f25
59 changed files with 2117 additions and 2605 deletions

View File

@ -1,34 +1,35 @@
# See https://github.com/silverstripe-labs/silverstripe-travis-support for setup details
sudo: false
language: php
php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7.0
dist: trusty
env:
- DB=MYSQL CORE_RELEASE=3.2
global:
- COMPOSER_ROOT_VERSION=4.0.x-dev
matrix:
include:
- php: 5.6
env: DB=MYSQL CORE_RELEASE=3
- php: 5.6
env: DB=PGSQL CORE_RELEASE=3.2
allow_failures:
env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1
- php: 7.0
env: DB=PGSQL PHPUNIT_TEST=1
- php: 7.1
env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1
before_script:
- composer self-update || true
- git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support
- php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss
- cd ~/builds/ss
- composer install
# Init PHP
- phpenv rehash
- phpenv config-rm xdebug.ini
# Install composer dependencies
- composer install --prefer-dist
- composer require --prefer-dist --no-update silverstripe/recipe-core:1.0.x-dev
- if [[ $DB == PGSQL ]]; then composer require --prefer-dist --no-update silverstripe/postgresql:2.0.x-dev; fi
- composer update
script:
- vendor/bin/phpunit multiform/tests
- if [[ $PHPUNIT_TEST ]]; then vendor/bin/phpunit; fi
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then phpdbg -qrr vendor/bin/phpunit --coverage-clover=coverage.xml; fi
- if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=framework/phpcs.xml.dist src/ tests/; fi
after_success:
- if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi

14
.upgrade.yml Normal file
View File

@ -0,0 +1,14 @@
mappings:
MultiFormObjectDecorator: SilverStripe\MultiForm\Extensions\MultiFormObjectDecorator
MultiForm: SilverStripe\MultiForm\Models\MultiForm
MultiFormSession: SilverStripe\MultiForm\Models\MultiFormSession
MultiFormStep: SilverStripe\MultiForm\Models\MultiFormStep
MultiFormPurgeTask: SilverStripe\MultiForm\Tasks\MultiFormPurgeTask
MultiFormObjectDecoratorTest: SilverStripe\MultiForm\Tests\MultiFormObjectDecoratorTest
MultiFormObjectDecoratorDataObject: SilverStripe\MultiForm\Tests\Stubs\MultiFormObjectDecoratorDataObject
MultiFormTest: SilverStripe\MultiForm\Tests\MultiFormTest
MultiFormTestController: SilverStripe\MultiForm\Tests\Stubs\MultiFormTestController
MultiFormTestForm: SilverStripe\MultiForm\Tests\Stubs\MultiFormTestForm
MultiFormTestStepOne: SilverStripe\MultiForm\Tests\Stubs\MultiFormTestStepOne
MultiFormTestStepTwo: SilverStripe\MultiForm\Tests\Stubs\MultiFormTestStepTwo
MultiFormTestStepThree: SilverStripe\MultiForm\Tests\Stubs\MultiFormTestStepThree

493
README.md
View File

@ -1,6 +1,6 @@
# MultiForm Module
[![Build Status](https://api.travis-ci.org/silverstripe/silverstripe-multiform.svg?branch=master)](https://travis-ci.org/silverstripe/silverstripe-multiform)
[![Build Status](https://travis-ci.org/silverstripe/silverstripe-multiform.svg?branch=master)](https://travis-ci.org/silverstripe/silverstripe-multiform)
[![Latest Stable Version](https://poser.pugx.org/silverstripe/multiform/version.svg)](https://github.com/silverstripe/silverstripe-multiform/releases)
[![Latest Unstable Version](https://poser.pugx.org/silverstripe/multiform/v/unstable.svg)](https://packagist.org/packages/silverstripe/multiform)
[![Total Downloads](https://poser.pugx.org/silverstripe/multiform/downloads.svg)](https://packagist.org/packages/silverstripe/multiform)
@ -83,7 +83,9 @@ Using [Composer](https://getcomposer.org/), you can install multiform into your
SilverStripe site using this command (while in the directory where your site is
currently located)
composer require "silverstripe/multiform:*"
```
composer require "silverstripe/multiform:*"
```
### 2. Create subclass of MultiForm
@ -91,13 +93,11 @@ First of all, we need to create a new subclass of *MultiForm*.
For the above example, our multi-form will be called *SurveyForm*
:::php
<?php
class SurveyForm extends MultiForm {
}
```php
class SurveyForm extends MultiForm {
}
```
### 3. Set up first step
@ -111,27 +111,23 @@ form.
So, for example, if we were going to have a first step which collects the
personal details of the form user, then we might have this class:
:::php
<?php
class SurveyFormPersonalDetailsStep extends MultiFormStep {
}
```php
class SurveyFormPersonalDetailsStep extends MultiFormStep {
}
```
Now that we've got our first step of the form defined, we need to go back to our
subclass of MultiForm, SurveyForm, and tell it that SurveyFormPersonalDetailsStep
is the first step.
:::php
<?php
class SurveyForm extends MultiForm {
public static $start_step = 'SurveyFormPersonalDetailsStep';
}
```php
class SurveyForm extends MultiForm {
private static $start_step = 'SurveyFormPersonalDetailsStep';
}
```
### 4. Define next step, and final step
@ -144,43 +140,39 @@ order to use flow control in our system.
To let the step know what step is next in the process, we do the same as setting
the `$start_step` variable *SurveyForm*, but we call it `$next_steps`.
:::php
<?php
class SurveyFormPersonalDetailsStep extends MultiFormStep {
public static $next_steps = 'SurveyFormOrganisationDetailsStep';
```php
class SurveyFormPersonalDetailsStep extends MultiFormStep {
public function getFields() {
return new FieldList(
new TextField('FirstName', 'First name'),
new TextField('Surname', 'Surname')
);
}
}
private static $next_steps = 'SurveyFormOrganisationDetailsStep';
public function getFields() {
return new FieldList(
new TextField('FirstName', 'First name'),
new TextField('Surname', 'Surname')
);
}
}
```
At the very least, each step also has to have a `getFields()` method returning
a *FieldSet* with some form field objects. These are the fields that the form
will render for the given step.
Keep in mind that our multi-form also requires an end point. This step is the
final, and needs to have another variable set to let the multi-form system know
final one, and needs to have another variable set to let the multi-form system know
this is the final step.
So, if we assume that the last step in our process is
SurveyFormOrganisationDetailsStep, then we can do something like this:
:::php
<?php
class SurveyFormOrganisationDetailsStep extends MultiFormStep {
public static $is_final_step = true;
}
```php
class SurveyFormOrganisationDetailsStep extends MultiFormStep {
private static $is_final_step = true;
}
```
### 5. Run database integrity check
@ -197,42 +189,40 @@ that the form can be rendered into a given template.
So, if we want to render our multi-form as `$SurveyForm` in the *Page.ss*
template, we need to create a SurveyForm method (function) on the controller:
:::php
<?php
class Page extends SiteTree {
// ...
}
class Page_Controller extends ContentController {
// ...
```php
class Page extends SiteTree {
//
private static $allowed_actions = array(
'SurveyForm',
'finished'
);
// ...
public function SurveyForm() {
return new SurveyForm($this, 'Form');
}
public function finished() {
return array(
'Title' => 'Thank you for your submission',
'Content' => '<p>You have successfully submitted the form!</p>'
);
}
// ...
}
}
class Page_Controller extends ContentController {
The `SurveyForm()` function will create a new instance our subclass of
// ...
//
private static $allowed_actions = array(
'SurveyForm',
'finished'
);
public function SurveyForm() {
return new SurveyForm($this, 'Form');
}
public function finished() {
return array(
'Title' => 'Thank you for your submission',
'Content' => '<p>You have successfully submitted the form!</p>'
);
}
// ...
}
```
The `SurveyForm()` function will create a new instance of our subclass of
MultiForm, which in this example, is *SurveyForm*. This in turn will then set
up all the form fields, actions, and validation available to each step, as well
as the session.
@ -242,21 +232,21 @@ like.
Your template should look something like this, to render the form in:
:::html
<div id="content">
<% if $Content %>
$Content
<% end_if %>
<% if $SurveyForm %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div>
```html
<div id="content">
<% if $Content %>
$Content
<% end_if %>
<% if $SurveyForm %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div>
```
In this case, the above template example is a *sub-template* inside the *Layout*
directory for the templates. Note that we have also included `$Form`, so
@ -281,11 +271,11 @@ To include these with our instance of multiform, we just need to add an
For example:
:::html
<% with $SurveyForm %>
<% include MultiFormProgressList %>
<% end_with %>
```html
<% with $SurveyForm %>
<% include MultiFormProgressList %>
<% end_with %>
```
This means the included template is rendered within the scope of the
SurveyForm instance returned, instead of the top level controller context.
@ -293,24 +283,24 @@ This gives us the data to show the progression of the steps.
Putting it together, we might have something looking like this:
:::html
<div id="content">
<% if $Content %>
$Content
<% end_if %>
<% if $SurveyForm %>
<% with $SurveyForm %>
<% include MultiFormProgressList %>
<% end_with %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div>
```html
<div id="content">
<% if $Content %>
$Content
<% end_if %>
<% if $SurveyForm %>
<% with $SurveyForm %>
<% include MultiFormProgressList %>
<% end_with %>
$SurveyForm
<% end_if %>
<% if $Form %>
$Form
<% end_if %>
</div>
```
Feel free to play around with the progress indicators. If you need something
specific to your project, just create a new "Include" template inside your own
@ -343,37 +333,35 @@ based on the submission value of another step. There are two methods supporting
Here is an example of how to populate the email address from step 1 in step2 :
:::php
<?php
```php
class Step1 extends MultiFormStep
{
private static $next_steps = 'Step2';
class Step1 extends MultiFormStep
{
public static $next_steps = 'Step2';
public function getFields() {
return new FieldList(
new EmailField('Email', 'Your email')
);
}
}
public function getFields() {
return new FieldList(
new EmailField('Email', 'Your email')
);
}
}
class Step2 extends MultiFormStep
{
private static $next_steps = 'Step3';
class Step2 extends MultiFormStep
{
public static $next_steps = 'Step3';
public function getFields() {
$fields = new FieldList(
new EmailField('Email', 'E-mail'),
new EmailField('Email2', 'Verify E-Mail')
);
public function getFields() {
$fields = new FieldList(
new EmailField('Email', 'E-mail'),
new EmailField('Email2', 'Verify E-Mail')
);
// set the email field to the input from Step 1
$this->copyValueFromOtherStep($fields, 'Step1', 'Email');
return $fields;
}
}
// set the email field to the input from Step 1
$this->copyValueFromOtherStep($fields, 'Step1', 'Email');
return $fields;
}
}
```
### 8. Finishing it up
@ -389,56 +377,54 @@ So, we must write some code on our subclass of *MultiForm*, overloading
Here is an example of what we could do here:
:::php
<?php
class SurveyForm extends MultiForm {
public static $start_step = 'SurveyFormPersonalDetailsStep';
public function finish($data, $form) {
parent::finish($data, $form);
```php
class SurveyForm extends MultiForm {
private static $start_step = 'SurveyFormPersonalDetailsStep';
public function finish($data, $form) {
parent::finish($data, $form);
$steps = DataObject::get(
'MultiFormStep',
"SessionID = {$this->session->ID}"
);
if($steps) {
foreach($steps as $step) {
if($step->class == 'SurveyFormPersonalDetailsStep') {
$member = new Member();
$data = $step->loadData();
$steps = DataObject::get(
MultiFormStep::class,
"SessionID = {$this->session->ID}"
);
if($steps) {
foreach($steps as $step) {
if($step->class == 'SurveyFormPersonalDetailsStep') {
$member = new Member();
$data = $step->loadData();
if($data) {
$member->update($data);
$member->write();
}
}
if($step->class == 'SurveyOrganisationDetailsStep') {
$organisation = new Organisation();
$data = $step->loadData();
if($data) {
$member->update($data);
$member->write();
}
}
if($data) {
$organisation->update($data);
if($step->class == 'SurveyOrganisationDetailsStep') {
$organisation = new Organisation();
$data = $step->loadData();
if($member && $member->ID) {
$organisation->MemberID = $member->ID;
}
if($data) {
$organisation->update($data);
$organisation->write();
}
}
// Shows the step data (unserialized by loadData)
// Debug::show($step->loadData());
}
}
if($member && $member->ID) {
$organisation->MemberID = $member->ID;
}
$this->controller->redirect($this->controller->Link() . 'finished');
}
}
$organisation->write();
}
}
// Shows the step data (unserialized by loadData)
// Debug::show($step->loadData());
}
}
$this->controller->redirect($this->controller->Link() . 'finished');
}
}
```
#### 9. Organisation data model
@ -449,17 +435,14 @@ groups in SilverStripe) so we need to create it:
This example has been chosen as a separate DataObject but you may wish to change
the code and add the data to the Member class instead.
:::php
<?php
class Organisation extends DataObject {
private static $db = array(
// Add your Organisation fields here
);
}
```php
class Organisation extends DataObject {
private static $db = array(
// Add your Organisation fields here
);
}
```
#### Warning
If you're dealing with sensitive data, it's best to delete the session and step
@ -468,9 +451,9 @@ data immediately after the form is successfully submitted.
You can delete it by calling this method on the finish() for your MultiForm
subclass:
:::php
$this->session->delete();
```php
$this->session->delete();
```
This will also go through each of it's steps and delete them as well.
@ -499,42 +482,42 @@ MultiForm class:
More than likely, you'll want the first one to be available when the form
renders. To that effect, you can start placing templates in the
*templates/Includes* directory for your project. You need to call them the same
*templates/Includes* directory for your project. You need to name them the same
as the class name for each step. For example, if you want *MembershipForm*, a
subclass of *MultiFormStep* to have it's own template, you would put
*MembershipForm.ss* into that directory, and run *?flush=1*.
If you'd like a pre-existing template on how to customise the form step, have a
look at Form.ss that's found within the sapphire module. Use that template, as
look at Form.ss that's found within the framework module. Use that template, as
a base for your new MembershipForm.ss template in your project templates.
For more information on this, please [look at the Form documentation](http://doc.silverstripe.org/framework/en/topics/forms#custom-form-templates).
### getNextStep()
If you are wanting to override the next step (so, if you want the next step to
be something different based on a user's choice of input during the step, you
If you are wanting to override the next step (for example if you want the next step to
be something different based on a user's choice of input during the step) you
can override getNextStep() on any given step to manually override what the next
step should be. An example:
:::php
class MyStep extends MultiFormStep
// ...
public function getNextStep() {
$data = $this->loadData();
if(@$data['Gender'] == 'Male') {
return 'TestThirdCase1Step';
} else {
return 'TestThirdCase2Step';
}
}
// ...
}
```php
class MyStep extends MultiFormStep
// ...
public function getNextStep() {
$data = $this->loadData();
if(@$data['Gender'] == 'Male') {
return 'TestThirdCase1Step';
} else {
return 'TestThirdCase2Step';
}
}
// ...
}
```
### Validation
To define validation on a step-by-step basis, please define getValidator() and
@ -543,22 +526,22 @@ validation see [:form](http://doc.silverstripe.org/form-validation).
e.g.
:::php
class MyStep extends MultiFormStep {
...
public function getValidator() {
return new RequiredFields(array(
'Name',
'Email'
));
}
...
}
```php
class MyStep extends MultiFormStep {
...
public function getValidator() {
return new RequiredFields(array(
'Name',
'Email'
));
}
...
}
```
### finish()
@ -573,29 +556,27 @@ won't be saved.
For example:
:::php
<?php
class SurveyForm extends MultiForm {
public static $start_step = 'SurveyFormPersonalDetailsStep';
public function finish($data, $form) {
parent::finish($data, $form);
```php
class SurveyForm extends MultiForm {
$steps = MultiFormStep::get()->filter(array(
"SessionID" => $this->session->ID
));
private static $start_step = 'SurveyFormPersonalDetailsStep';
if($steps) {
foreach($steps as $step) {
// Shows the step data (unserialized by loadData)
Debug::show($step->loadData());
}
}
}
}
public function finish($data, $form) {
parent::finish($data, $form);
$steps = MultiFormStep::get()->filter(array(
"SessionID" => $this->session->ID
));
if($steps) {
foreach($steps as $step) {
// Shows the step data (unserialized by loadData)
Debug::show($step->loadData());
}
}
}
}
```
The above is a sample bit of code that simply fetches all the steps in the
database that were saved. Further refinement could include getting steps only
@ -613,9 +594,9 @@ idea to immediately delete this data after the user has submitted.
This can be easily achieved by adding the following line at the end of your
`finish()` method on your MultiForm subclass.
:::php
$this->session->delete();
```php
$this->session->delete();
```
### Expiring old session data

View File

@ -1,2 +1 @@
<?php

3
_config/config.yml Normal file
View File

@ -0,0 +1,3 @@
---
Name: multisiteconfig
---

7
_config/legacy.yml Normal file
View File

@ -0,0 +1,7 @@
---
Name: multisitelegacy
---
SilverStripe\ORM\DatabaseAdmin:
classname_value_remapping:
MultiFormSession: SilverStripe\MultiForm\Models\MultiFormSession
MultiFormStep: SilverStripe\MultiForm\Models\MultiFormStep

View File

@ -1,69 +0,0 @@
<?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 DataExtension {
private static $db = array(
'MultiFormIsTemporary' => 'Boolean',
);
private static $has_one = array(
'MultiFormSession' => 'MultiFormSession',
);
/**
* Augment any queries to MultiFormObjectDecorator and only
* return anything that isn't considered temporary.
*/
public function augmentSQL(SQLQuery &$query) {
$where = $query->getWhere();
if(!$where && !$this->wantsTemporary($query)) {
$from = array_values($query->getFrom());
$query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'");
return;
}
if(
strpos($where[0], ".`ID` = ") === false
&& strpos($where[0], ".ID = ") === false
&& strpos($where[0], "ID = ") !== 0
&& !$this->wantsTemporary($query)
) {
$from = array_values($query->getFrom());
$query->addWhere("{$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->getWhere() as $whereClause) {
$from = array_values($query->getFrom());
// SQLQuery will automatically add double quotes and single quotes to values, so check against that.
if($whereClause == "{$from[0]}.\"MultiFormIsTemporary\" = '1'") {
return true;
}
}
return false;
}
}

View File

@ -1,699 +0,0 @@
<?php
/**
* MultiForm 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>
* private static $allowed_actions = array('next','prev');
* </code>
*
* @package multiform
*/
abstract class MultiForm extends Form {
/**
* A session object stored in the database, to identify and store
* data for this MultiForm instance.
*
* @var MultiFormSession
*/
protected $session;
/**
* The current encrypted MultiFormSession identification.
*
* @var string
*/
protected $currentSessionHash;
/**
* Defines which subclass of {@link MultiFormStep} should be the first
* step in the multi-step process.
*
* @var string Classname of a {@link MultiFormStep} subclass
*/
public static $start_step;
/**
* Set the casting for these fields.
*
* @var array
*/
private static $casting = array(
'CompletedStepCount' => 'Int',
'TotalStepCount' => 'Int',
'CompletedPercent' => 'Float'
);
/**
* @var string
*/
private static $get_var = 'MultiFormSessionID';
/**
* 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
*/
public static $ignored_fields = array(
'url',
'executeForm',
'SecurityID'
);
/**
* Any of the actions defined in this variable are exempt from
* being validated.
*
* This is most useful for the "Back" (action_prev) action, as
* you typically don't validate the form when the user is going
* back a step.
*
* @var array
*/
public static $actions_exempt_from_validation = array(
'action_prev'
);
/**
* @var string
*/
protected $displayLink;
/**
* Flag which is being used in getAllStepsRecursive() to allow adding the completed flag on the steps
*
* @var boolean
*/
protected $currentStepHasBeenFound = false;
/**
* Start the MultiForm instance.
*
* @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) {
// First set the controller and name manually so they are available for
// field construction.
$this->controller = $controller;
$this->name = $name;
// 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 the step returned above as the current step
$this->setCurrentStep($currentStep);
// Set the form of the step to this form instance
$currentStep->setForm($this);
// 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)
$validator = null;
$applyValidation = true;
$actionNames = static::$actions_exempt_from_validation;
if($actionNames) {
foreach ($actionNames as $exemptAction) {
if(!empty($_REQUEST[$exemptAction])) {
$applyValidation = false;
break;
}
}
}
// Apply validation if the current step requires validation (is not exempt)
if($applyValidation) {
if($currentStep->getValidator()) {
$validator = $currentStep->getValidator();
}
}
// Give the fields, actions, and validation for the current step back to the parent Form class
parent::__construct($controller, $name, $fields, $actions, $validator);
$getVar = $this->config()->get_var;
// Set a hidden field in our form with an encrypted hash to identify this session.
$this->fields->push(new HiddenField($getVar, false, $this->session->Hash));
// If there is saved data for the current step, we load it into the form it here
//(CAUTION: loadData() MUST unserialize first!)
if($data = $currentStep->loadData()) {
$this->loadDataFrom($data);
}
// Disable security token - we tie a form to a session ID instead
$this->disableSecurityToken();
self::$ignored_fields[] = $getVar;
}
/**
* Accessor method to $this->controller.
*
* @return Controller this MultiForm was instanciated on.
*/
public function getController() {
return $this->controller;
}
/**
* Returns the get_var to the template engine
*
* @return string
*/
public function getGetVar() {
return $this->config()->get_var;
}
/**
* 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() {
$startStepClass = static::$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 $start_step on ' . $this->class,
E_USER_ERROR
);
// Determine whether we use the current step, or create one if it doesn't exist
$currentStep = null;
$StepID = $this->controller->request->getVar('StepID');
if(isset($StepID)) {
$currentStep = DataObject::get_one(
'MultiFormStep',
array(
'SessionID' => $this->session->ID,
'ID' => $StepID
)
);
} elseif($this->session->CurrentStepID) {
$currentStep = $this->session->CurrentStep();
}
// Always fall back to creating a new step (in case the session or request data is invalid)
if(!$currentStep || !$currentStep->ID) {
$currentStep = Object::create($startStepClass);
$currentStep->SessionID = $this->session->ID;
$currentStep->write();
$this->session->CurrentStepID = $currentStep->ID;
$this->session->write();
$this->session->flushCache();
}
if($currentStep) $currentStep->setForm($this);
return $currentStep;
}
/**
* 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;
$step->setForm($this);
return $this->session->write();
}
/**
* Accessor method to $this->session.
*
* @return MultiFormSession
*/
public function getSession() {
return $this->session;
}
/**
* Set up the session.
*
* 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.
*
* @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() {
$this->session = $this->getCurrentSession();
// If there was no session found, create a new one instead
if(!$this->session) {
$this->session = new MultiFormSession();
}
// 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();
}
}
/**
* Set the currently used encrypted hash to identify
* the MultiFormSession.
*
* @param string $hash Encrypted identification to session
*/
public function setCurrentSessionHash($hash) {
$this->currentSessionHash = $hash;
$this->setSession();
}
/**
* Return the currently used {@link MultiFormSession}
* @return MultiFormSession|boolean FALSE
*/
public function getCurrentSession() {
if(!$this->currentSessionHash) {
$this->currentSessionHash = $this->controller->request->getVar($this->config()->get_var);
if(!$this->currentSessionHash) {
return false;
}
}
$this->session = MultiFormSession::get()->filter(array(
"Hash" => $this->currentSessionHash,
"IsComplete" => 0
))->first();
return $this->session;
}
/**
* 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()}.
*
* @param string $filter SQL WHERE statement
* @return DataObjectSet|boolean A set of MultiFormStep subclasses
*/
public function getSavedSteps($filter = null) {
$filter .= ($filter) ? ' AND ' : '';
$filter .= sprintf("\"SessionID\" = '%s'", $this->session->ID);
return DataObject::get('MultiFormStep', $filter);
}
/**
* 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
*/
public function getSavedStepByClass($className) {
return DataObject::get_one(
'MultiFormStep',
sprintf("\"SessionID\" = '%s' AND \"ClassName\" = '%s'",
$this->session->ID,
Convert::raw2sql($className)
)
);
}
/**
* Build a FieldList 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,
* 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.
*
* @param $currentStep Subclass of MultiFormStep
* @return FieldList of FormAction objects
*/
public function actionsFor($step) {
// Create default multi step actions (next, prev), and merge with extra actions, if any
$actions = (class_exists('FieldList')) ? new FieldList() : 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($step->isFinalStep()) {
$actions->push(new FormAction('finish', $step->getSubmitText()));
} else {
$actions->push(new FormAction('next', $step->getNextText()));
}
// 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
if($step->getNextStep()) {
$actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_next');
// Assume that this is the last step, insert the action before the finish action
} else {
$actions->insertBefore($prev = new FormAction('prev', $step->getPrevText()), 'action_finish');
}
//remove browser validation from prev action
$prev->setAttribute("formnovalidate", "formnovalidate");
}
// Merge any extra action fields defined on the step
$actions->merge($step->getExtraActions());
return $actions;
}
/**
* 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.
*
* @return SSViewer object to render the template with
*/
public function forTemplate() {
$return = $this->renderWith(array(
$this->getCurrentStep()->class,
'MultiFormStep',
$this->class,
'MultiForm',
'Form'
));
$this->clearMessage();
return $return;
}
/**
* 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) {
// Save the form data for the current step
$this->save($data);
if(!$this->getCurrentStep()->isFinalStep()) {
$this->controller->redirectBack();
return false;
}
if(!$this->getCurrentStep()->validateStep($data, $form)) {
Session::set("FormInfo.{$form->FormName()}.data", $form->getData());
$this->controller->redirectBack();
return false;
}
}
/**
* 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 (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
*/
public function next($data, $form) {
// Save the form data for the current step
$this->save($form->getData());
// Get the next step class
$nextStepClass = $this->getCurrentStep()->getNextStep();
if(!$nextStepClass) {
$this->controller->redirectBack();
return false;
}
// Perform custom step validation (use MultiFormStep->getValidator() for
// built-in functionality). The data needs to be manually saved on error
// so the form is re-populated.
if(!$this->getCurrentStep()->validateStep($data, $form)) {
Session::set("FormInfo.{$form->FormName()}.data", $form->getData());
$this->controller->redirectBack();
return false;
}
// validation succeeded so we reset it to remove errors and messages
$this->resetValidation();
// 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 = Object::create($nextStepClass);
$nextStep->SessionID = $this->session->ID;
$nextStep->write();
}
// Set the next step found as the current step
$this->setCurrentStep($nextStep);
// Redirect to the next step
$this->controller->redirect($nextStep->Link());
}
/**
* Determine what to do when the previous action is called.
*
* 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
*/
public function prev($data, $form) {
// Save the form data for the current step
$this->save($form->getData());
// Get the previous step class
$prevStepClass = $this->getCurrentStep()->getPreviousStep();
if(!$prevStepClass && !$this->getCurrentStep()->canGoBack()) {
$this->controller->redirectBack();
return false;
}
// 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->setCurrentStep($prevStep);
// Redirect to the previous step
$this->controller->redirect($prevStep->Link());
}
/**
* Save the raw data given back from the form into session.
*
* 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
*/
protected function save($data) {
$currentStep = $this->getCurrentStep();
if(is_array($data)) {
foreach($data as $field => $value) {
if(in_array($field, static::$ignored_fields)) {
unset($data[$field]);
}
}
$currentStep->saveData($data);
}
return;
}
// ############ Misc ############
/**
* 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.
*
* @return string
*/
public function FormAction() {
$action = parent::FormAction();
$action .= (strpos($action, '?')) ? '&amp;' : '?';
$action .= "{$this->config()->get_var}={$this->session->Hash}";
return $action;
}
/**
* Returns the link to the page where the form is displayed. The user is
* redirected to this link with a session param after each step is
* submitted.
*
* @return string
*/
public function getDisplayLink() {
return $this->displayLink ? $this->displayLink : Controller::curr()->Link();
}
/**
* Set the link to the page on which the form is displayed.
*
* The link defaults to the controllers current link. However if the form
* is displayed inside an action the display link must be explicitly set.
*
* @param string $link
*/
public function setDisplayLink($link) {
$this->displayLink = $link;
}
/**
* Determine the steps to show in a linear fashion, starting from the
* first step. We run {@link getAllStepsRecursive} passing the steps found
* by reference to get a listing of the steps.
*
* @return DataObjectSet of MultiFormStep instances
*/
public function getAllStepsLinear() {
$stepsFound = (class_exists('ArrayList')) ? new ArrayList() : new DataObjectSet();
$firstStep = DataObject::get_one(static::$start_step, "\"SessionID\" = {$this->session->ID}");
$firstStep->LinkingMode = ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
$firstStep->setForm($this);
$stepsFound->push($firstStep);
// mark the further steps as non-completed if the first step is the current
if ($firstStep->ID == $this->getCurrentStep()->ID) {
$this->currentStepHasBeenFound = true;
} else {
$firstStep->addExtraClass('completed');
}
$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.
* 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
* @return DataObjectSet of MultiFormStep instances
*/
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
$nextStep = $step->getNextStepFromDatabase();
if(!$nextStep) {
// 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());
}
// once the current steps has been found we won't add the completed class anymore.
if ($nextStep->ID == $this->getCurrentStep()->ID) $this->currentStepHasBeenFound = true;
$nextStep->LinkingMode = ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
// add the completed class
if (!$this->currentStepHasBeenFound) $nextStep->addExtraClass('completed');
$nextStep->setForm($this);
// Add the array data, and do a callback
$stepsFound->push($nextStep);
$this->getAllStepsRecursive($nextStep, $stepsFound);
}
// Once we've reached the final step, we just return what we've collected
} else {
return $stepsFound;
}
}
/**
* 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.
*
* @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() {
$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->getCompletedStepCount() * 100 / $this->getTotalStepCount();
}
}

View File

@ -1,68 +0,0 @@
<?php
/**
* 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 {
private static $db = array(
'Hash' => 'Varchar(40)', // cryptographic hash identification to this session
'IsComplete' => 'Boolean' // flag to determine if this session is marked completed
);
private static $has_one = array(
'Submitter' => 'Member',
'CurrentStep' => 'MultiFormStep'
);
private static $has_many = array(
'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.
*/
public function onBeforeWrite() {
// save submitter if a Member is logged in
$currentMember = Member::currentUser();
if(!$this->SubmitterID && $currentMember) $this->SubmitterID = $currentMember->ID;
parent::onBeforeWrite();
}
/**
* These actions are performed when delete() is called on this object.
*/
public function onBeforeDelete() {
// delete dependent form steps and relation
$steps = $this->FormSteps();
if($steps) foreach($steps as $step) {
if($step && $step->exists()) {
$steps->remove($step);
$step->delete();
$step->destroy();
}
}
parent::onBeforeDelete();
}
}

View File

@ -1,465 +0,0 @@
<?php
/**
* MultiFormStep controls the behaviour of a single form step in the MultiForm
* process. All form steps are required to be subclasses of this class, as it
* encapsulates the functionality required for the step to be aware of itself
* in the process by knowing what it's next step is, and if applicable, it's previous
* step.
*
* @package multiform
*/
class MultiFormStep extends DataObject {
private static $db = array(
'Data' => 'Text' // stores serialized maps with all session information
);
private static $has_one = array(
'Session' => 'MultiFormSession'
);
/**
* Centerpiece of the flow control for the form.
*
* If set to a string, you 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
*/
public 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
*/
public static $is_final_step = false;
/**
* 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
*/
protected static $can_go_back = true;
/**
* Title of this step.
*
* Used for the step indicator templates.
*
* @var string
*/
protected $title;
/**
* Form class that this step is directly related to.
*
* @var MultiForm subclass
*/
protected $form;
/**
* List of additional CSS classes for this step
*
* @var array $extraClasses
*/
protected $extraClasses = array();
/**
* Temporary cache to increase the performance for repeated look ups.
*
* @var array $cache
*/
protected $step_data_cache = array();
/**
* Form fields to be rendered with this step.
* (Form object is created in {@link MultiForm}.
*
* This function needs to be implemented on your
* subclasses of MultiFormStep.
*
* @return FieldList
*/
public function getFields() {
user_error('Please implement getFields on your MultiFormStep subclass', E_USER_ERROR);
}
/**
* Additional form actions to be added to this step.
* (Form object is created in {@link MultiForm}.
*
* Note: This is optional, and is to be implemented
* on your subclasses of MultiFormStep.
*
* @return FieldList
*/
public function getExtraActions() {
return (class_exists('FieldList')) ? new FieldList() : new FieldSet();
}
/**
* Get a validator specific to this form.
* The form is automatically validated in {@link Form->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 ? $this->title : $this->class;
}
/**
* 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() {
$form = $this->form;
return Controller::join_links($form->getDisplayLink(), "?{$form->config()->get_var}={$this->Session()->Hash}");
}
/**
* Unserialize stored session data and return it.
* 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, leaving
* others that are not required.
*
* @return array
*/
public function loadData() {
return ($this->Data && is_string($this->Data)) ? unserialize($this->Data) : array();
}
/**
* 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 {@link MultiForm}
*/
public function saveData($data) {
$this->Data = serialize($data);
$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(
Controller::curr(),
'Form',
$this->getFields(),
((class_exists('FieldList')) ? new FieldList() : 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
* 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()}.
*
* @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
*
* @return string Classname of a {@link MultiFormStep} subclass
*/
public function getNextStep() {
$nextSteps = static::$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, and then gone back a step.
*
* @return MultiFormStep|boolean
*/
public function getNextStepFromDatabase() {
if($this->SessionID && is_numeric($this->SessionID)) {
$nextSteps = static::$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;
}
}
}
/**
* Accessor method for self::$next_steps
*
* @return string|array
*/
public function getNextSteps() {
return static::$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) {
$step->setForm($this->form);
if($step->getNextStep()) {
if($step->getNextStep() == $this->class) {
return $step->class;
}
}
}
}
}
/**
* 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() {
if($prevStepClass = $this->getPreviousStep()) {
return DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->SessionID}");
}
}
/**
* Get the text to the use on the button to the previous step.
* @return string
*/
public function getPrevText() {
return _t('MultiForm.BACK', 'Back');
}
/**
* Get the text to use on the button to the next step.
* @return string
*/
public function getNextText() {
return _t('MultiForm.NEXT', 'Next');
}
/**
* Get the text to use on the button to submit the form.
* @return string
*/
public function getSubmitText() {
return _t('MultiForm.SUBMIT', 'Submit');
}
/**
* Sets the form that this step is directly related to.
*
* @param MultiForm subclass $form
*/
public function setForm($form) {
$this->form = $form;
}
/**
* @return Form
*/
public function getForm() {
return $this->form;
}
// ##################### Utility ####################
/**
* Determines whether the user is able to go back using the "action_back"
* Determines whether the user is able to go back using the "action_back"
* 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 static::$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.
*
* @return boolean
*/
public function isFinalStep() {
return static::$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;
}
/**
* Add a CSS-class to the step. If needed, multiple classes can be added by delimiting a string with spaces.
*
* @param string $class A string containing a classname or several class names delimited by a space.
* @return MultiFormStep
*/
public function addExtraClass($class) {
// split at white space
$classes = preg_split('/\s+/', $class);
foreach($classes as $class) {
// add classes one by one
$this->extraClasses[$class] = $class;
}
return $this;
}
/**
* Remove a CSS-class from the step. Multiple classes names can be passed through as a space delimited string.
*
* @param string $class
* @return MultiFormStep
*/
public function removeExtraClass($class) {
// split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
// unset one by one
unset($this->extraClasses[$class]);
}
return $this;
}
/**
* @return string
*/
public function getExtraClasses() {
return join(' ', array_keys($this->extraClasses));
}
/**
* Returns the submitted value, if any, of any steps.
*
* @param string $fromStep (classname)
* @param string $key
*
* @return mixed
*/
public function getValueFromOtherStep($fromStep, $key) {
// load the steps in the cache, if this one doesn't exist
if (!array_key_exists('steps_' . $fromStep, $this->step_data_cache)) {
$steps = MultiFormStep::get()->filter('SessionID', $this->form->session->ID);
if($steps) {
foreach($steps as $step) {
$this->step_data_cache['steps_' . $step->ClassName] = $step->loadData();
}
}
}
// check both as PHP isn't recursive
if(isset($this->step_data_cache['steps_' . $fromStep])) {
if(isset($this->step_data_cache['steps_' . $fromStep][$key])) {
return $this->step_data_cache['steps_' . $fromStep][$key];
}
}
return null;
}
/**
* allows to get a value from another step copied over
*
* @param FieldList $fields
* @param string $formStep
* @param string $fieldName
* @param string $fieldNameTarget (optional)
*/
public function copyValueFromOtherStep(FieldList $fields, $formStep, $fieldName, $fieldNameTarget = null) {
// if a target field isn't defined use the same fieldname
if (!$fieldNameTarget) $fieldNameTarget = $fieldName;
$fields->fieldByName($fieldNameTarget)->setValue($this->getValueFromOtherStep($formStep, $fieldName));
}
}

View File

@ -1,55 +0,0 @@
<?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 process() method through cli-script.php:
* `php framework/cli-script.php MultiFormPurgeTask`
* or
* `framework/sake MultiFormPurgeTask`
*
* @package multiform
*/
class MultiFormPurgeTask extends BuildTask {
/**
* Days after which sessions expire and
* are automatically deleted.
*
* @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($request) {
$sessions = $this->getExpiredSessions();
$delCount = 0;
if($sessions) foreach($sessions as $session) {
$session->delete();
$delCount++;
}
echo $delCount . ' session records deleted that were older than ' . self::$session_expiry_days . ' days.';
}
/**
* Return all MultiFormSession database records that are older than
* the days specified in $session_expiry_days
*
* @return DataObjectSet
*/
protected function getExpiredSessions() {
return DataObject::get(
'MultiFormSession',
"DATEDIFF(NOW(), \"MultiFormSession\".\"Created\") > " . self::$session_expiry_days
);
}
}

1
codecov.yml Normal file
View File

@ -0,0 +1 @@
comment: false

View File

@ -1,22 +1,41 @@
{
"name": "silverstripe/multiform",
"description": "SilverStripe forms with multiple steps, flow control and state persistence",
"type": "silverstripe-module",
"keywords": ["silverstripe", "forms"],
"authors": [{
"name": "Ingo Schommer",
"email": "ingo@silverstripe.com"
}, {
"name": "Sean Harvey",
"email": "sean@silverstripe.com"
}],
"require": {
"silverstripe/framework": "~3.2"
},
"license": "BSD-3-Clause",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
}
"name": "silverstripe/multiform",
"description": "SilverStripe forms with multiple steps, flow control and state persistence",
"type": "silverstripe-module",
"keywords": [
"silverstripe",
"forms"
],
"authors": [
{
"name": "Ingo Schommer",
"email": "ingo@silverstripe.com"
},
{
"name": "Sean Harvey",
"email": "sean@silverstripe.com"
}
],
"require": {
"silverstripe/framework": "^4@dev"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"squizlabs/php_codesniffer": "^3.0",
"silverstripe/versioned": "^1@dev"
},
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
}
},
"autoload": {
"psr-4": {
"SilverStripe\\MultiForm\\": "src/",
"SilverStripe\\MultiForm\\Tests\\": "tests/"
}
},
"license": "BSD-3-Clause",
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@ -1,31 +0,0 @@
<?php
/**
* Arabic (Saudi Arabia) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('ar_SA', $lang) && is_array($lang['ar_SA'])) {
$lang['ar_SA'] = array_merge($lang['en_US'], $lang['ar_SA']);
} else {
$lang['ar_SA'] = $lang['en_US'];
}
$lang['ar_SA']['MultiForm']['BACK'] = 'السابق';
$lang['ar_SA']['MultiForm']['NEXT'] = 'التالي';
$lang['ar_SA']['MultiForm']['SUBMIT'] = 'إرسال';
$lang['ar_SA']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['ar_SA']['MultiFormSession']['db_IsComplete'] = 'مكتمل؟';
$lang['ar_SA']['MultiFormSession']['has_many_FormSteps'] = 'خطوات النموذج';
$lang['ar_SA']['MultiFormSession']['plural_name'] = '(لايوجد)';
$lang['ar_SA']['MultiFormSession']['singular_name'] = '(لايوجد)';
$lang['ar_SA']['MultiFormStep']['db_Data'] = 'بيانات';
$lang['ar_SA']['MultiFormStep']['plural_name'] = '(لايوجد)';
$lang['ar_SA']['MultiFormStep']['singular_name'] = '(لايوجد)';
?>

View File

@ -1,27 +0,0 @@
<?php
/**
* Bulgarian (Bulgaria) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('bg_BG', $lang) && is_array($lang['bg_BG'])) {
$lang['bg_BG'] = array_merge($lang['en_US'], $lang['bg_BG']);
} else {
$lang['bg_BG'] = $lang['en_US'];
}
$lang['bg_BG']['MultiForm']['BACK'] = 'Назад';
$lang['bg_BG']['MultiForm']['NEXT'] = 'Следващо';
$lang['bg_BG']['MultiForm']['SUBMIT'] = 'Прати';
$lang['bg_BG']['MultiFormSession']['plural_name'] = '(никакви)';
$lang['bg_BG']['MultiFormSession']['singular_name'] = '(никакво)';
$lang['bg_BG']['MultiFormStep']['plural_name'] = '(никакви)';
$lang['bg_BG']['MultiFormStep']['singular_name'] = '(никакво)';
?>

View File

@ -1,28 +0,0 @@
<?php
/**
* Bosnian (Bosnia and Herzegovina) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('bs_BA', $lang) && is_array($lang['bs_BA'])) {
$lang['bs_BA'] = array_merge($lang['en_US'], $lang['bs_BA']);
} else {
$lang['bs_BA'] = $lang['en_US'];
}
$lang['bs_BA']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['bs_BA']['MultiFormSession']['db_IsComplete'] = 'JeZavršen';
$lang['bs_BA']['MultiFormSession']['has_many_FormSteps'] = 'KoraciForme';
$lang['bs_BA']['MultiFormSession']['plural_name'] = '(ništa)';
$lang['bs_BA']['MultiFormSession']['singular_name'] = '(ništa)';
$lang['bs_BA']['MultiFormStep']['db_Data'] = 'Podaci';
$lang['bs_BA']['MultiFormStep']['plural_name'] = '(ništa)';
$lang['bs_BA']['MultiFormStep']['singular_name'] = '(ništa)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Czech (Czech Republic) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('cs_CZ', $lang) && is_array($lang['cs_CZ'])) {
$lang['cs_CZ'] = array_merge($lang['en_US'], $lang['cs_CZ']);
} else {
$lang['cs_CZ'] = $lang['en_US'];
}
$lang['cs_CZ']['MultiForm']['BACK'] = 'Zpět';
$lang['cs_CZ']['MultiForm']['NEXT'] = 'Další';
$lang['cs_CZ']['MultiForm']['SUBMIT'] = 'Odeslat';
$lang['cs_CZ']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['cs_CZ']['MultiFormSession']['db_IsComplete'] = 'JeKompletni';
$lang['cs_CZ']['MultiFormSession']['has_many_FormSteps'] = 'FormularoveKroky';
$lang['cs_CZ']['MultiFormSession']['plural_name'] = '(žádný)';
$lang['cs_CZ']['MultiFormSession']['singular_name'] = '(žádný)';
$lang['cs_CZ']['MultiFormStep']['db_Data'] = 'Data';
$lang['cs_CZ']['MultiFormStep']['plural_name'] = '(žádný)';
$lang['cs_CZ']['MultiFormStep']['singular_name'] = '(žádný)';
?>

View File

@ -1,28 +0,0 @@
<?php
/**
* Danish (Denmark) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('da_DK', $lang) && is_array($lang['da_DK'])) {
$lang['da_DK'] = array_merge($lang['en_US'], $lang['da_DK']);
} else {
$lang['da_DK'] = $lang['en_US'];
}
$lang['da_DK']['MultiFormSession']['db_Hash'] = 'Havelåge';
$lang['da_DK']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['da_DK']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['da_DK']['MultiFormSession']['plural_name'] = '(ingen)';
$lang['da_DK']['MultiFormSession']['singular_name'] = '(ingen)';
$lang['da_DK']['MultiFormStep']['db_Data'] = 'Data';
$lang['da_DK']['MultiFormStep']['plural_name'] = '(ingen)';
$lang['da_DK']['MultiFormStep']['singular_name'] = '(none)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* German (Germany) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('de_DE', $lang) && is_array($lang['de_DE'])) {
$lang['de_DE'] = array_merge($lang['en_US'], $lang['de_DE']);
} else {
$lang['de_DE'] = $lang['en_US'];
}
$lang['de_DE']['MultiForm']['BACK'] = 'Zurück';
$lang['de_DE']['MultiForm']['NEXT'] = 'Weiter';
$lang['de_DE']['MultiForm']['SUBMIT'] = 'Absenden';
$lang['de_DE']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['de_DE']['MultiFormSession']['db_IsComplete'] = 'Abgeschlossen?';
$lang['de_DE']['MultiFormSession']['has_many_FormSteps'] = 'Formularschritte';
$lang['de_DE']['MultiFormSession']['plural_name'] = 'Multi-Formulare';
$lang['de_DE']['MultiFormSession']['singular_name'] = 'Multi-Formular';
$lang['de_DE']['MultiFormStep']['db_Data'] = 'Daten';
$lang['de_DE']['MultiFormStep']['plural_name'] = 'Multi-Formular-Schritte';
$lang['de_DE']['MultiFormStep']['singular_name'] = 'Multi-Formular-Schritt';
?>

View File

@ -1,11 +1,11 @@
en:
MultiForm:
SilverStripe\MultiForm\MultiForm:
BACK: Back
NEXT: Next
SUBMIT: Submit
MultiFormSession:
SilverStripe\MultiForm\MultiFormSession:
PLURALNAME: 'Multi Form Sessions'
SINGULARNAME: 'Multi Form Session'
MultiFormStep:
SilverStripe\MultiForm\MultiFormStep:
PLURALNAME: 'Multi Form Steps'
SINGULARNAME: 'Multi Form Step'

View File

@ -1,29 +0,0 @@
<?php
global $lang;
$lang['en_US']['MultiForm']['BACK'] = 'Back';
$lang['en_US']['MultiForm']['NEXT'] = 'Next';
$lang['en_US']['MultiForm']['SUBMIT'] = 'Submit';
$lang['en_US']['MultiFormSession']['PLURALNAME'] = array(
'Multi Form Sessions',
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']['SINGULARNAME'] = array(
'Multi Form Session',
50,
'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
);
$lang['en_US']['MultiFormStep']['PLURALNAME'] = array(
'Multi Form Steps',
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']['SINGULARNAME'] = array(
'Multi Form Step',
50,
'Singular name of the object, used in dropdowns and to generally identify a single object in the interface'
);
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Esperanto language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('eo_XX', $lang) && is_array($lang['eo_XX'])) {
$lang['eo_XX'] = array_merge($lang['en_US'], $lang['eo_XX']);
} else {
$lang['eo_XX'] = $lang['en_US'];
}
$lang['eo_XX']['MultiForm']['BACK'] = 'Retro';
$lang['eo_XX']['MultiForm']['NEXT'] = 'Sekva';
$lang['eo_XX']['MultiForm']['SUBMIT'] = 'Transsendi';
$lang['eo_XX']['MultiFormSession']['db_Hash'] = 'Haketo';
$lang['eo_XX']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['eo_XX']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['eo_XX']['MultiFormSession']['plural_name'] = '(neniu)';
$lang['eo_XX']['MultiFormSession']['singular_name'] = '(neniu)';
$lang['eo_XX']['MultiFormStep']['db_Data'] = 'Datumoj';
$lang['eo_XX']['MultiFormStep']['plural_name'] = '(neniu)';
$lang['eo_XX']['MultiFormStep']['singular_name'] = '(neniu)';
?>

View File

@ -1,20 +0,0 @@
<?php
/**
* language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('es_', $lang) && is_array($lang['es_'])) {
$lang['es_'] = array_merge($lang['en_US'], $lang['es_']);
} else {
$lang['es_'] = $lang['en_US'];
}
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Spanish (Argentina) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('es_AR', $lang) && is_array($lang['es_AR'])) {
$lang['es_AR'] = array_merge($lang['en_US'], $lang['es_AR']);
} else {
$lang['es_AR'] = $lang['en_US'];
}
$lang['es_AR']['MultiForm']['BACK'] = 'Volver';
$lang['es_AR']['MultiForm']['NEXT'] = 'Siguiente';
$lang['es_AR']['MultiForm']['SUBMIT'] = 'Enviar';
$lang['es_AR']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['es_AR']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['es_AR']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['es_AR']['MultiFormSession']['plural_name'] = '(ninguno)';
$lang['es_AR']['MultiFormSession']['singular_name'] = '(ninguno)';
$lang['es_AR']['MultiFormStep']['db_Data'] = 'Datos';
$lang['es_AR']['MultiFormStep']['plural_name'] = '(ninguno)';
$lang['es_AR']['MultiFormStep']['singular_name'] = '(ninguno)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Spanish (Mexico) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('es_MX', $lang) && is_array($lang['es_MX'])) {
$lang['es_MX'] = array_merge($lang['en_US'], $lang['es_MX']);
} else {
$lang['es_MX'] = $lang['en_US'];
}
$lang['es_MX']['MultiForm']['BACK'] = 'Atrás';
$lang['es_MX']['MultiForm']['NEXT'] = 'Siguiente';
$lang['es_MX']['MultiForm']['SUBMIT'] = 'Envíar';
$lang['es_MX']['MultiFormSession']['db_Hash'] = 'Desclose';
$lang['es_MX']['MultiFormSession']['db_IsComplete'] = 'Concluido';
$lang['es_MX']['MultiFormSession']['has_many_FormSteps'] = 'Formularios';
$lang['es_MX']['MultiFormSession']['plural_name'] = '(ningunos)';
$lang['es_MX']['MultiFormSession']['singular_name'] = '(ningún)';
$lang['es_MX']['MultiFormStep']['db_Data'] = 'Datos';
$lang['es_MX']['MultiFormStep']['plural_name'] = '(ningunos)';
$lang['es_MX']['MultiFormStep']['singular_name'] = '(ningún)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Estonian (Estonia) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('et_EE', $lang) && is_array($lang['et_EE'])) {
$lang['et_EE'] = array_merge($lang['en_US'], $lang['et_EE']);
} else {
$lang['et_EE'] = $lang['en_US'];
}
$lang['et_EE']['MultiForm']['BACK'] = 'Tagasi';
$lang['et_EE']['MultiForm']['NEXT'] = 'Järgmine';
$lang['et_EE']['MultiForm']['SUBMIT'] = 'Saada';
$lang['et_EE']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['et_EE']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['et_EE']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['et_EE']['MultiFormSession']['plural_name'] = '(none)';
$lang['et_EE']['MultiFormSession']['singular_name'] = '(none)';
$lang['et_EE']['MultiFormStep']['db_Data'] = 'Andmed';
$lang['et_EE']['MultiFormStep']['plural_name'] = '(none)';
$lang['et_EE']['MultiFormStep']['singular_name'] = '(none)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* French (France) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('fr_FR', $lang) && is_array($lang['fr_FR'])) {
$lang['fr_FR'] = array_merge($lang['en_US'], $lang['fr_FR']);
} else {
$lang['fr_FR'] = $lang['en_US'];
}
$lang['fr_FR']['MultiForm']['BACK'] = 'Retour';
$lang['fr_FR']['MultiForm']['NEXT'] = 'Suivant';
$lang['fr_FR']['MultiForm']['SUBMIT'] = 'Envoyez';
$lang['fr_FR']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['fr_FR']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['fr_FR']['MultiFormSession']['has_many_FormSteps'] = 'Formulaire multi-étapes';
$lang['fr_FR']['MultiFormSession']['plural_name'] = '(aucun)';
$lang['fr_FR']['MultiFormSession']['singular_name'] = '(aucun)';
$lang['fr_FR']['MultiFormStep']['db_Data'] = 'Data';
$lang['fr_FR']['MultiFormStep']['plural_name'] = '(aucun)';
$lang['fr_FR']['MultiFormStep']['singular_name'] = '(aucun)';
?>

View File

@ -1,30 +0,0 @@
<?php
/**
* Indonesian (Indonesia) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('id_ID', $lang) && is_array($lang['id_ID'])) {
$lang['id_ID'] = array_merge($lang['en_US'], $lang['id_ID']);
} else {
$lang['id_ID'] = $lang['en_US'];
}
$lang['id_ID']['MultiForm']['BACK'] = 'Kembali';
$lang['id_ID']['MultiForm']['NEXT'] = 'Berikutnya';
$lang['id_ID']['MultiForm']['SUBMIT'] = 'Kirim';
$lang['id_ID']['MultiFormSession']['db_Hash'] = 'Tanda Pagar';
$lang['id_ID']['MultiFormSession']['db_IsComplete'] = 'TelahSelesai';
$lang['id_ID']['MultiFormSession']['plural_name'] = '(tidak ada)';
$lang['id_ID']['MultiFormSession']['singular_name'] = '(tidak ada)';
$lang['id_ID']['MultiFormStep']['db_Data'] = 'Data';
$lang['id_ID']['MultiFormStep']['plural_name'] = '(tidak ada)';
$lang['id_ID']['MultiFormStep']['singular_name'] = '(tidak ada)';
?>

View File

@ -1,25 +0,0 @@
<?php
/**
* Icelandic (Iceland) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('is_IS', $lang) && is_array($lang['is_IS'])) {
$lang['is_IS'] = array_merge($lang['en_US'], $lang['is_IS']);
} else {
$lang['is_IS'] = $lang['en_US'];
}
$lang['is_IS']['MultiFormSession']['plural_name'] = '(ekkert)';
$lang['is_IS']['MultiFormSession']['singular_name'] = '(ekkert)';
$lang['is_IS']['MultiFormStep']['db_Data'] = 'Gögn';
$lang['is_IS']['MultiFormStep']['plural_name'] = '(ekkert)';
$lang['is_IS']['MultiFormStep']['singular_name'] = '(ekkert)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Italian (Italy) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('it_IT', $lang) && is_array($lang['it_IT'])) {
$lang['it_IT'] = array_merge($lang['en_US'], $lang['it_IT']);
} else {
$lang['it_IT'] = $lang['en_US'];
}
$lang['it_IT']['MultiForm']['BACK'] = 'Indietro';
$lang['it_IT']['MultiForm']['NEXT'] = 'Successivo';
$lang['it_IT']['MultiForm']['SUBMIT'] = 'Invia';
$lang['it_IT']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['it_IT']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['it_IT']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['it_IT']['MultiFormSession']['plural_name'] = '(nessuno)';
$lang['it_IT']['MultiFormSession']['singular_name'] = '(nessuno)';
$lang['it_IT']['MultiFormStep']['db_Data'] = 'Dati';
$lang['it_IT']['MultiFormStep']['plural_name'] = '(nessuno)';
$lang['it_IT']['MultiFormStep']['singular_name'] = '(nessuno)';
?>

View File

@ -1,25 +0,0 @@
<?php
/**
* Japanese (Japan) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('ja_JP', $lang) && is_array($lang['ja_JP'])) {
$lang['ja_JP'] = array_merge($lang['en_US'], $lang['ja_JP']);
} else {
$lang['ja_JP'] = $lang['en_US'];
}
$lang['ja_JP']['MultiForm']['BACK'] = '戻る';
$lang['ja_JP']['MultiForm']['NEXT'] = '次へ';
$lang['ja_JP']['MultiForm']['SUBMIT'] = '送信';
$lang['ja_JP']['MultiFormSession']['db_Hash'] = 'ハッシュ';
$lang['ja_JP']['MultiFormStep']['db_Data'] = 'データ';
?>

View File

@ -1,28 +0,0 @@
<?php
/**
* Malay (Malaysia) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('ms_MY', $lang) && is_array($lang['ms_MY'])) {
$lang['ms_MY'] = array_merge($lang['en_US'], $lang['ms_MY']);
} else {
$lang['ms_MY'] = $lang['en_US'];
}
$lang['ms_MY']['MultiFormSession']['db_Hash'] = 'Cincangan';
$lang['ms_MY']['MultiFormSession']['db_IsComplete'] = 'TelahLengkap';
$lang['ms_MY']['MultiFormSession']['has_many_FormSteps'] = 'JejakLangkahBorang';
$lang['ms_MY']['MultiFormSession']['plural_name'] = '(tiada)';
$lang['ms_MY']['MultiFormSession']['singular_name'] = '(tiada)';
$lang['ms_MY']['MultiFormStep']['db_Data'] = 'Data';
$lang['ms_MY']['MultiFormStep']['plural_name'] = '(tiada)';
$lang['ms_MY']['MultiFormStep']['singular_name'] = '(tiada)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Norwegian Bokmal (Norway) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('nb_NO', $lang) && is_array($lang['nb_NO'])) {
$lang['nb_NO'] = array_merge($lang['en_US'], $lang['nb_NO']);
} else {
$lang['nb_NO'] = $lang['en_US'];
}
$lang['nb_NO']['MultiForm']['BACK'] = 'Forrige';
$lang['nb_NO']['MultiForm']['NEXT'] = 'Neste';
$lang['nb_NO']['MultiForm']['SUBMIT'] = 'Send';
$lang['nb_NO']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['nb_NO']['MultiFormSession']['db_IsComplete'] = 'ErFullflørt';
$lang['nb_NO']['MultiFormSession']['has_many_FormSteps'] = 'SkjemaSteg';
$lang['nb_NO']['MultiFormSession']['plural_name'] = '(ingen)';
$lang['nb_NO']['MultiFormSession']['singular_name'] = '(ingen)';
$lang['nb_NO']['MultiFormStep']['db_Data'] = 'Data';
$lang['nb_NO']['MultiFormStep']['plural_name'] = '(ingen)';
$lang['nb_NO']['MultiFormStep']['singular_name'] = '(ingen)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Dutch (Netherlands) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('nl_NL', $lang) && is_array($lang['nl_NL'])) {
$lang['nl_NL'] = array_merge($lang['en_US'], $lang['nl_NL']);
} else {
$lang['nl_NL'] = $lang['en_US'];
}
$lang['nl_NL']['MultiForm']['BACK'] = 'Terug';
$lang['nl_NL']['MultiForm']['NEXT'] = 'Volgende';
$lang['nl_NL']['MultiForm']['SUBMIT'] = 'Versturen';
$lang['nl_NL']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['nl_NL']['MultiFormSession']['db_IsComplete'] = 'IsCompleet';
$lang['nl_NL']['MultiFormSession']['has_many_FormSteps'] = 'FormulierStappen';
$lang['nl_NL']['MultiFormSession']['plural_name'] = '(geen)';
$lang['nl_NL']['MultiFormSession']['singular_name'] = '(geen)';
$lang['nl_NL']['MultiFormStep']['db_Data'] = 'Data';
$lang['nl_NL']['MultiFormStep']['plural_name'] = '(geen)';
$lang['nl_NL']['MultiFormStep']['singular_name'] = '(geen)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Polish (Poland) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('pl_PL', $lang) && is_array($lang['pl_PL'])) {
$lang['pl_PL'] = array_merge($lang['en_US'], $lang['pl_PL']);
} else {
$lang['pl_PL'] = $lang['en_US'];
}
$lang['pl_PL']['MultiForm']['BACK'] = 'Wstecz';
$lang['pl_PL']['MultiForm']['NEXT'] = 'Dalej';
$lang['pl_PL']['MultiForm']['SUBMIT'] = 'Wyślij';
$lang['pl_PL']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['pl_PL']['MultiFormSession']['db_IsComplete'] = 'CzyGotowe';
$lang['pl_PL']['MultiFormSession']['has_many_FormSteps'] = 'KrokiFormularza';
$lang['pl_PL']['MultiFormSession']['plural_name'] = '(brak)';
$lang['pl_PL']['MultiFormSession']['singular_name'] = '(brak)';
$lang['pl_PL']['MultiFormStep']['db_Data'] = 'Dane';
$lang['pl_PL']['MultiFormStep']['plural_name'] = '(brak)';
$lang['pl_PL']['MultiFormStep']['singular_name'] = '(brak)';
?>

View File

@ -1,27 +0,0 @@
<?php
/**
* Portuguese (Portugal) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('pt_PT', $lang) && is_array($lang['pt_PT'])) {
$lang['pt_PT'] = array_merge($lang['en_US'], $lang['pt_PT']);
} else {
$lang['pt_PT'] = $lang['en_US'];
}
$lang['pt_PT']['MultiFormSession']['db_IsComplete'] = 'Está completa';
$lang['pt_PT']['MultiFormSession']['has_many_FormSteps'] = 'Passos do formulário';
$lang['pt_PT']['MultiFormSession']['plural_name'] = '(nenhum)';
$lang['pt_PT']['MultiFormSession']['singular_name'] = '(nenhum)';
$lang['pt_PT']['MultiFormStep']['db_Data'] = 'Dados';
$lang['pt_PT']['MultiFormStep']['plural_name'] = '(nenhum)';
$lang['pt_PT']['MultiFormStep']['singular_name'] = '(nenhum)';
?>

View File

@ -1,26 +0,0 @@
<?php
/**
* Serbian (Serbia) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('sr_RS', $lang) && is_array($lang['sr_RS'])) {
$lang['sr_RS'] = array_merge($lang['en_US'], $lang['sr_RS']);
} else {
$lang['sr_RS'] = $lang['en_US'];
}
$lang['sr_RS']['MultiFormSession']['db_IsComplete'] = 'ЈеЗавршен';
$lang['sr_RS']['MultiFormSession']['plural_name'] = '(без)';
$lang['sr_RS']['MultiFormSession']['singular_name'] = '(без)';
$lang['sr_RS']['MultiFormStep']['db_Data'] = 'Подаци';
$lang['sr_RS']['MultiFormStep']['plural_name'] = '(без)';
$lang['sr_RS']['MultiFormStep']['singular_name'] = '(без)';
?>

View File

@ -1,28 +0,0 @@
<?php
/**
* Swedish (Sweden) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('sv_SE', $lang) && is_array($lang['sv_SE'])) {
$lang['sv_SE'] = array_merge($lang['en_US'], $lang['sv_SE']);
} else {
$lang['sv_SE'] = $lang['en_US'];
}
$lang['sv_SE']['MultiForm']['BACK'] = 'Tillbaka';
$lang['sv_SE']['MultiForm']['NEXT'] = 'Nästa';
$lang['sv_SE']['MultiForm']['SUBMIT'] = 'Skicka';
$lang['sv_SE']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['sv_SE']['MultiFormSession']['plural_name'] = '(ingen)';
$lang['sv_SE']['MultiFormStep']['db_Data'] = 'Data';
$lang['sv_SE']['MultiFormStep']['plural_name'] = '(inga)';
$lang['sv_SE']['MultiFormStep']['singular_name'] = '(ingen)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Turkish (Turkey) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('tr_TR', $lang) && is_array($lang['tr_TR'])) {
$lang['tr_TR'] = array_merge($lang['en_US'], $lang['tr_TR']);
} else {
$lang['tr_TR'] = $lang['en_US'];
}
$lang['tr_TR']['MultiForm']['BACK'] = 'Geri';
$lang['tr_TR']['MultiForm']['NEXT'] = 'İleri';
$lang['tr_TR']['MultiForm']['SUBMIT'] = 'Gönder';
$lang['tr_TR']['MultiFormSession']['db_Hash'] = 'Hash';
$lang['tr_TR']['MultiFormSession']['db_IsComplete'] = 'IsComplete';
$lang['tr_TR']['MultiFormSession']['has_many_FormSteps'] = 'FormSteps';
$lang['tr_TR']['MultiFormSession']['plural_name'] = '(hiçbiri)';
$lang['tr_TR']['MultiFormSession']['singular_name'] = '(hiçbiri)';
$lang['tr_TR']['MultiFormStep']['db_Data'] = 'Veri';
$lang['tr_TR']['MultiFormStep']['plural_name'] = '(hiçbiri)';
$lang['tr_TR']['MultiFormStep']['singular_name'] = '(hiçbiri)';
?>

View File

@ -1,31 +0,0 @@
<?php
/**
* Chinese (China) language pack
* @package modules: multiform
* @subpackage i18n
*/
i18n::include_locale_file('modules: multiform', 'en_US');
global $lang;
if(array_key_exists('zh_CN', $lang) && is_array($lang['zh_CN'])) {
$lang['zh_CN'] = array_merge($lang['en_US'], $lang['zh_CN']);
} else {
$lang['zh_CN'] = $lang['en_US'];
}
$lang['zh_CN']['MultiForm']['BACK'] = '区域';
$lang['zh_CN']['MultiForm']['NEXT'] = '下一个';
$lang['zh_CN']['MultiForm']['SUBMIT'] = '提交';
$lang['zh_CN']['MultiFormSession']['db_Hash'] = 'Hash 字符串';
$lang['zh_CN']['MultiFormSession']['db_IsComplete'] = '是否已完成';
$lang['zh_CN']['MultiFormSession']['has_many_FormSteps'] = '表单步骤';
$lang['zh_CN']['MultiFormSession']['plural_name'] = '多名称';
$lang['zh_CN']['MultiFormSession']['singular_name'] = '单名称';
$lang['zh_CN']['MultiFormStep']['db_Data'] = '数据';
$lang['zh_CN']['MultiFormStep']['plural_name'] = '多名称';
$lang['zh_CN']['MultiFormStep']['singular_name'] = '单名称';
?>

View File

@ -1,4 +1,4 @@
Copyright (c) 2016, SilverStripe Limited
Copyright (c) 2017, SilverStripe Limited
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

13
phpunit.xml.dist Normal file
View File

@ -0,0 +1,13 @@
<phpunit bootstrap="framework/tests/bootstrap.php" colors="true">
<testsuite name="Default">
<directory>tests/</directory>
</testsuite>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src/</directory>
<exclude>
<directory suffix=".php">tests/</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

754
src/Models/MultiForm.php Normal file
View File

@ -0,0 +1,754 @@
<?php
namespace SilverStripe\MultiForm\Models;
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Session;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\View\SSViewer;
/**
* MultiForm 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>
* private static $allowed_actions = array('next','prev');
* </code>
*
*/
abstract class MultiForm extends Form
{
/**
* A session object stored in the database, to identify and store
* data for this MultiForm instance.
*
* @var MultiFormSession
*/
protected $session;
/**
* The current encrypted MultiFormSession identification.
*
* @var string
*/
protected $currentSessionHash;
/**
* Defines which subclass of {@link MultiFormStep} should be the first
* step in the multi-step process.
*
* @var string Classname of a {@link MultiFormStep} subclass
*/
private static $start_step;
/**
* Set the casting for these fields.
*
* @var array
*/
private static $casting = [
'CompletedStepCount' => 'Int',
'TotalStepCount' => 'Int',
'CompletedPercent' => 'Float'
];
/**
* @var string
*/
private static $get_var = 'MultiFormSessionID';
/**
* 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
*/
private static $ignored_fields = [
'url',
'executeForm',
'SecurityID'
];
/**
* Any of the actions defined in this variable are exempt from
* being validated.
*
* This is most useful for the "Back" (action_prev) action, as
* you typically don't validate the form when the user is going
* back a step.
*
* @var array
*/
private static $actions_exempt_from_validation = [
'action_prev'
];
/**
* @var string
*/
protected $displayLink;
/**
* Flag which is being used in getAllStepsRecursive() to allow adding the completed flag on the steps
*
* @var boolean
*/
protected $currentStepHasBeenFound = false;
/**
* Start the MultiForm instance.
*
* @param Controller $controller Controller instance this form is created on
* @param string $name The form name, typically the same as the method name
*/
public function __construct($controller, $name)
{
// First set the controller and name manually so they are available for
// field construction.
$this->controller = $controller;
$this->name = $name;
// 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 the step returned above as the current step
$this->setCurrentStep($currentStep);
// Set the form of the step to this form instance
$currentStep->setForm($this);
// 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)
$validator = null;
$applyValidation = true;
$actionNames = $this->config()->get('actions_exempt_from_validation');
if ($actionNames) {
foreach ($actionNames as $exemptAction) {
if (!empty($this->getRequest()->requestVar($exemptAction))) {
$applyValidation = false;
break;
}
}
}
// Apply validation if the current step requires validation (is not exempt)
if ($applyValidation) {
if ($currentStep->getValidator()) {
$this->setValidator($currentStep->getValidator());
}
}
// Give the fields, actions, and validation for the current step back to the parent Form class
parent::__construct($controller, $name, $fields, $actions);
$getVar = $this->getGetVar();
// Set a hidden field in our form with an encrypted hash to identify this session.
$this->fields->push(HiddenField::create($getVar, false, $this->session->Hash));
// If there is saved data for the current step, we load it into the form it here
//(CAUTION: loadData() MUST unserialize first!)
if ($data = $currentStep->loadData()) {
$this->loadDataFrom($data);
}
// Disable security token - we tie a form to a session ID instead
$this->disableSecurityToken();
$this->config()->merge('ignored_fields', $getVar);
}
/**
* Accessor method to $this->controller.
*
* @return Controller this MultiForm was instanciated on.
*/
public function getController()
{
return $this->controller;
}
/**
* Returns the get_var to the template engine
*
* @return string
*/
public function getGetVar()
{
return $this->config()->get('get_var');
}
/**
* 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()
{
$startStepClass = $this->config()->get('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 $start_step on ' . $this->class,
E_USER_ERROR
);
}
// Determine whether we use the current step, or create one if it doesn't exist
$currentStep = null;
$StepID = $this->controller->getRequest()->getVar('StepID');
if (isset($StepID)) {
$currentStep = DataObject::get_one(
MultiFormStep::class,
[
'SessionID' => $this->session->ID,
'ID' => $StepID
]
);
} elseif ($this->session->CurrentStepID) {
$currentStep = $this->session->CurrentStep();
}
// Always fall back to creating a new step (in case the session or request data is invalid)
if (!$currentStep || !$currentStep->ID) {
$currentStep = Injector::inst()->create($startStepClass);
$currentStep->SessionID = $this->session->ID;
$currentStep->write();
$this->session->CurrentStepID = $currentStep->ID;
$this->session->write();
$this->session->flushCache();
}
if ($currentStep) {
$currentStep->setForm($this);
}
return $currentStep;
}
/**
* 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;
$step->setForm($this);
return $this->session->write();
}
/**
* Accessor method to $this->session.
*
* @return MultiFormSession
*/
public function getMultiFormSession()
{
if (!$this->session) {
$this->setSession();
}
return $this->session;
}
/**
* Set up the session.
*
* 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.
*
* @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()
{
$this->session = $this->getCurrentSession();
// If there was no session found, create a new one instead
if (!$this->session) {
$this->session = MultiFormSession::create();
}
// 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();
}
}
/**
* Set the currently used encrypted hash to identify
* the MultiFormSession.
*
* @param string $hash Encrypted identification to session
*/
public function setCurrentSessionHash($hash)
{
$this->currentSessionHash = $hash;
$this->setSession();
}
/**
* Return the currently used {@link MultiFormSession}
* @return MultiFormSession|boolean FALSE
*/
public function getCurrentSession()
{
if (!$this->currentSessionHash) {
$this->currentSessionHash = $this->controller->getRequest()->getVar($this->getGetVar());
if (!$this->currentSessionHash) {
return false;
}
}
$this->session = MultiFormSession::get()->filter([
"Hash" => $this->currentSessionHash,
"IsComplete" => 0
])->first();
return $this->session;
}
/**
* 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()}.
*
* @param string $filter SQL WHERE statement
* @return DataList|boolean A set of MultiFormStep subclasses
*/
public function getSavedSteps($filter = null)
{
$filter .= ($filter) ? ' AND ' : '';
$filter .= sprintf("\"SessionID\" = '%s'", $this->session->ID);
return DataObject::get(MultiFormStep::class, $filter);
}
/**
* 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 DataObject
*/
public function getSavedStepByClass($className)
{
return DataObject::get_one(
MultiFormStep::class,
sprintf(
"\"SessionID\" = '%s' AND \"ClassName\" = '%s'",
$this->session->ID,
Convert::raw2sql($className)
)
);
}
/**
* Build a FieldList 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,
* 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.
*
* @param MultiFormStep $step Subclass of MultiFormStep
* @return FieldList of FormAction objects
*/
public function actionsFor($step)
{
// Create default multi step actions (next, prev), and merge with extra actions, if any
$actions = FieldList::create();
// 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 ($step->isFinalStep()) {
$actions->push(FormAction::create('finish', $step->getSubmitText()));
} else {
$actions->push(FormAction::create('next', $step->getNextText()));
}
// 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
if ($step->getNextStep()) {
$actions->insertBefore($prev = FormAction::create('prev', $step->getPrevText()), 'action_next');
// Assume that this is the last step, insert the action before the finish action
} else {
$actions->insertBefore($prev = FormAction::create('prev', $step->getPrevText()), 'action_finish');
}
//remove browser validation from prev action
$prev->setAttribute("formnovalidate", "formnovalidate");
}
// Merge any extra action fields defined on the step
$actions->merge($step->getExtraActions());
return $actions;
}
/**
* 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.
*
* @return SSViewer object to render the template with
*/
public function forTemplate()
{
$return = $this->renderWith([
$this->getCurrentStep()->class,
'MultiFormStep',
$this->class,
'MultiForm',
'Form'
]);
$this->clearMessage();
return $return;
}
/**
* 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 Form $form The form that the action was called on
* @return bool
*/
public function finish($data, $form)
{
// Save the form data for the current step
$this->save($data);
if (!$this->getCurrentStep()->isFinalStep()) {
$this->controller->redirectBack();
return false;
}
if (!$this->getCurrentStep()->validateStep($data, $form)) {
$this->getRequest()->getSession()->set("FormInfo.{$form->FormName()}.data", $form->getData());
$this->controller->redirectBack();
return false;
}
}
/**
* 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 (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 Form $form The form that the action was called on
* @return bool
*/
public function next($data, $form)
{
// Save the form data for the current step
$this->save($form->getData());
// Get the next step class
$nextStepClass = $this->getCurrentStep()->getNextStep();
if (!$nextStepClass) {
$this->controller->redirectBack();
return false;
}
// Perform custom step validation (use MultiFormStep->getValidator() for
// built-in functionality). The data needs to be manually saved on error
// so the form is re-populated.
if (!$this->getCurrentStep()->validateStep($data, $form)) {
$this->getRequest()->getSession()->set("FormInfo.{$form->FormName()}.data", $form->getData());
$this->controller->redirectBack();
return false;
}
// validation succeeded so we reset it to remove errors and messages
$this->clearFormState();
// 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 = Injector::inst()->create($nextStepClass);
$nextStep->SessionID = $this->session->ID;
$nextStep->write();
}
// Set the next step found as the current step
$this->setCurrentStep($nextStep);
// Redirect to the next step
$this->controller->redirect($nextStep->Link());
}
/**
* Determine what to do when the previous action is called.
*
* 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 Form $form The form that the action was called on
* @return bool|HTTPResponse
*/
public function prev($data, $form)
{
// Save the form data for the current step
$this->save($form->getData());
// Get the previous step class
$prevStepClass = $this->getCurrentStep()->getPreviousStep();
if (!$prevStepClass && !$this->getCurrentStep()->canGoBack()) {
$this->controller->redirectBack();
return false;
}
// 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->setCurrentStep($prevStep);
// Redirect to the previous step
return $this->controller->redirect($prevStep->Link());
}
/**
* Save the raw data given back from the form into session.
*
* 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
*/
protected function save($data)
{
$currentStep = $this->getCurrentStep();
if (is_array($data)) {
foreach ($data as $field => $value) {
if (in_array($field, $this->config()->get('ignored_fields'))) {
unset($data[$field]);
}
}
$currentStep->saveData($data);
}
return;
}
// ############ Misc ############
/**
* 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.
*
* @return string
*/
public function FormAction()
{
$action = parent::FormAction();
$action .= (strpos($action, '?')) ? '&amp;' : '?';
$action .= "{$this->getGetVar()}={$this->session->Hash}";
return $action;
}
/**
* Returns the link to the page where the form is displayed. The user is
* redirected to this link with a session param after each step is
* submitted.
*
* @return string
*/
public function getDisplayLink()
{
return $this->displayLink ? $this->displayLink : Controller::curr()->Link();
}
/**
* Set the link to the page on which the form is displayed.
*
* The link defaults to the controllers current link. However if the form
* is displayed inside an action the display link must be explicitly set.
*
* @param string $link
*/
public function setDisplayLink($link)
{
$this->displayLink = $link;
}
/**
* Determine the steps to show in a linear fashion, starting from the
* first step. We run {@link getAllStepsRecursive} passing the steps found
* by reference to get a listing of the steps.
*
* @return ArrayList of MultiFormStep instances
*/
public function getAllStepsLinear()
{
$stepsFound = ArrayList::create();
$firstStep = DataObject::get_one($this->config()->get('start_step'), "\"SessionID\" = {$this->session->ID}");
$firstStep->LinkingMode = ($firstStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
$firstStep->setForm($this);
$stepsFound->push($firstStep);
// mark the further steps as non-completed if the first step is the current
if ($firstStep->ID == $this->getCurrentStep()->ID) {
$this->currentStepHasBeenFound = true;
} else {
$firstStep->addExtraClass('completed');
}
$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.
* 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 MultiFormStep $step Subclass of MultiFormStep to find the next step of
* @param $stepsFound $stepsFound DataObjectSet reference, the steps found to call back on
* @return DataList of MultiFormStep instances
*/
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
$nextStep = $step->getNextStepFromDatabase();
if (!$nextStep) {
// 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());
}
// once the current steps has been found we won't add the completed class anymore.
if ($nextStep->ID == $this->getCurrentStep()->ID) {
$this->currentStepHasBeenFound = true;
}
$nextStep->LinkingMode = ($nextStep->ID == $this->getCurrentStep()->ID) ? 'current' : 'link';
// add the completed class
if (!$this->currentStepHasBeenFound) {
$nextStep->addExtraClass('completed');
}
$nextStep->setForm($this);
// Add the array data, and do a callback
$stepsFound->push($nextStep);
$this->getAllStepsRecursive($nextStep, $stepsFound);
}
// Once we've reached the final step, we just return what we've collected
} else {
return $stepsFound;
}
}
/**
* 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.
*
* @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()
{
$steps = DataObject::get(MultiFormStep::class, "\"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->getCompletedStepCount() * 100 / $this->getTotalStepCount();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace SilverStripe\MultiForm\Models;
use SilverStripe\ORM\DataObject;
use SilverStripe\Security\Member;
use SilverStripe\Security\Security;
/**
* 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.
*
*/
class MultiFormSession extends DataObject
{
private static $db = [
'Hash' => 'Varchar(40)', // cryptographic hash identification to this session
'IsComplete' => 'Boolean' // flag to determine if this session is marked completed
];
private static $has_one = [
'Submitter' => Member::class,
'CurrentStep' => MultiFormStep::class
];
private static $has_many = [
'FormSteps' => MultiFormStep::class
];
private static $table_name = 'MultiFormSession';
/**
* 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.
*/
public function onBeforeWrite()
{
// save submitter if a Member is logged in
$currentMember = Security::getCurrentUser();
if (!$this->SubmitterID && $currentMember) {
$this->SubmitterID = $currentMember->ID;
}
parent::onBeforeWrite();
}
/**
* These actions are performed when delete() is called on this object.
*/
public function onBeforeDelete()
{
// delete dependent form steps and relation
$steps = $this->FormSteps();
if ($steps) {
foreach ($steps as $step) {
if ($step && $step->exists()) {
$steps->remove($step);
$step->delete();
$step->destroy();
}
}
}
parent::onBeforeDelete();
}
}

View File

@ -0,0 +1,522 @@
<?php
namespace SilverStripe\MultiForm\Models;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\Validator;
use SilverStripe\ORM\DataObject;
/**
* MultiFormStep controls the behaviour of a single form step in the MultiForm
* process. All form steps are required to be subclasses of this class, as it
* encapsulates the functionality required for the step to be aware of itself
* in the process by knowing what it's next step is, and if applicable, it's previous
* step.
*
*/
class MultiFormStep extends DataObject
{
private static $db = [
'Data' => 'Text' // stores serialized maps with all session information
];
private static $has_one = [
'Session' => MultiFormSession::class
];
private static $table_name = 'MultiFormStep';
/**
* Centerpiece of the flow control for the form.
*
* If set to a string, you 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
*/
private 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
*/
private static $is_final_step = false;
/**
* 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
*/
protected static $can_go_back = true;
/**
* Title of this step.
*
* Used for the step indicator templates.
*
* @var string
*/
protected $title;
/**
* Form class that this step is directly related to.
*
* @var MultiForm subclass
*/
protected $form;
/**
* List of additional CSS classes for this step
*
* @var array $extraClasses
*/
protected $extraClasses = [];
/**
* Temporary cache to increase the performance for repeated look ups.
*
* @var array $cache
*/
protected $step_data_cache = [];
/**
* Form fields to be rendered with this step.
* (Form object is created in {@link MultiForm}.
*
* This function needs to be implemented on your
* subclasses of MultiFormStep.
*
* @return FieldList
*/
public function getFields()
{
user_error('Please implement getFields on your MultiFormStep subclass', E_USER_ERROR);
}
/**
* Additional form actions to be added to this step.
* (Form object is created in {@link MultiForm}.
*
* Note: This is optional, and is to be implemented
* on your subclasses of MultiFormStep.
*
* @return FieldList
*/
public function getExtraActions()
{
return FieldList::create();
}
/**
* Get a validator specific to this form.
* The form is automatically validated in {@link Form->httpSubmission()}.
*
* @return bool|Validator
*/
public function getValidator()
{
return false;
}
/**
* Accessor method for $this->title
*
* @return string Title of this step
*/
public function getTitle()
{
return $this->title ? $this->title : get_class($this);
}
/**
* 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()
{
$form = $this->form;
return Controller::join_links(
$form->getDisplayLink(),
"?{$form->getGetVar()}={$this->getSession()->Hash}"
);
}
/**
* Unserialize stored session data and return it.
* 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, leaving
* others that are not required.
*
* @return array
*/
public function loadData()
{
return ($this->Data && is_string($this->Data)) ? unserialize($this->Data) : [];
}
/**
* 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 {@link MultiForm}
*/
public function saveData($data)
{
$this->Data = serialize($data);
$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
* @return DataObject
*/
public function saveInto($obj)
{
$form = Form::create(
Controller::curr(),
'Form',
$this->getFields(),
FieldList::create()
);
$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
* 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()}.
*
* @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
*
* @return string Classname of a {@link MultiFormStep} subclass
*/
public function getNextStep()
{
$nextSteps = $this->config()->get('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, and then gone back a step.
*
* @return MultiFormStep|boolean|void
*/
public function getNextStepFromDatabase()
{
if ($this->SessionID && is_numeric($this->SessionID)) {
$nextSteps = $this->config()->get('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;
}
}
}
/**
* Accessor method for self::$next_steps
*
* @return string|array
*/
public function getNextSteps()
{
return $this->config()->get('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|void Classname of a {@link MultiFormStep} subclass
*/
public function getPreviousStep()
{
$steps = DataObject::get(MultiFormStep::class, "\"SessionID\" = {$this->SessionID}", '"LastEdited" DESC');
if ($steps) {
foreach ($steps as $step) {
$step->setForm($this->form);
if ($step->getNextStep()) {
if ($step->getNextStep() == $this->class) {
return $step->class;
}
}
}
}
}
/**
* 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()
{
if ($prevStepClass = $this->getPreviousStep()) {
return DataObject::get_one($prevStepClass, "\"SessionID\" = {$this->SessionID}");
}
}
/**
* Get the text to the use on the button to the previous step.
* @return string
*/
public function getPrevText()
{
return _t(__CLASS__ . '.BACK', 'Back');
}
/**
* Get the text to use on the button to the next step.
* @return string
*/
public function getNextText()
{
return _t(__CLASS__ . '.NEXT', 'Next');
}
/**
* Get the text to use on the button to submit the form.
* @return string
*/
public function getSubmitText()
{
return _t(__CLASS__ . '.SUBMIT', 'Submit');
}
/**
* Sets the form that this step is directly related to.
*
* @param MultiForm $form subclass
*/
public function setForm($form)
{
$this->form = $form;
}
/**
* @return Form
*/
public function getForm()
{
return $this->form;
}
// ##################### Utility ####################
/**
* Determines whether the user is able to go back using the "action_back"
* Determines whether the user is able to go back using the "action_back"
* 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->config()->get('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.
*
* @return boolean
*/
public function isFinalStep()
{
return $this->config()->get('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->getSession()->CurrentStep()->class) ? true : false;
}
/**
* Add a CSS-class to the step. If needed, multiple classes can be added by delimiting a string with spaces.
*
* @param string $class A string containing a classname or several class names delimited by a space.
* @return MultiFormStep
*/
public function addExtraClass($class)
{
// split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
// add classes one by one
$this->extraClasses[$class] = $class;
}
return $this;
}
/**
* Remove a CSS-class from the step. Multiple classes names can be passed through as a space delimited string.
*
* @param string $class
* @return MultiFormStep
*/
public function removeExtraClass($class)
{
// split at white space
$classes = preg_split('/\s+/', $class);
foreach ($classes as $class) {
// unset one by one
unset($this->extraClasses[$class]);
}
return $this;
}
/**
* @return string
*/
public function getExtraClasses()
{
return join(' ', array_keys($this->extraClasses));
}
/**
* Returns the submitted value, if any, of any steps.
*
* @param string $fromStep (classname)
* @param string $key
*
* @return mixed
*/
public function getValueFromOtherStep($fromStep, $key)
{
// load the steps in the cache, if this one doesn't exist
if (!array_key_exists('steps_' . $fromStep, $this->step_data_cache)) {
$steps = self::get()->filter('SessionID', $this->form->session->ID);
if ($steps) {
foreach ($steps as $step) {
$this->step_data_cache['steps_' . $step->ClassName] = $step->loadData();
}
}
}
// check both as PHP isn't recursive
if (isset($this->step_data_cache['steps_' . $fromStep])) {
if (isset($this->step_data_cache['steps_' . $fromStep][$key])) {
return $this->step_data_cache['steps_' . $fromStep][$key];
}
}
return null;
}
/**
* allows to get a value from another step copied over
*
* @param FieldList $fields
* @param string $formStep
* @param string $fieldName
* @param string $fieldNameTarget (optional)
*/
public function copyValueFromOtherStep(FieldList $fields, $formStep, $fieldName, $fieldNameTarget = null)
{
// if a target field isn't defined use the same fieldname
if (!$fieldNameTarget) {
$fieldNameTarget = $fieldName;
}
$fields->fieldByName($fieldNameTarget)->setValue($this->getValueFromOtherStep($formStep, $fieldName));
}
/**
* Gets the linked MultiFormSession
* @return MultiFormSession
*/
public function getSession()
{
return $this->Session();
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace SilverStripe\MultiForm\Extensions;
use SilverStripe\MultiForm\Models\MultiFormSession;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\Queries\SQLSelect;
/**
* 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
*
*/
class MultiFormObjectDecorator extends DataExtension
{
private static $db = [
'MultiFormIsTemporary' => 'Boolean',
];
private static $has_one = [
'MultiFormSession' => MultiFormSession::class,
];
/**
* Augment any queries to MultiFormObjectDecorator and only
* return anything that isn't considered temporary.
* @param SQLSelect $query
* @param DataQuery|null $dataQuery
*/
public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
{
$where = $query->getWhere();
if (!$where && !$this->wantsTemporary($query)) {
$from = array_values($query->getFrom());
$query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'");
return;
}
$filterKey = key($where[0]);
if (strpos($filterKey, ".`ID` = ") === false
&& strpos($filterKey, ".ID = ") === false
&& strpos($filterKey, "ID = ") !== 0
&& !$this->wantsTemporary($query)
) {
$from = array_values($query->getFrom());
$query->addWhere("{$from[0]}.\"MultiFormIsTemporary\" = '0'");
}
}
/**
* Determines if the current query is supposed
* to be exempt from the automatic filtering out
* of temporary records.
*
* @param SQLSelect $query
* @return boolean
*/
protected function wantsTemporary($query)
{
foreach ($query->getWhere() as $whereClause) {
$from = array_values($query->getFrom());
// SQLQuery will automatically add double quotes and single quotes to values, so check against that.
$key = key($whereClause);
if ($key == "{$from[0]}.\"MultiFormIsTemporary\" = ?" && current($whereClause[$key]) == 1) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace SilverStripe\MultiForm\Tasks;
use SilverStripe\Dev\BuildTask;
use SilverStripe\MultiForm\Models\MultiFormSession;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
/**
* 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 process() method through cli-script.php:
* `php framework/cli-script.php MultiFormPurgeTask`
* or
* `framework/sake MultiFormPurgeTask`
*
*/
class MultiFormPurgeTask extends BuildTask
{
/**
* Days after which sessions expire and
* are automatically deleted.
*
* @var int
*/
private static $session_expiry_days = 7;
private static $segment = 'MultiFormPurgeTask';
/**
* 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($request)
{
$sessions = $this->getExpiredSessions();
$delCount = 0;
if ($sessions) {
foreach ($sessions as $session) {
$session->delete();
$delCount++;
}
}
echo $delCount . ' session records deleted that were older than '
. $this->config()->get('session_expiry_days') . ' days.'. PHP_EOL;
}
/**
* Return all MultiFormSession database records that are older than
* the days specified in $session_expiry_days
*
* @return DataList
*/
protected function getExpiredSessions()
{
return DataObject::get(
MultiFormSession::class,
"DATEDIFF(NOW(), \"MultiFormSession\".\"Created\") > " . $this->config()->get('session_expiry_days')
);
}
}

View File

@ -1,9 +1,19 @@
<ul class="stepIndicator current-$CurrentStep.class">
<% loop AllStepsLinear %>
<li class="$ClassName<% if LinkingMode %> $LinkingMode<% end_if %><% if FirstLast %> $FirstLast<% end_if %><% if $ExtraClasses %> $ExtraClasses<% end_if %>">
<% if LinkingMode = current %><% else %><% if ID %><a href="{$Top.URLSegment}/?${Top.GetVar}={$SessionID}&amp;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_loop %>
<% loop $AllStepsLinear %>
<li class="$ClassName<% if $LinkingMode %> $LinkingMode<% end_if %><% if $FirstLast %> $FirstLast<% end_if %><% if $ExtraClasses %> $ExtraClasses<% end_if %>">
<% if $LinkingMode = current %>
<% else %>
<% if $ID %>
<a href="{$Top.URLSegment}/?${Top.GetVar}={$SessionID}&amp;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_loop %>
</ul>

View File

@ -1 +1 @@
<p>You've completed {$CompletedPercent.Nice}% ($CompletedStepCount/$TotalStepCount)</p>
<p><%t SilverStripe\\MultiForm\\MultiForm.ProgressPercent "You've completed {percent}% ({completedSteps}/{totalSteps})" percent=$CompletedPercent.Nice completedSteps=$CompletedStepCount totalSteps$TotalStepCount %></p>

View File

@ -1,44 +1,42 @@
<?php
class MultiFormObjectDecoratorTest extends SapphireTest {
protected static $fixture_file = 'MultiFormObjectDecoratorTest.yml';
namespace SilverStripe\MultiForm\Tests;
protected $requiredExtensions = array(
'MultiFormObjectDecorator_DataObject' => array('MultiFormObjectDecorator')
);
use SilverStripe\Dev\SapphireTest;
use SilverStripe\MultiForm\Extensions\MultiFormObjectDecorator;
use SilverStripe\MultiForm\Tests\Stubs\MultiFormObjectDecoratorDataObject;
protected $extraDataObjects = array(
'MultiFormObjectDecorator_DataObject'
);
class MultiFormObjectDecoratorTest extends SapphireTest
{
protected static $fixture_file = 'MultiFormObjectDecoratorTest.yml';
public function testTemporaryDataFilteredQuery() {
$records = MultiFormObjectDecorator_DataObject::get()
->map('Name')
->toArray();
protected static $required_extensions = [
MultiFormObjectDecoratorDataObject::class => [MultiFormObjectDecorator::class]
];
$this->assertContains('Test 1', $records);
$this->assertContains('Test 2', $records);
$this->assertNotContains('Test 3', $records);
protected static $extra_dataobjects = [
MultiFormObjectDecoratorDataObject::class
];
}
public function testTemporaryDataFilteredQuery()
{
$records = MultiFormObjectDecoratorDataObject::get()
->map('Name')
->toArray();
public function testTemporaryDataQuery() {
$records = MultiFormObjectDecorator_DataObject::get()
->filter(array('MultiFormIsTemporary' => 1))
->map('Name')
->toArray();
$this->assertNotContains('Test 1', $records);
$this->assertNotContains('Test 2', $records);
$this->assertContains('Test 3', $records);
}
}
class MultiFormObjectDecorator_DataObject extends DataObject {
private static $db = array(
'Name' => 'Varchar'
);
$this->assertContains('Test 1', $records);
$this->assertContains('Test 2', $records);
$this->assertNotContains('Test 3', $records);
}
public function testTemporaryDataQuery()
{
$records = MultiFormObjectDecoratorDataObject::get()
->filter(['MultiFormIsTemporary' => 1])
->map('Name')
->toArray();
$this->assertNotContains('Test 1', $records);
$this->assertNotContains('Test 2', $records);
$this->assertContains('Test 3', $records);
}
}

View File

@ -1,4 +1,4 @@
MultiFormObjectDecorator_DataObject:
SilverStripe\MultiForm\Tests\Stubs\MultiFormObjectDecoratorDataObject:
test-data-1:
Name: Test 1
MultiFormIsTemporary: 0

View File

@ -1,178 +1,141 @@
<?php
namespace SilverStripe\MultiForm\Tests;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\FunctionalTest;
use SilverStripe\MultiForm\Models\MultiForm;
use SilverStripe\MultiForm\Models\MultiFormSession;
use SilverStripe\MultiForm\Tests\Stubs\MultiFormTestController;
use SilverStripe\MultiForm\Tests\Stubs\MultiFormTestForm;
use SilverStripe\MultiForm\Tests\Stubs\MultiFormTestStepOne;
use SilverStripe\MultiForm\Tests\Stubs\MultiFormTestStepTwo;
/**
* MultiFormTest
* For testing purposes, we have some test classes:
*
* - MultiFormTest_Controller (simulation of a real Controller class)
* - MultiFormTest_Form (subclass of MultiForm)
* - MultiFormTest_StepOne (subclass of MultiFormStep)
* - MultiFormTest_StepTwo (subclass of MultiFormStep)
* - MultiFormTest_StepThree (subclass of MultiFormStep)
* - MultiFormTestController (simulation of a real Controller class)
* - MultiFormTestForm (subclass of MultiForm)
* - MultiFormTestStepOne (subclass of MultiFormStep)
* - MultiFormTestStepTwo (subclass of MultiFormStep)
* - MultiFormTestStepThree (subclass of MultiFormStep)
*
* The above classes are used to simulate real-world behaviour
* of the multiform module - for example, MultiFormTest_Controller
* of the multiform module - for example, MultiFormTestController
* is a simulation of a page where MultiFormTest_Form is a simple
* multi-step contact form it belongs to.
*
* @package multiform
* @subpackage tests
*/
class MultiFormTest extends FunctionalTest {
class MultiFormTest extends FunctionalTest
{
protected static $fixture_file = 'MultiFormTest.yml';
public static $fixture_file = 'multiform/tests/MultiFormTest.yml';
/**
* @var MultiFormTestController
*/
protected $controller;
protected $controller;
/**
* @var MultiFormTestForm
*/
protected $form;
public function setUp() {
parent::setUp();
protected function setUp()
{
parent::setUp();
$this->controller = new MultiFormTest_Controller();
$this->form = $this->controller->Form();
}
$this->controller = new MultiFormTestController();
$this->controller->setRequest(new HTTPRequest('GET', '/'));
$this->controller->getRequest()->setSession(new Session([]));
$this->controller->pushCurrent();
$form = $this->form = $this->controller->Form();
Injector::inst()->registerService($form, MultiForm::class);
$this->form = $form;
}
public function testInitialisingForm() {
$this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0));
$this->assertTrue(is_numeric($this->form->getSession()->ID) && ($this->form->getSession()->ID > 0));
$this->assertEquals('MultiFormTest_StepOne', $this->form->getStartStep());
}
public function testInitialisingForm()
{
$this->assertTrue(is_numeric($this->form->getCurrentStep()->ID) && ($this->form->getCurrentStep()->ID > 0));
$this->assertTrue(
is_numeric($this->form->getMultiFormSession()->ID)
&& ($this->form->getMultiFormSession()->ID > 0)
);
$this->assertEquals(MultiFormTestStepOne::class, $this->form->getStartStep());
}
public function testSessionGeneration() {
$this->assertTrue($this->form->session->ID > 0);
}
public function testSessionGeneration()
{
$this->assertTrue($this->form->getMultiFormSession()->ID > 0);
}
public function testMemberLogging() {
// Grab any user to fake being logged in as, and ensure that after a session is written it has
// that user as the submitter.
$userId = Member::get_one("Member")->ID;
$this->session()->inst_set('loggedInAs', $userId);
public function testMemberLogging()
{
// Grab any user to fake being logged in as, and ensure that after a session is written it has
// that user as the submitter.
$session = $this->form->session;
$session->write();
$userId = $this->logInWithPermission('ADMIN');
$this->assertEquals($userId, $session->SubmitterID);
}
$session = $this->form->getMultiFormSession();
$session->write();
public function testSecondStep() {
$this->assertEquals('MultiFormTest_StepTwo', $this->form->getCurrentStep()->getNextStep());
}
$this->assertEquals($userId, $session->SubmitterID);
}
public function testParentForm() {
$currentStep = $this->form->getCurrentStep();
$this->assertEquals($currentStep->getForm()->class, $this->form->class);
}
public function testSecondStep()
{
$this->assertEquals(MultiFormTestStepTwo::class, $this->form->getCurrentStep()->getNextStep());
}
public function testTotalStepCount() {
$this->assertEquals(3, $this->form->getAllStepsLinear()->Count());
}
public function testParentForm()
{
$currentStep = $this->form->getCurrentStep();
$this->assertEquals($currentStep->getForm()->class, $this->form->class);
}
public function testCompletedSession() {
$this->form->setCurrentSessionHash($this->form->session->Hash);
$this->assertInstanceOf('MultiFormSession', $this->form->getCurrentSession());
$this->form->session->markCompleted();
$this->assertNull($this->form->getCurrentSession());
}
public function testTotalStepCount()
{
$this->assertEquals(3, $this->form->getAllStepsLinear()->Count());
}
public function testIncorrectSessionIdentifier() {
$this->form->setCurrentSessionHash('sdfsdf3432325325sfsdfdf'); // made up!
public function testCompletedSession()
{
$this->form->setCurrentSessionHash($this->form->getMultiFormSession()->Hash);
$this->assertInstanceOf(MultiFormSession::class, $this->form->getCurrentSession());
$this->form->getMultiFormSession()->markCompleted();
$this->assertNull($this->form->getCurrentSession());
}
// A new session is generated, even though we made up the identifier
$this->assertInstanceOf('MultiFormSession', $this->form->session);
}
public function testIncorrectSessionIdentifier()
{
$this->form->setCurrentSessionHash('sdfsdf3432325325sfsdfdf'); // made up!
function testCustomGetVar() {
Config::nest();
Config::inst()->update('MultiForm', 'get_var', 'SuperSessionID');
// A new session is generated, even though we made up the identifier
$this->assertInstanceOf(MultiFormSession::class, $this->form->getMultiFormSession());
}
$form = $this->controller->Form();
$this->assertContains('SuperSessionID', $form::$ignored_fields, "GET var wasn't added to ignored fields");
$this->assertContains('SuperSessionID', $form->FormAction(), "Form action doesn't contain correct session
ID parameter");
$this->assertContains('SuperSessionID', $form->getCurrentStep()->Link(), "Form step doesn't contain correct
session ID parameter");
Config::unnest();
}
}
/**
* @package multiform
* @subpackage tests
*/
class MultiFormTest_Controller extends Controller implements TestOnly {
public function Link() {
return 'MultiFormTest_Controller';
}
public function Form($request = null) {
$form = new MultiFormTest_Form($this, 'Form');
$form->setHTMLID('MultiFormTest_Form');
return $form;
}
}
/**
* @package multiform
* @subpackage tests
*/
class MultiFormTest_Form extends MultiForm implements TestOnly {
public static $start_step = 'MultiFormTest_StepOne';
public function getStartStep() {
return self::$start_step;
}
}
/**
* @package multiform
* @subpackage tests
*/
class MultiFormTest_StepOne extends MultiFormStep implements TestOnly {
public static $next_steps = 'MultiFormTest_StepTwo';
public function getFields() {
$class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet';
return new $class(
new TextField('FirstName', 'First name'),
new TextField('Surname', 'Surname'),
new EmailField('Email', 'Email address')
);
}
}
/**
* @package multiform
* @subpackage tests
*/
class MultiFormTest_StepTwo extends MultiFormStep implements TestOnly {
public static $next_steps = 'MultiFormTest_StepThree';
public function getFields() {
$class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet';
return new $class(
new TextareaField('Comments', 'Tell us a bit about yourself...')
);
}
}
/**
* @package multiform
* @subpackage tests
*/
class MultiFormTest_StepThree extends MultiFormStep implements TestOnly {
public static $is_final_step = true;
public function getFields() {
$class = (class_exists('FieldList')) ? 'FieldList' : 'FieldSet';
return new $class(
new TextField('Test', 'Anything else you\'d like to tell us?')
);
}
public function testCustomGetVar()
{
Config::modify()->set(MultiForm::class, 'get_var', 'SuperSessionID');
$form = $this->controller->Form();
$this->assertContains(
'SuperSessionID',
$form->config()->get('ignored_fields'),
'GET var wasn\'t added to ignored fields'
);
$this->assertContains(
'SuperSessionID',
$form->FormAction(),
"Form action doesn't contain correct session ID parameter"
);
$this->assertContains(
'SuperSessionID',
$form->getCurrentStep()->Link(),
"Form step doesn't contain correct session ID parameter"
);
}
}

View File

@ -1,4 +1,4 @@
Member:
SilverStripe\Security\Member:
admin:
FirstName: Admin
Surname: Admin

View File

@ -0,0 +1,15 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class MultiFormObjectDecoratorDataObject extends DataObject implements TestOnly
{
private static $db = [
'Name' => 'Varchar'
];
private static $table_name = 'MultiFormObjectDecoratorDataObject';
}

View File

@ -0,0 +1,23 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\EmailField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\MultiForm\Models\MultiFormStep;
class MultiFormTestStepOne extends MultiFormStep implements TestOnly
{
private static $next_steps = MultiFormTestStepTwo::class;
public function getFields()
{
return FieldList::create(
new TextField('FirstName', 'First name'),
new TextField('Surname', 'Surname'),
new EmailField('Email', 'Email address')
);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Control\Controller;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\TestOnly;
class MultiFormTestController extends Controller implements TestOnly
{
private static $url_segment = 'MultiFormTestController';
public function Form()
{
return Injector::inst()->get(MultiFormTestForm::class, false, [$this, 'Form'])
->setHTMLID(MultiFormTestForm::class);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\MultiForm\Models\MultiForm;
class MultiFormTestForm extends MultiForm implements TestOnly
{
private static $start_step = MultiFormTestStepOne::class;
public function getStartStep()
{
return $this->config()->get('start_step');
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextField;
use SilverStripe\MultiForm\Models\MultiFormStep;
class MultiFormTestStepThree extends MultiFormStep implements TestOnly
{
private static $is_final_step = true;
public function getFields()
{
return FieldList::create(
new TextField('Test', 'Anything else you\'d like to tell us?')
);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace SilverStripe\MultiForm\Tests\Stubs;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TextareaField;
use SilverStripe\MultiForm\Models\MultiFormStep;
class MultiFormTestStepTwo extends MultiFormStep implements TestOnly
{
private static $next_steps = MultiFormTestStepThree::class;
public function getFields()
{
return new FieldList(
new TextareaField('Comments', 'Tell us a bit about yourself...')
);
}
}