From bbc1cb82702b678b21bef15394f067c146e47625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thierry=20Fran=C3=A7ois?= Date: Sat, 20 Sep 2014 17:09:48 +0300 Subject: [PATCH 01/25] FIX #3458 iframe transport multi file upload FIX #3343, FIX #3148 UploadField now handles multiple file upload through iframe transport correctly (mainly for IE) as well as upload errors on a per file basis. --- filesystem/Upload.php | 9 ++++++++ forms/UploadField.php | 25 ++++++++++++--------- tests/forms/uploadfield/UploadFieldTest.php | 9 ++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/filesystem/Upload.php b/filesystem/Upload.php index 370116ff3..6c2e8a97e 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -270,9 +270,11 @@ class Upload extends Controller { /** * Clear out all errors (mostly set by {loadUploaded()}) + * including the validator's errors */ public function clearErrors() { $this->errors = array(); + $this->validator->clearErrors(); } /** @@ -342,6 +344,13 @@ class Upload_Validator { return $this->errors; } + /** + * Clear out all errors + */ + public function clearErrors() { + $this->errors = array(); + } + /** * Set information about temporary file produced by PHP. * @param array $tmpFile diff --git a/forms/UploadField.php b/forms/UploadField.php index e5b161da4..b09a126af 100644 --- a/forms/UploadField.php +++ b/forms/UploadField.php @@ -1222,20 +1222,25 @@ class UploadField extends FileField { $name = $this->getName(); $postVars = $request->postVar($name); - // Save the temporary file into a File object + // Extract uploaded files from Form data $uploadedFiles = $this->extractUploadedFileData($postVars); - $firstFile = reset($uploadedFiles); - $file = $this->saveTemporaryFile($firstFile, $error); - if(empty($file)) { - $return = array('error' => $error); - } else { - $return = $this->encodeFileAttributes($file); + $return = array(); + + // Save the temporary files into a File objects + // and save data/error on a per file basis + foreach ($uploadedFiles as $tempFile) { + $file = $this->saveTemporaryFile($tempFile, $error); + if(empty($file)) { + array_push($return, array('error' => $error)); + } else { + array_push($return, $this->encodeFileAttributes($file)); + } + $this->upload->clearErrors(); } - + // Format response with json - $response = new SS_HTTPResponse(Convert::raw2json(array($return))); + $response = new SS_HTTPResponse(Convert::raw2json($return)); $response->addHeader('Content-Type', 'text/plain'); - if (!empty($return['error'])) $response->setStatusCode(403); return $response; } diff --git a/tests/forms/uploadfield/UploadFieldTest.php b/tests/forms/uploadfield/UploadFieldTest.php index 89dac5e0c..666c6bd76 100644 --- a/tests/forms/uploadfield/UploadFieldTest.php +++ b/tests/forms/uploadfield/UploadFieldTest.php @@ -167,8 +167,9 @@ class UploadFieldTest extends FunctionalTest { 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', array('AllowedExtensionsField' => $this->getUploadFile($invalidFile)) ); - $this->assertTrue($response->isError()); - $this->assertContains('Extension is not allowed', $response->getBody()); + $response = json_decode($response->getBody(), true); + $this->assertTrue(array_key_exists('error', $response[0])); + $this->assertContains('Extension is not allowed', $response[0]['error']); $validFile = 'valid.txt'; $_FILES = array('AllowedExtensionsField' => $this->getUploadFile($validFile)); @@ -176,8 +177,8 @@ class UploadFieldTest extends FunctionalTest { 'UploadFieldTest_Controller/Form/field/AllowedExtensionsField/upload', array('AllowedExtensionsField' => $this->getUploadFile($validFile)) ); - $this->assertFalse($response->isError()); - $this->assertNotContains('Extension is not allowed', $response->getBody()); + $response = json_decode($response->getBody(), true); + $this->assertFalse(array_key_exists('error', $response[0])); } /** From fe08447fc0e6a65d639e7660c44f4eb92c4e0c50 Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 27 Apr 2015 14:10:42 +1200 Subject: [PATCH 02/25] Clean up PasswordField --- forms/PasswordField.php | 50 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/forms/PasswordField.php b/forms/PasswordField.php index 21ac2e212..c230ee9bf 100644 --- a/forms/PasswordField.php +++ b/forms/PasswordField.php @@ -1,11 +1,12 @@ 3) { - Deprecation::notice('3.0', 'Use setMaxLength() instead of constructor arguments', - Deprecation::SCOPE_GLOBAL); + Deprecation::notice( + '3.0', 'Use setMaxLength() instead of constructor arguments', + Deprecation::SCOPE_GLOBAL + ); } parent::__construct($name, $title, $value); } - + /** + * {@inheritdoc} + */ public function getAttributes() { - $attributes = array_merge( - parent::getAttributes(), - array('type' => 'password') + $attributes = array( + 'type' => 'password', ); $autocomplete = Config::inst()->get('PasswordField', 'autocomplete'); - if (isset($autocomplete)) { - $attributes['autocomplete'] = $autocomplete ? 'on' : 'off'; + + if($autocomplete) { + $attributes['autocomplete'] = 'on'; + } else { + $attributes['autocomplete'] = 'off'; } - return $attributes; + return array_merge( + parent::getAttributes(), + $attributes + ); } /** - * Makes a pretty readonly field with some stars in it + * Creates a read-only version of the field. + * + * @return FormField */ public function performReadonlyTransformation() { $field = $this->castedCopy('ReadonlyField'); + $field->setValue('*****'); - + return $field; } + /** + * {@inheritdoc} + */ public function Type() { return 'text password'; } } - From c59eb1bb704cfbb4ded44ad829c7f0e9d7281f6d Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 27 Apr 2015 15:08:56 +1200 Subject: [PATCH 03/25] Clean Up ToggleCompositeField --- forms/ToggleCompositeField.php | 64 +++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/forms/ToggleCompositeField.php b/forms/ToggleCompositeField.php index 8231c2d62..225c222dd 100644 --- a/forms/ToggleCompositeField.php +++ b/forms/ToggleCompositeField.php @@ -1,4 +1,5 @@ name = $name; $this->title = $title; @@ -24,30 +29,46 @@ class ToggleCompositeField extends CompositeField { parent::__construct($children); } + /** + * @param array $properties + * + * @return HTMLText + */ public function FieldHolder($properties = array()) { Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery/jquery.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-ui/jquery-ui.js'); Requirements::javascript(FRAMEWORK_DIR . '/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js'); Requirements::javascript(FRAMEWORK_DIR . '/javascript/ToggleCompositeField.js'); + Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery.ui.css'); - $obj = $properties ? $this->customise($properties) : $this; - return $obj->renderWith($this->getTemplates()); + $context = $this; + + if(count($properties)) { + $context = $this->customise($properties); + } + + return $context->renderWith($this->getTemplates()); } + /** + * {@inheritdoc} + */ public function getAttributes() { + $attributes = array( + 'id' => $this->id(), + 'class' => $this->extraClass(), + ); + if($this->getStartClosed()) { - $class = 'ss-toggle ss-toggle-start-closed'; + $attributes['class'] .= ' ss-toggle ss-toggle-start-closed'; } else { - $class = 'ss-toggle'; + $attributes['class'] .= ' ss-toggle'; } return array_merge( $this->attributes, - array( - 'id' => $this->id(), - 'class' => $class . ' ' . $this->extraClass() - ) + $attributes ); } @@ -59,13 +80,15 @@ class ToggleCompositeField extends CompositeField { } /** - * Controls whether the field is open or closed by default. By default the - * field is closed. + * Controls whether the field is open or closed by default. By default the field is closed. * - * @param bool $bool + * @param bool $startClosed + * + * @return $this */ - public function setStartClosed($bool) { - $this->startClosed = (bool) $bool; + public function setStartClosed($startClosed) { + $this->startClosed = (bool) $startClosed; + return $this; } @@ -77,12 +100,13 @@ class ToggleCompositeField extends CompositeField { } /** - * @param int $level + * @param int $headingLevel + * + * @return $this */ - public function setHeadingLevel($level) { - $this->headingLevel = $level; + public function setHeadingLevel($headingLevel) { + $this->headingLevel = $headingLevel; + return $this; } - } - From 53cf2929d492ce7c7f3a8b4034c6161a950a36a4 Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 27 Apr 2015 15:15:32 +1200 Subject: [PATCH 04/25] Clean up TextareaField --- forms/TextareaField.php | 79 ++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/forms/TextareaField.php b/forms/TextareaField.php index fe22d4cdb..f907d21fc 100644 --- a/forms/TextareaField.php +++ b/forms/TextareaField.php @@ -1,12 +1,11 @@ tag in the * form HTML. - * - * Usage - * + * * * new TextareaField( * $name = "description", @@ -14,22 +13,54 @@ * $value = "This is the default description" * ); * - * + * * @package forms * @subpackage fields-basic */ class TextareaField extends FormField { - /** - * @var int Visible number of text lines. + * Visible number of text lines. + * + * @var int */ protected $rows = 5; /** - * @var int Width of the text area (in average character widths) + * Visible number of text columns. + * + * @var int */ protected $cols = 20; + /** + * Set the number of rows in the textarea + * + * @param int $rows + * + * @return $this + */ + public function setRows($rows) { + $this->rows = $rows; + + return $this; + } + + /** + * Set the number of columns in the textarea + * + * @param int $cols + * + * @return $this + */ + public function setColumns($cols) { + $this->cols = $cols; + + return $this; + } + + /** + * {@inheritdoc} + */ public function getAttributes() { return array_merge( parent::getAttributes(), @@ -42,30 +73,22 @@ class TextareaField extends FormField { ); } + /** + * {@inheritdoc} + */ public function Type() { - return parent::Type() . ($this->readonly ? ' readonly' : ''); - } - - /** - * Set the number of rows in the textarea - * - * @param int - */ - public function setRows($rows) { - $this->rows = $rows; - return $this; - } - - /** - * Set the number of columns in the textarea - * - * @return int - */ - public function setColumns($cols) { - $this->cols = $cols; - return $this; + $parent = parent::Type(); + + if($this->readonly) { + return $parent . ' readonly'; + } + + return $parent; } + /** + * @return string + */ public function Value() { return htmlentities($this->value, ENT_COMPAT, 'UTF-8'); } From 7963a0b870770ccc686bebda3af6aa80b2c691ee Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 27 Apr 2015 15:38:34 +1200 Subject: [PATCH 05/25] Clean up EmailField --- forms/EmailField.php | 47 ++++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/forms/EmailField.php b/forms/EmailField.php index a6920f381..b1593bcb2 100644 --- a/forms/EmailField.php +++ b/forms/EmailField.php @@ -1,54 +1,59 @@ 'email' + 'type' => 'email', ) ); } /** - * Validates for RFC 2822 compliant email adresses. - * + * Validates for RFC 2822 compliant email addresses. + * * @see http://www.regular-expressions.info/email.html * @see http://www.ietf.org/rfc/rfc2822.txt - * + * * @param Validator $validator - * @return String + * + * @return string */ public function validate($validator) { $this->value = trim($this->value); - $pcrePattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*' - . '@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'; + $pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'; - // PHP uses forward slash (/) to delimit start/end of pattern, so it must be escaped - $pregSafePattern = str_replace('/', '\\/', $pcrePattern); + // Escape delimiter characters. + $safePattern = str_replace('/', '\\/', $pattern); - if($this->value && !preg_match('/' . $pregSafePattern . '/i', $this->value)){ + if($this->value && !preg_match('/' . $safePattern . '/i', $this->value)) { $validator->validationError( $this->name, - _t('EmailField.VALIDATION', "Please enter an email address"), - "validation" + _t('EmailField.VALIDATION', 'Please enter an email address'), + 'validation' ); - return false; - } else{ - return true; - } - } + return false; + } + + return true; + } } From 4c6ec7afd97f329d857e37453793880992bed74f Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Mon, 27 Apr 2015 16:00:53 +1200 Subject: [PATCH 06/25] Clean up LiteralField --- forms/LiteralField.php | 75 ++++++++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/forms/LiteralField.php b/forms/LiteralField.php index c6fc69876..c2b576329 100644 --- a/forms/LiteralField.php +++ b/forms/LiteralField.php @@ -1,76 +1,103 @@ Usage - * + * * * new LiteralField ( * $name = "literalfield", * $content = 'some bold text and a link' * ) * - * + * * @package forms * @subpackage fields-dataless */ class LiteralField extends DatalessField { - /** - * @var string $content + * @var string|FormField */ protected $content; - + + /** + * @param string $name + * @param string|FormField $content + */ public function __construct($name, $content) { - $this->content = $content; - + $this->setContent($content); + parent::__construct($name); } - + + /** + * @param array $properties + * + * @return string + */ public function FieldHolder($properties = array()) { - if(is_object($this->content)) { - $obj = $this->content; - if($properties) - $obj = $obj->customise($properties); - return $obj->forTemplate(); - } else { - return $this->content; + if($this->content instanceof ViewableData) { + $context = $this->content; + + if($properties) { + $context = $context->customise($properties); + } + + return $context->forTemplate(); } + + return $this->content; } + /** + * @param array $properties + * + * @return string + */ public function Field($properties = array()) { return $this->FieldHolder($properties); } /** - * Sets the content of this field to a new value + * Sets the content of this field to a new value. * - * @param string $content + * @param string|FormField $content + * + * @return $this */ public function setContent($content) { $this->content = $content; + return $this; } - + /** * @return string */ public function getContent() { return $this->content; } - + /** * Synonym of {@link setContent()} so that LiteralField is more compatible with other field types. + * + * @param string|FormField $content + * + * @return $this */ - public function setValue($value) { - $this->setContent($value); + public function setValue($content) { + $this->setContent($content); + return $this; } + /** + * @return static + */ public function performReadonlyTransformation() { $clone = clone $this; + $clone->setReadonly(true); + return $clone; } - } From 05c2d04b421d51234f103f3c53aecf13eaf16cf8 Mon Sep 17 00:00:00 2001 From: Christopher Pitt Date: Tue, 28 Apr 2015 09:05:44 +1200 Subject: [PATCH 07/25] Clean up HeaderField --- forms/HeaderField.php | 69 +++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/forms/HeaderField.php b/forms/HeaderField.php index 971a498d3..4bea26157 100644 --- a/forms/HeaderField.php +++ b/forms/HeaderField.php @@ -1,4 +1,5 @@ to
HTML tag. Default: 2 + * The level of the

to

HTML tag. + * + * @var int */ protected $headingLevel = 2; - + + /** + * @param string $name + * @param null|string $title + * @param int $headingLevel + */ public function __construct($name, $title = null, $headingLevel = 2) { - // legacy handling for old parameters: $title, $heading, ... - // instead of new handling: $name, $title, $heading, ... + // legacy handling: + // $title, $headingLevel... $args = func_get_args(); + if(!isset($args[1]) || is_numeric($args[1])) { - $title = (isset($args[0])) ? $args[0] : null; - // Use "HeaderField(title)" as the default field name for a HeaderField; if it's just set to title then we - // risk causing accidental duplicate-field creation. - // this means i18nized fields won't be easily accessible through fieldByName() + if(isset($args[0])) { + $title = $args[0]; + } + + // Prefix name to avoid collisions. $name = 'HeaderField' . $title; - $headingLevel = (isset($args[1])) ? $args[1] : null; - $form = (isset($args[3])) ? $args[3] : null; - } - - if($headingLevel) $this->headingLevel = $headingLevel; - + + if(isset($args[1])) { + $headingLevel = $args[1]; + } + } + + $this->setHeadingLevel($headingLevel); + parent::__construct($name, $title); } + /** + * @return int + */ public function getHeadingLevel() { return $this->headingLevel; } - - public function setHeadingLevel($level) { - $this->headingLevel = $level; + + /** + * @param int $headingLevel + * + * @return $this + */ + public function setHeadingLevel($headingLevel) { + $this->headingLevel = $headingLevel; + return $this; } + /** + * {@inheritdoc} + */ public function getAttributes() { return array_merge( + parent::getAttributes(), array( 'id' => $this->ID(), - 'class' => $this->extraClass() - ), - $this->attributes + 'class' => $this->extraClass(), + ) ); } + /** + * @return null + */ public function Type() { return null; } - } From 8b4a9a3988151f3f311e1f16fa91a6dab2ca65d8 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Thu, 7 May 2015 16:01:24 +0100 Subject: [PATCH 08/25] Store current page before marking tree nodes (fixes #4137, fixes silverstripe/silverstripe-cms#1135) --- admin/code/LeftAndMain.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 268229dd0..82c330755 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -811,6 +811,12 @@ class LeftAndMain extends Controller implements PermissionProvider { // Get the tree root $record = ($rootID) ? $this->getRecord($rootID) : null; $obj = $record ? $record : singleton($className); + + // Get the current page + // NOTE: This *must* be fetched before markPartialTree() is called, as this + // causes the Hierarchy::$marked cache to be flushed (@see CMSMain::getRecord) + // which means that deleted pages stored in the marked tree would be removed + $currentPage = $this->currentPage(); // Mark the nodes of the tree to return if ($filterFunction) $obj->setMarkingFilterFunction($filterFunction); @@ -818,10 +824,7 @@ class LeftAndMain extends Controller implements PermissionProvider { $obj->markPartialTree($nodeCountThreshold, $this, $childrenMethod, $numChildrenMethod); // Ensure current page is exposed - // This call flushes the Hierarchy::$marked cache when the current node is deleted - // @see CMSMain::getRecord() - // This will make it impossible to show children under a deleted parent page - // if($p = $this->currentPage()) $obj->markToExpose($p); + if($currentPage) $obj->markToExpose($currentPage); // NOTE: SiteTree/CMSMain coupling :-( if(class_exists('SiteTree')) { From 07b9cdc8bbee981a36de20cb7d6c1c9c29eb96e8 Mon Sep 17 00:00:00 2001 From: Matt Thomson Date: Tue, 19 May 2015 15:48:56 +1200 Subject: [PATCH 09/25] DOCS: Add links to lessons 10-16 to the tutorials page --- docs/en/01_Tutorials/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/01_Tutorials/index.md b/docs/en/01_Tutorials/index.md index de5afce94..54bb385ff 100644 --- a/docs/en/01_Tutorials/index.md +++ b/docs/en/01_Tutorials/index.md @@ -20,6 +20,12 @@ These include video screencasts, written tutorials and code examples to get you * [Lesson 8: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/introduction-to-the-orm) * [Lesson 9: Data Relationships - $has_many](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-has-many) * [Lesson 10: Introduction to the ORM](http://www.silverstripe.org/learn/lessons/working-with-data-relationships-many-many) +* [Lesson 11: Introduction to frontend forms](http://www.silverstripe.org/learn/lessons/introduction-to-frontend-forms) +* [Lesson 12: Data Extensions and SiteConfig](http://www.silverstripe.org/learn/lessons/data-extensions-and-siteconfig) +* [Lesson 13: Introduction to ModelAdmin](http://www.silverstripe.org/learn/lessons/introduction-to-modeladmin) +* [Lesson 14: Controller Actions/DataObjects as Pages](http://www.silverstripe.org/learn/lessons/controller-actions-dataobjects-as-pages) +* [Lesson 15: Building a Search Form](http://www.silverstripe.org/learn/lessons/building-a-search-form) +* [Lesson 16: Lists and Pagination](http://www.silverstripe.org/learn/lessons/lists-and-pagination) ## Help: If you get stuck From 5a323f7fe3b8683f2e15df9c4fc8b060a46dd979 Mon Sep 17 00:00:00 2001 From: David Alexander Date: Tue, 9 Jun 2015 16:53:20 +1200 Subject: [PATCH 10/25] Update 03_Environment_Management.md --- .../03_Environment_Management.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/en/00_Getting_Started/03_Environment_Management.md b/docs/en/00_Getting_Started/03_Environment_Management.md index 5b6f2e943..b3f50ad7e 100644 --- a/docs/en/00_Getting_Started/03_Environment_Management.md +++ b/docs/en/00_Getting_Started/03_Environment_Management.md @@ -105,21 +105,21 @@ This is my `_ss_environment.php` file. I have it placed in `/var`, as each of th | Name | Description | | ---- | ----------- | -| `TEMP_FOLDER` | Absolute file path to store temporary files such as cached templates or the class manifest. Needs to be writeable by the webserver user. Defaults to *silverstripe-cache* in the webroot, and falls back to *sys_get_temp_dir()*. See *getTempFolder()* in *framework/core/TempPath.php* | -| `SS_DATABASE_CLASS` | The database class to use, MySQLDatabase, MSSQLDatabase, etc. defaults to MySQLDatabase| -| `SS_DATABASE_SERVER`| The database server to use, defaulting to localhost| -| `SS_DATABASE_USERNAME`| The database username (mandatory)| -| `SS_DATABASE_PASSWORD`| The database password (mandatory)| -| `SS_DATABASE_PORT`| The database port| +| `TEMP_FOLDER` | Absolute file path to store temporary files such as cached templates or the class manifest. Needs to be writeable by the webserver user. Defaults to *silverstripe-cache* in the webroot, and falls back to *sys_get_temp_dir()*. See *getTempFolder()* in *framework/core/TempPath.php*.| +| `SS_DATABASE_CLASS` | The database class to use, MySQLDatabase, MSSQLDatabase, etc. defaults to MySQLDatabase.| +| `SS_DATABASE_SERVER`| The database server to use, defaulting to localhost.| +| `SS_DATABASE_USERNAME`| The database username (mandatory).| +| `SS_DATABASE_PASSWORD`| The database password (mandatory).| +| `SS_DATABASE_PORT`| The database port.| | `SS_DATABASE_SUFFIX`| A suffix to add to the database name.| | `SS_DATABASE_PREFIX`| A prefix to add to the database name.| | `SS_DATABASE_TIMEZONE`| Set the database timezone to something other than the system timezone. | `SS_DATABASE_NAME` | Set the database name. Assumes the `$database` global variable in your config is missing or empty. | -| `SS_DATABASE_CHOOSE_NAME`| Boolean/Int. If set, then the system will choose a default database name for you if one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of the box without the installer needing to alter any files. This helps prevent accidental changes to the environment. If `SS_DATABASE_CHOOSE_NAME` is an integer greater than one, then an ancestor folder will be used for the database name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.| +| `SS_DATABASE_CHOOSE_NAME`| Boolean/Int. If defined, then the system will choose a default database name for you if one isn't give in the $database variable. The database name will be "SS_" followed by the name of the folder into which you have installed SilverStripe. If this is enabled, it means that the phpinstaller will work out of the box without the installer needing to alter any files. This helps prevent accidental changes to the environment. If `SS_DATABASE_CHOOSE_NAME` is an integer greater than one, then an ancestor folder will be used for the database name. This is handy for a site that's hosted from /sites/examplesite/www or /buildbot/allmodules-2.3/build. If it's 2, the parent folder will be chosen; if it's 3 the grandparent, and so on.| | `SS_ENVIRONMENT_TYPE`| The environment type: dev, test or live.| | `SS_DEFAULT_ADMIN_USERNAME`| The username of the default admin. This is a user with administrative privileges.| | `SS_DEFAULT_ADMIN_PASSWORD`| The password of the default admin. This will not be stored in the database.| | `SS_USE_BASIC_AUTH`| Protect the site with basic auth (good for test sites).
When using CGI/FastCGI with Apache, you will have to add the `RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]` rewrite rule to your `.htaccess` file| -| `SS_SEND_ALL_EMAILS_TO`| If you set this define, all emails will be redirected to this address.| -| `SS_SEND_ALL_EMAILS_FROM`| If you set this define, all emails will be send from this address.| -| `SS_ERROR_LOG` | Relative path to the log file | +| `SS_SEND_ALL_EMAILS_TO`| If you define this constant, all emails will be redirected to this address.| +| `SS_SEND_ALL_EMAILS_FROM`| If you define this constant, all emails will be sent from this address.| +| `SS_ERROR_LOG` | Relative path to the log file. | From c062670ba320134d57174bf24870c37df271ee53 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 9 Jun 2015 16:01:33 +0100 Subject: [PATCH 11/25] Removing unreachable test line --- tests/view/SSViewerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index 550eaa046..c8ce9f2f3 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -1093,7 +1093,6 @@ after') // Let's throw something random in there. $self->setExpectedException('InvalidArgumentException'); $templates = SSViewer::get_templates_by_class(array()); - $this->assertCount(0, $templates); }); } From 168955135b22f1650680b90bcee4b1a13f06e081 Mon Sep 17 00:00:00 2001 From: Nick Date: Wed, 10 Jun 2015 13:49:52 +1200 Subject: [PATCH 12/25] Fix the order of params The parameters for the strpos and substr functions were wrong. strpos always failed so never triggered the substr. This would mean $fieldClass would be left with 'Varchar(255)' Fixing this had no speed or memory effect. --- model/DataObject.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/model/DataObject.php b/model/DataObject.php index 88132c894..7f14288f8 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -358,8 +358,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!is_string($fieldClass)) continue; // Strip off any parameters - $bPos = strpos('(', $fieldClass); - if($bPos !== FALSE) $fieldClass = substr(0,$bPos, $fieldClass); + $bPos = strpos($fieldClass, '('); + if($bPos !== FALSE) $fieldClass = substr($fieldClass, 0, $bPos); // Test to see if it implements CompositeDBField if(ClassInfo::classImplements($fieldClass, 'CompositeDBField')) { From 67e1cee64a28f5dcffc24e5327b95b3aa27e0bb2 Mon Sep 17 00:00:00 2001 From: Myles Beardsmore Date: Wed, 10 Jun 2015 16:56:31 +1200 Subject: [PATCH 13/25] Completed missing comment ending Completed missing comment ending --- .../06_Testing/How_Tos/00_Write_a_SapphireTest.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md index ed8d69764..9bae22450 100644 --- a/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md +++ b/docs/en/02_Developer_Guides/06_Testing/How_Tos/00_Write_a_SapphireTest.md @@ -15,7 +15,7 @@ how you can load default records into the test database. /** * Defines the fixture file to use for this test class * - / + */ protected static $fixture_file = 'SiteTreeTest.yml'; /** @@ -79,4 +79,4 @@ For more information on PHPUnit's assertions see the [PHPUnit manual](http://www ## API Documentation * [api:SapphireTest] -* [api:FunctionalTest] \ No newline at end of file +* [api:FunctionalTest] From 6be04887315522e5b95b83be1e301691441b985c Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 11 Jun 2015 16:07:52 +0100 Subject: [PATCH 14/25] FIX TreeDropdownField doesnt change label on unselect --- forms/TreeDropdownField.php | 16 +++++++++++----- javascript/TreeDropdownField.js | 8 ++++++-- templates/forms/TreeDropdownField.ss | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/forms/TreeDropdownField.php b/forms/TreeDropdownField.php index 935446ac9..aca909f12 100644 --- a/forms/TreeDropdownField.php +++ b/forms/TreeDropdownField.php @@ -215,16 +215,21 @@ class TreeDropdownField extends FormField { Requirements::css(FRAMEWORK_DIR . '/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css'); Requirements::css(FRAMEWORK_DIR . '/css/TreeDropdownField.css'); - + + if($this->showSearch) { + $emptyTitle = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown'); + } else { + $emptyTitle = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown'); + } + $record = $this->Value() ? $this->objectForKey($this->Value()) : null; if($record instanceof ViewableData) { $title = $record->obj($this->labelField)->forTemplate(); } elseif($record) { $title = Convert::raw2xml($record->{$this->labelField}); - } else if($this->showSearch) { - $title = _t('DropdownField.CHOOSESEARCH', '(Choose or Search)', 'start value of a dropdown'); - } else { - $title = _t('DropdownField.CHOOSE', '(Choose)', 'start value of a dropdown'); + } + else { + $title = $emptyTitle; } // TODO Implement for TreeMultiSelectField @@ -237,6 +242,7 @@ class TreeDropdownField extends FormField { $properties, array( 'Title' => $title, + 'EmptyTitle' => $emptyTitle, 'Metadata' => ($metadata) ? Convert::raw2json($metadata) : null, ) ); diff --git a/javascript/TreeDropdownField.js b/javascript/TreeDropdownField.js index 448b95365..eb5887565 100644 --- a/javascript/TreeDropdownField.js +++ b/javascript/TreeDropdownField.js @@ -154,17 +154,21 @@ var updateFn = function() { var val = self.getValue(); if(val) { - + var node = tree.find('*[data-id="' + val + '"]'), title = node.children('a').find("span.jstree_pageicon")?node.children('a').find("span.item").html():null; if(!title) title=(node.length > 0) ? tree.jstree('get_text', node[0]) : null; - + if(title) { self.setTitle(title); self.data('title', title); } if(node) tree.jstree('select_node', node); } + else { + self.setTitle(self.data('empty-title')); + self.removeData('title'); + } }; // Load the tree if its not already present diff --git a/templates/forms/TreeDropdownField.ss b/templates/forms/TreeDropdownField.ss index e5d2345b2..49cc422cc 100644 --- a/templates/forms/TreeDropdownField.ss +++ b/templates/forms/TreeDropdownField.ss @@ -2,6 +2,7 @@ class="TreeDropdownField <% if $extraClass %> $extraClass<% end_if %><% if $ShowSearch %> searchable<% end_if %>" data-url-tree="$Link('tree')" data-title="$Title.ATT" + data-empty-title="$EmptyTitle.ATT" <% if $Description %>title="$Description.ATT"<% end_if %> <% if $Metadata %>data-metadata="$Metadata.ATT"<% end_if %>> From 95860e92294701e611691ccf9e0b08b8cdb47d3b Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Fri, 12 Jun 2015 10:28:32 +1200 Subject: [PATCH 15/25] Update and fix PHP docblocks, document spelling mistakes and strip trailing whitespace on Form.php --- forms/Form.php | 458 ++++++++++++++++++++++++++++++------------------- 1 file changed, 283 insertions(+), 175 deletions(-) diff --git a/forms/Form.php b/forms/Form.php index 0826d2f17..6699dfa56 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -11,13 +11,13 @@ * can be passed using the URL or get variables. These restrictions are in place so that we can * recreate the form object upon form submission, without the use of a session, which would be too * resource-intensive. - * + * * You will need to create at least one method for processing the submission (through {@link FormAction}). * This method will be passed two parameters: the raw request data, and the form object. * Usually you want to save data into a {@link DataObject} by using {@link saveInto()}. * If you want to process the submitted data in any way, please use {@link getData()} rather than * the raw request data. - * + * *

Validation

* Each form needs some form of {@link Validator} to trigger the {@link FormField->validate()} methods for each field. * You can't disable validator for security reasons, because crucial behaviour like extension checks for file uploads @@ -33,9 +33,9 @@ * You can find out the base URL for your form by looking at the *
value. For example, the edit form in the CMS would be located at * "admin/EditForm". This URL will render the form without its surrounding - * template when called through GET instead of POST. - * - * By appending to this URL, you can render invidual form elements + * template when called through GET instead of POST. + * + * By appending to this URL, you can render individual form elements * through the {@link FormField->FieldHolder()} method. * For example, the "URLSegment" field in a standard CMS form would be * accessible through "admin/EditForm/field/URLSegment/FieldHolder". @@ -53,37 +53,57 @@ class Form extends RequestHandler { * A performance enhancement over the generate-the-form-tag-and-then-remove-it code that was there previously */ public $IncludeFormTag = true; - + + /** + * @var FieldList + */ protected $fields; - + + /** + * @var FieldList + */ protected $actions; /** * @var Controller */ protected $controller; - + + /** + * @var string + */ protected $name; + /** + * @var Validator + */ protected $validator; - + + /** + * @var string + */ protected $formMethod = "post"; /** * @var boolean */ protected $strictFormMethodCheck = false; - - protected static $current_action; - + /** - * @var Dataobject $record Populated by {@link loadDataFrom()}. + * @var string + */ + protected static $current_action; + + /** + * @var DataObject $record Populated by {@link loadDataFrom()}. */ protected $record; /** * Keeps track of whether this form has a default action or not. * Set to false by $this->disableDefaultAction(); + * + * @var boolean */ protected $hasDefaultAction = true; @@ -95,47 +115,59 @@ class Form extends RequestHandler { * @var string */ protected $target; - + /** - * Legend value, to be inserted into the + * Legend value, to be inserted into the * element before the
* in Form.ss template. * * @var string */ protected $legend; - + /** * The SS template to render this form HTML into. * Default is "Form", but this can be changed to * another template for customisation. - * + * * @see Form->setTemplate() * @var string */ protected $template; - - protected $buttonClickedFunc; - - protected $message; - - protected $messageType; - + /** - * Should we redirect the user back down to the + * @var callable + */ + protected $buttonClickedFunc; + + /** + * @var string + */ + protected $message; + + /** + * @var string + */ + protected $messageType; + + /** + * Should we redirect the user back down to the * the form on validation errors rather then just the page - * + * * @var bool */ protected $redirectToFormOnValidationError = false; - + + /** + * @var bool + */ protected $security = true; - + /** * @var SecurityToken */ protected $securityToken = null; - + /** * @var array $extraClasses List of additional CSS classes for the form tag. */ @@ -152,17 +184,25 @@ class Form extends RequestHandler { */ protected $attributes = array(); + /** + * @var array + */ private static $allowed_actions = array( - 'handleField', + 'handleField', 'httpSubmission', 'forTemplate', ); + /** + * @var bool + */ + protected $securityTokenAdded; + /** * Create a new form, with the given fields an action buttons. - * + * * @param Controller $controller The parent controller, necessary to create the appropriate form action tag. - * @param String $name The method on the controller that will return this form object. + * @param string $name The method on the controller that will return this form object. * @param FieldList $fields All of the fields in the form - a {@link FieldList} of {@link FormField} objects. * @param FieldList $actions All of the action buttons in the form - a {@link FieldLis} of * {@link FormAction} objects @@ -170,7 +210,7 @@ class Form extends RequestHandler { */ public function __construct($controller, $name, FieldList $fields, FieldList $actions, $validator = null) { parent::__construct(); - + if(!$fields instanceof FieldList) { throw new InvalidArgumentException('$fields must be a valid FieldList instance'); } @@ -188,7 +228,7 @@ class Form extends RequestHandler { $this->actions = $actions; $this->controller = $controller; $this->name = $name; - + if(!$this->controller) user_error("$this->class form created without a controller", E_USER_ERROR); // Form validation @@ -197,7 +237,7 @@ class Form extends RequestHandler { // Form error controls $this->setupFormErrors(); - + // Check if CSRF protection is enabled, either on the parent controller or from the default setting. Note that // method_exists() is used as some controllers (e.g. GroupTest) do not always extend from Object. if(method_exists($controller, 'securityTokenEnabled') || (method_exists($controller, 'hasMethod') @@ -207,10 +247,13 @@ class Form extends RequestHandler { } else { $securityEnabled = SecurityToken::is_enabled(); } - + $this->securityToken = ($securityEnabled) ? new SecurityToken() : new NullSecurityToken(); } - + + /** + * @var array + */ private static $url_handlers = array( 'field/$FieldName!' => 'handleField', 'POST ' => 'httpSubmission', @@ -221,6 +264,8 @@ class Form extends RequestHandler { /** * Set up current form errors in session to * the current form if appropriate. + * + * @return $this */ public function setupFormErrors() { $errorInfo = Session::get("FormInfo.{$this->FormName()}"); @@ -247,17 +292,20 @@ class Form extends RequestHandler { return $this; } - + /** * Handle a form submission. GET and POST requests behave identically. * Populates the form with {@link loadDataFrom()}, calls {@link validate()}, * and only triggers the requested form action/method * if the form is valid. + * + * @param SS_HTTPRequest $request + * @throws SS_HTTPResponse_Exception */ public function httpSubmission($request) { // Strict method check if($this->strictFormMethodCheck) { - + // Throws an error if the method is bad... if($this->formMethod != strtolower($request->httpMethod())) { $response = Controller::curr()->getResponse(); @@ -265,15 +313,15 @@ class Form extends RequestHandler { $this->httpError(405, _t("Form.BAD_METHOD", "This form requires a ".$this->formMethod." submission")); } - // ...and only uses the vairables corresponding to that method type + // ...and only uses the variables corresponding to that method type $vars = $this->formMethod == 'get' ? $request->getVars() : $request->postVars(); } else { $vars = $request->requestVars(); } - + // Populate the form $this->loadDataFrom($vars, true); - + // Protection against CSRF attacks $token = $this->getSecurityToken(); if( ! $token->checkRequest($request)) { @@ -296,7 +344,7 @@ class Form extends RequestHandler { return $this->controller->redirectBack(); } } - + // Determine the action button clicked $funcName = null; foreach($vars as $paramName => $paramVal) { @@ -308,18 +356,18 @@ class Form extends RequestHandler { parse_str($paramVars, $newRequestParams); $vars = array_merge((array)$vars, (array)$newRequestParams); } - + // Cleanup action_, _x and _y from image fields $funcName = preg_replace(array('/^action_/','/_x$|_y$/'),'',$paramName); break; } } - - // If the action wasnt' set, choose the default on the form. + + // If the action wasn't set, choose the default on the form. if(!isset($funcName) && $defaultAction = $this->defaultAction()){ $funcName = $defaultAction->actionName(); } - + if(isset($funcName)) { Form::set_current_action($funcName); $this->setButtonClicked($funcName); @@ -335,7 +383,7 @@ class Form extends RequestHandler { && !$this->actions->dataFieldByName('action_' . $funcName) ) { return $this->httpError( - 403, + 403, sprintf('Action "%s" not allowed on controller (Class: %s)', $funcName, get_class($this->controller)) ); } elseif( @@ -345,7 +393,7 @@ class Form extends RequestHandler { // all form methods are callable (e.g. the legacy "callfieldmethod()") ) { return $this->httpError( - 403, + 403, sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->name) ); } @@ -362,17 +410,17 @@ class Form extends RequestHandler { } if (!$fieldsHaveMethod) { return $this->httpError( - 403, + 403, sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name()) ); } }*/ - + // Validate the form if(!$this->validate()) { return $this->getValidationErrorResponse(); } - + // First, try a handler method on the controller (has been checked for allowed_actions above already) if($this->controller->hasMethod($funcName)) { return $this->controller->$funcName($vars, $this, $request); @@ -382,10 +430,14 @@ class Form extends RequestHandler { } elseif($field = $this->checkFieldsForAction($this->Fields(), $funcName)) { return $field->$funcName($vars, $this, $request); } - + return $this->httpError(404); } + /** + * @param string $action + * @return bool + */ public function checkAccessAction($action) { return ( parent::checkAccessAction($action) @@ -403,15 +455,15 @@ class Form extends RequestHandler { * Returns the appropriate response up the controller chain * if {@link validate()} fails (which is checked prior to executing any form actions). * By default, returns different views for ajax/non-ajax request, and - * handles 'appliction/json' requests with a JSON object containing the error messages. + * handles 'application/json' requests with a JSON object containing the error messages. * Behaviour can be influenced by setting {@link $redirectToFormOnValidationError}. - * + * * @return SS_HTTPResponse|string */ protected function getValidationErrorResponse() { $request = $this->getRequest(); if($request->isAjax()) { - // Special case for legacy Validator.js implementation + // Special case for legacy Validator.js implementation // (assumes eval'ed javascript collected through FormResponse) $acceptType = $request->getHeader('Accept'); if(strpos($acceptType, 'application/json') !== FALSE) { @@ -424,7 +476,7 @@ class Form extends RequestHandler { $response = new SS_HTTPResponse($this->forTemplate()); $response->addHeader('Content-Type', 'text/html'); } - + return $response; } else { if($this->getRedirectToFormOnValidationError()) { @@ -440,10 +492,12 @@ class Form extends RequestHandler { return $this->controller->redirectBack(); } } - + /** * Fields can have action to, let's check if anyone of the responds to $funcname them - * + * + * @param SS_List|array $fields + * @param callable $funcName * @return FormField */ protected function checkFieldsForAction($fields, $funcName) { @@ -465,13 +519,13 @@ class Form extends RequestHandler { * for tabs instead. This means that if you have a tab and a * formfield with the same name, this method gives priority * to the formfield. - * + * * @param SS_HTTPRequest $request * @return FormField */ public function handleField($request) { $field = $this->Fields()->dataFieldByName($request->param('FieldName')); - + if($field) { return $field; } else { @@ -486,19 +540,21 @@ class Form extends RequestHandler { public function makeReadonly() { $this->transform(new ReadonlyTransformation()); } - + /** - * Set whether the user should be redirected back down to the - * form on the page upon validation errors in the form or if + * Set whether the user should be redirected back down to the + * form on the page upon validation errors in the form or if * they just need to redirect back to the page * - * @param bool Redirect to the form + * @param bool $bool + * @return $this + * @internal param Redirect $bool to the form */ public function setRedirectToFormOnValidationError($bool) { $this->redirectToFormOnValidationError = $bool; return $this; } - + /** * Get whether the user should be redirected back down to the * form on the page upon validation errors @@ -512,6 +568,10 @@ class Form extends RequestHandler { /** * Add a plain text error message to a field on this form. It will be saved into the session * and used the next time this form is displayed. + * @param string $fieldName + * @param string $message + * @param string $messageType + * @param bool $escapeHtml */ public function addErrorMessage($fieldName, $message, $messageType, $escapeHtml = true) { Session::add_to_array("FormInfo.{$this->FormName()}.errors", array( @@ -521,6 +581,9 @@ class Form extends RequestHandler { )); } + /** + * @param FormTransformation $trans + */ public function transform(FormTransformation $trans) { $newFields = new FieldList(); foreach($this->fields as $field) { @@ -539,7 +602,7 @@ class Form extends RequestHandler { if($this->validator) $this->validator->removeValidation(); } - + /** * Get the {@link Validator} attached to this form. * @return Validator @@ -550,8 +613,10 @@ class Form extends RequestHandler { /** * Set the {@link Validator} on this form. + * @param Validator $validator + * @return $this */ - public function setValidator( Validator $validator ) { + public function setValidator(Validator $validator ) { if($validator) { $this->validator = $validator; $this->validator->setForm($this); @@ -569,6 +634,7 @@ class Form extends RequestHandler { /** * Convert this form to another format. + * @param FormTransformation $format */ public function transformTo(FormTransformation $format) { $newFields = new FieldList(); @@ -582,48 +648,48 @@ class Form extends RequestHandler { $this->validator->removeValidation(); } - + /** * Generate extra special fields - namely the security token field (if required). - * + * * @return FieldList */ public function getExtraFields() { $extraFields = new FieldList(); - + $token = $this->getSecurityToken(); $tokenField = $token->updateFieldSet($this->fields); if($tokenField) $tokenField->setForm($this); $this->securityTokenAdded = true; - + // add the "real" HTTP method if necessary (for PUT, DELETE and HEAD) if($this->FormMethod() != $this->FormHttpMethod()) { $methodField = new HiddenField('_method', '', $this->FormHttpMethod()); $methodField->setForm($this); $extraFields->push($methodField); } - + return $extraFields; } - + /** * Return the form's fields - used by the templates - * + * * @return FieldList The form fields */ public function Fields() { foreach($this->getExtraFields() as $field) { if(!$this->fields->fieldByName($field->getName())) $this->fields->push($field); } - + return $this->fields; } - + /** * Return all fields * in a form - including fields nested in {@link CompositeFields}. * Useful when doing custom field layouts. - * + * * @return FieldList */ public function HiddenFields() { @@ -637,20 +703,21 @@ class Form extends RequestHandler { public function VisibleFields() { return $this->Fields()->VisibleFields(); } - + /** * Setter for the form fields. * * @param FieldList $fields + * @return $this */ public function setFields($fields) { $this->fields = $fields; return $this; } - + /** * Return the form's action buttons - used by the templates - * + * * @return FieldList The action list */ public function Actions() { @@ -661,12 +728,13 @@ class Form extends RequestHandler { * Setter for the form actions. * * @param FieldList $actions + * @return $this */ public function setActions($actions) { $this->actions = $actions; return $this; } - + /** * Unset all form actions */ @@ -676,8 +744,9 @@ class Form extends RequestHandler { } /** - * @param String - * @param String + * @param string $name + * @param string $value + * @return $this */ public function setAttribute($name, $value) { $this->attributes[$name] = $value; @@ -685,12 +754,15 @@ class Form extends RequestHandler { } /** - * @return String + * @return string $name */ public function getAttribute($name) { if(isset($this->attributes[$name])) return $this->attributes[$name]; } + /** + * @return array + */ public function getAttributes() { $attrs = array( 'id' => $this->FormName(), @@ -712,10 +784,11 @@ class Form extends RequestHandler { /** * Return the attributes of the form tag - used by the templates. - * - * @param Array Custom attributes to process. Falls back to {@link getAttributes()}. + * + * @param array Custom attributes to process. Falls back to {@link getAttributes()}. * If at least one argument is passed as a string, all arguments act as excludes by name. - * @return String HTML attributes, ready for insertion into an HTML tag + * + * @return string HTML attributes, ready for insertion into an HTML tag */ public function getAttributesHTML($attrs = null) { $exclude = (is_string($attrs)) ? func_get_args() : null; @@ -738,12 +811,12 @@ class Form extends RequestHandler { $attrs = $this->getAttributes(); // Remove empty - $attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);')); - + $attrs = array_filter((array)$attrs, create_function('$v', 'return ($v || $v === 0);')); + // Remove excluded if($exclude) $attrs = array_diff_key($attrs, array_flip($exclude)); - // Create markkup + // Create markup $parts = array(); foreach($attrs as $name => $value) { $parts[] = ($value === true) ? "{$name}=\"{$name}\"" : "{$name}=\"" . Convert::raw2att($value) . "\""; @@ -757,41 +830,45 @@ class Form extends RequestHandler { } /** - * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing - * another frame - * - * @param target The value of the target - */ + * Set the target of this form to any value - useful for opening the form contents in a new window or refreshing + * another frame + * + * @param string $target The value of the target + * @return $this + */ public function setTarget($target) { $this->target = $target; return $this; } - + /** * Set the legend value to be inserted into * the element in the Form.ss template. + * @param $legend + * @return $this */ public function setLegend($legend) { $this->legend = $legend; return $this; } - + /** * Set the SS template that this form should use * to render with. The default is "Form". - * + * * @param string $template The name of the template (without the .ss extension) + * @return $this */ public function setTemplate($template) { $this->template = $template; return $this; } - + /** * Return the template to render this form with. * If the template isn't set, then default to the * form class name e.g "Form". - * + * * @return string */ public function getTemplate() { @@ -824,7 +901,8 @@ class Form extends RequestHandler { * Sets the form encoding type. The most common encoding types are defined * in {@link ENC_TYPE_URLENCODED} and {@link ENC_TYPE_MULTIPART}. * - * @param string $enctype + * @param $encType + * @return $this */ public function setEncType($encType) { $this->encType = $encType; @@ -840,18 +918,18 @@ class Form extends RequestHandler { * gets evaluated in {@link Director::direct()}. * See {@link FormMethod()} to get a HTTP method * for safe insertion into a tag. - * + * * @return string HTTP method */ public function FormHttpMethod() { return $this->formMethod; } - + /** * Returns the form method to be used in the tag. * See {@link FormHttpMethod()} to get the "real" method. - * - * @return string Form tag compatbile HTTP method: 'get' or 'post' + * + * @return string Form tag compatible HTTP method: 'get' or 'post' */ public function FormMethod() { if(in_array($this->formMethod,array('get','post'))) { @@ -860,12 +938,13 @@ class Form extends RequestHandler { return 'post'; } } - + /** * Set the form method: GET, POST, PUT, DELETE. - * - * @param $method string - * @param $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. + * + * @param string $method + * @param bool $strict If non-null, pass value to {@link setStrictFormMethodCheck()}. + * @return $this */ public function setFormMethod($method, $strict = null) { $this->formMethod = strtolower($method); @@ -884,6 +963,7 @@ class Form extends RequestHandler { * form. * * @param $bool boolean + * @return $this */ public function setStrictFormMethodCheck($bool) { $this->strictFormMethodCheck = (bool)$bool; @@ -896,12 +976,12 @@ class Form extends RequestHandler { public function getStrictFormMethodCheck() { return $this->strictFormMethodCheck; } - + /** * Return the form's action attribute. * This is build by adding an executeForm get variable to the parent controller's Link() value - * - * @return string + * + * @return string */ public function FormAction() { if ($this->formActionPath) { @@ -912,15 +992,18 @@ class Form extends RequestHandler { return Controller::join_links($this->controller->Link(), $this->name); } } - + /** @ignore */ private $formActionPath = false; - + /** * Set the form action attribute to a custom URL. - * + * * Note: For "normal" forms, you shouldn't need to use this method. It is recommended only for situations where * you have two relatively distinct parts of the system trying to communicate via a form post. + * + * @param string $path + * @return $this */ public function setFormAction($path) { $this->formActionPath = $path; @@ -942,12 +1025,15 @@ class Form extends RequestHandler { /** * Set the HTML ID attribute of the form + * + * @param string $id + * @return $this */ public function setHTMLID($id) { $this->htmlID = $id; return $this; } - + /** * Returns this form's controller. * This is used in the templates. @@ -1005,14 +1091,14 @@ class Form extends RequestHandler { * The next functions store and modify the forms * message attributes. messages are stored in session under * $_SESSION[formname][message]; - * + * * @return string */ public function Message() { $this->getMessageFromSession(); return $this->message; } - + /** * @return string */ @@ -1037,12 +1123,13 @@ class Form extends RequestHandler { /** * Set a status message for the form. - * - * @param message the text of the message - * @param type Should be set to good, bad, or warning. + * + * @param string $message the text of the message + * @param string $type Should be set to good, bad, or warning. * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML. * In that case, you might want to use {@link Convert::raw2xml()} to escape any * user supplied data in the message. + * @return $this */ public function setMessage($message, $type, $escapeHtml = true) { $this->message = ($escapeHtml) ? Convert::raw2xml($message) : $message; @@ -1052,16 +1139,16 @@ class Form extends RequestHandler { /** * Set a message to the session, for display next time this form is shown. - * - * @param message the text of the message - * @param type Should be set to good, bad, or warning. + * + * @param string $message the text of the message + * @param string $type Should be set to good, bad, or warning. * @param boolean $escapeHtml Automatically sanitize the message. Set to FALSE if the message contains HTML. * In that case, you might want to use {@link Convert::raw2xml()} to escape any * user supplied data in the message. */ public function sessionMessage($message, $type, $escapeHtml = true) { Session::set( - "FormInfo.{$this->FormName()}.formError.message", + "FormInfo.{$this->FormName()}.formError.message", $escapeHtml ? Convert::raw2xml($message) : $message ); Session::set("FormInfo.{$this->FormName()}.formError.type", $type); @@ -1069,7 +1156,7 @@ class Form extends RequestHandler { public static function messageForForm( $formName, $message, $type, $escapeHtml = true) { Session::set( - "FormInfo.{$formName}.formError.message", + "FormInfo.{$formName}.formError.message", $escapeHtml ? Convert::raw2xml($message) : $message ); Session::set("FormInfo.{$formName}.formError.type", $type); @@ -1089,13 +1176,13 @@ class Form extends RequestHandler { /** * Returns the DataObject that has given this form its data * through {@link loadDataFrom()}. - * + * * @return DataObject */ public function getRecord() { return $this->record; } - + /** * Get the legend value to be inserted into the * element in Form.ss @@ -1113,7 +1200,7 @@ class Form extends RequestHandler { * Triggered through {@link httpSubmission()}. * Note that CSRF protection takes place in {@link httpSubmission()}, * if it fails the form data will never reach this method. - * + * * @return boolean */ public function validate(){ @@ -1123,7 +1210,7 @@ class Form extends RequestHandler { if($errors){ // Load errors into session and post back $data = $this->getData(); - Session::set("FormInfo.{$this->FormName()}.errors", $errors); + Session::set("FormInfo.{$this->FormName()}.errors", $errors); Session::set("FormInfo.{$this->FormName()}.data", $data); return false; } @@ -1141,20 +1228,20 @@ class Form extends RequestHandler { * If you passed an array, it will call $object[MyField]. * Doesn't save into dataless FormFields ({@link DatalessField}), * as determined by {@link FieldList->dataFields()}. - * + * * By default, if a field isn't set (as determined by isset()), * its value will not be saved to the field, retaining * potential existing values. - * + * * Passed data should not be escaped, and is saved to the FormField instances unescaped. * Escaping happens automatically on saving the data through {@link saveInto()}. - * + * * @uses FieldList->dataFields() * @uses FormField->setValue() - * + * * @param array|DataObject $data * @param int $mergeStrategy - * For every field, {@link $data} is interogated whether it contains a relevant property/key, and + * For every field, {@link $data} is interrogated whether it contains a relevant property/key, and * what that property/key's value is. * * By default, if {@link $data} does contain a property/key, the fields value is always replaced by {@link $data}'s @@ -1172,7 +1259,7 @@ class Form extends RequestHandler { * For backwards compatibility reasons, this parameter can also be set to === true, which is the same as passing * CLEAR_MISSING * - * @param $fieldList An optional list of fields to process. This can be useful when you have a + * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a * form that has some fields that save to one object, and some that save to another. * @return Form */ @@ -1198,9 +1285,9 @@ class Form extends RequestHandler { if($dataFields) foreach($dataFields as $field) { $name = $field->getName(); - // Skip fields that have been exlcuded + // Skip fields that have been excluded if($fieldList && !in_array($name, $fieldList)) continue; - + // First check looks for (fieldname)_unchanged, an indicator that we shouldn't overwrite the field value if(is_array($data) && isset($data[$name . '_unchanged'])) continue; @@ -1253,13 +1340,13 @@ class Form extends RequestHandler { return $this; } - + /** * Save the contents of this form into the given data object. * It will make use of setCastedField() to do this. - * - * @param $dataObject The object to save data into - * @param $fieldList An optional list of fields to process. This can be useful when you have a + * + * @param DataObjectInterface $dataObject The object to save data into + * @param FieldList $fieldList An optional list of fields to process. This can be useful when you have a * form that has some fields that save to one object, and some that save to another. */ public function saveInto(DataObjectInterface $dataObject, $fieldList = null) { @@ -1282,7 +1369,7 @@ class Form extends RequestHandler { } if($lastField) $lastField->saveInto($dataObject); } - + /** * Get the submitted data from this form through * {@link FieldList->dataFields()}, which filters out @@ -1290,13 +1377,13 @@ class Form extends RequestHandler { * Calls {@link FormField->dataValue()} on each field, * which returns a value suitable for insertion into a DataObject * property. - * + * * @return array */ public function getData() { $dataFields = $this->fields->dataFields(); $data = array(); - + if($dataFields){ foreach($dataFields as $field) { if($field->getName()) { @@ -1311,8 +1398,9 @@ class Form extends RequestHandler { * Call the given method on the given field. * This is used by Ajax-savvy form fields. By putting '&action=callfieldmethod' to the end * of the form action, they can access server-side data. - * @param fieldName The name of the field. Can be overridden by $_REQUEST[fieldName] - * @param methodName The name of the field. Can be overridden by $_REQUEST[methodName] + * + * @param array $data + * @return mixed */ public function callfieldmethod($data) { $fieldName = $data['fieldName']; @@ -1335,12 +1423,11 @@ class Form extends RequestHandler { } else { user_error("Form::callfieldmethod() Field '$fieldName' not found", E_USER_ERROR); } - } /** * Return a rendered version of this form. - * + * * This is returned when you access a form as $FormObject rather * than <% with FormObject %> */ @@ -1365,7 +1452,7 @@ class Form extends RequestHandler { $this->getTemplate(), 'Form' )); - + return $view->dontRewriteHashlinks()->process($this); } @@ -1391,6 +1478,8 @@ class Form extends RequestHandler { /** * Render this form using the given template, and return the result as a string * You can pass either an SSViewer or a template name + * @param string|array $template + * @return HTMLText */ public function renderWithoutActionButton($template) { $custom = $this->customise(array( @@ -1404,13 +1493,18 @@ class Form extends RequestHandler { /** * Sets the button that was clicked. This should only be called by the Controller. - * @param funcName The name of the action method that will be called. + * + * @param callable $funcName The name of the action method that will be called. + * @return $this */ public function setButtonClicked($funcName) { $this->buttonClickedFunc = $funcName; return $this; } + /** + * @return mixed + */ public function buttonClicked() { foreach($this->actions->dataFields() as $action) { if($action->hasMethod('actionname') && $this->buttonClickedFunc == $action->actionName()) { @@ -1421,6 +1515,8 @@ class Form extends RequestHandler { /** * Return the default button that should be clicked when another one isn't available + * + * @return mixed|void */ public function defaultAction() { if($this->hasDefaultAction && $this->actions) @@ -1436,45 +1532,45 @@ class Form extends RequestHandler { $this->hasDefaultAction = false; return $this; } - + /** * Disable the requirement of a security token on this form instance. This security protects - * against CSRF attacks, but you should disable this if you don't want to tie + * against CSRF attacks, but you should disable this if you don't want to tie * a form to a session - eg a search form. - * + * * Check for token state with {@link getSecurityToken()} and {@link SecurityToken->isEnabled()}. */ public function disableSecurityToken() { $this->securityToken = new NullSecurityToken(); return $this; } - + /** * Enable {@link SecurityToken} protection for this form instance. - * + * * Check for token state with {@link getSecurityToken()} and {@link SecurityToken->isEnabled()}. */ public function enableSecurityToken() { $this->securityToken = new SecurityToken(); return $this; } - + /** * Returns the security token for this form (if any exists). * Doesn't check for {@link securityTokenEnabled()}. * Use {@link SecurityToken::inst()} to get a global token. - * + * * @return SecurityToken|null */ public function getSecurityToken() { return $this->securityToken; } - + /** * Returns the name of a field, if that's the only field that the current controller is interested in. * It checks for a call to the callfieldmethod action. * This is useful for optimising your forms - * + * * @return string */ public static function single_field_required() { @@ -1491,26 +1587,29 @@ class Form extends RequestHandler { /** * Set the current form action. Should only be called by Controller. + * + * @param $action */ public static function set_current_action($action) { self::$current_action = $action; } - + /** - * Compiles all CSS-classes. - * + * Compiles all CSS-classes. + * * @return string */ public function extraClass() { return implode(array_unique($this->extraClasses), ' '); } - + /** * Add a CSS-class to the form-container. If needed, multiple classes can - * be added by delimiting a string with spaces. + * be added by delimiting a string with spaces. * * @param string $class A string containing a classname or several class - * names delimited by a single space. + * names delimited by a single space. + * @return $this */ public function addExtraClass($class) { //split at white space @@ -1527,6 +1626,7 @@ class Form extends RequestHandler { * be passed through as a space delimited string * * @param string $class + * @return $this */ public function removeExtraClass($class) { //split at white space @@ -1537,7 +1637,7 @@ class Form extends RequestHandler { } return $this; } - + public function debug() { $result = "

$this->class

    "; foreach($this->fields as $field) { @@ -1550,28 +1650,34 @@ class Form extends RequestHandler { return $result; } - - + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TESTING HELPERS ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - + /** * Test a submission of this form. + * @param string $action + * @param array $data * @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in * your unit test. + * @throws SS_HTTPResponse_Exception */ public function testSubmission($action, $data) { $data['action_' . $action] = true; return Director::test($this->FormAction(), $data, Controller::curr()->getSession()); - + //$response = $this->controller->run($data); //return $response; } - + /** * Test an ajax submission of this form. + * + * @param string $action + * @param array $data * @return SS_HTTPResponse the response object that the handling controller produces. You can interrogate this in * your unit test. */ @@ -1587,14 +1693,16 @@ class Form extends RequestHandler { */ class Form_FieldMap extends ViewableData { protected $form; - + public function __construct($form) { $this->form = $form; parent::__construct(); } - + /** * Ensure that all potential method calls get passed to __call(), therefore to dataFieldByName + * @param string $method + * @return bool */ public function hasMethod($method) { return true; From d673fdf4978d69ed56ad926d13be486ae344cb5a Mon Sep 17 00:00:00 2001 From: Matthew Hailwood Date: Fri, 12 Jun 2015 11:59:48 +1200 Subject: [PATCH 16/25] Float labels with ss-ui-button class in accordion Currently things like the upload field etc break when you use them inside a ToggleCompositeField: ![Broken](http://i.imgur.com/abUopVf.png) This fixes it by adding the float back ![Fixed](http://i.imgur.com/7P0muen.png) --- admin/css/screen.css | 3 ++- admin/scss/_forms.scss | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/admin/css/screen.css b/admin/css/screen.css index 62ee1e6bd..1572d73f3 100644 --- a/admin/css/screen.css +++ b/admin/css/screen.css @@ -139,7 +139,7 @@ body, html { font-size: 12px; line-height: 16px; font-family: Arial, sans-serif; .ui-accordion .ui-accordion-header { border-color: #c0c0c2; margin-bottom: 0; } .ui-accordion .ui-accordion-content { border: 1px solid #c0c0c2; border-top: none; } -.ui-autocomplete { max-height: 240px; overflow-x: hidden; overflow-y: auto; } +.ui-autocomplete { max-height: 240px; overflow-x: hidden; overflow-y: auto; /** sorry about the !important but the specificity of other selectors mandates it over writing out very specific selectors **/ } .ui-autocomplete-loading { background-image: url(../images/throbber.gif) !important; background-position: 97% center !important; background-repeat: no-repeat !important; background-size: auto !important; } /** This file defines common styles for form elements used throughout the CMS interface. It is an addition to the base styles defined in framework/css/Form.css. @package framework @subpackage admin */ @@ -249,6 +249,7 @@ form.small .field input.text, form.small .field textarea, form.small .field sele .ss-toggle .ui-accordion-content .field:last-child { margin-bottom: 0; } .ss-toggle .ui-accordion-content .field .middleColumn { margin-left: 0; } .ss-toggle .ui-accordion-content .field label { float: none; margin-left: 0; } +.ss-toggle .ui-accordion-content .field label.ss-ui-button { float: left; } .ss-toggle .ui-accordion-content .field .description { margin-left: 0; } /** ---------------------------------------------------- Checkbox Field ---------------------------------------------------- */ diff --git a/admin/scss/_forms.scss b/admin/scss/_forms.scss index b5135955f..446619f53 100644 --- a/admin/scss/_forms.scss +++ b/admin/scss/_forms.scss @@ -584,6 +584,10 @@ form.small .field, .field.small { label { float: none; margin-left: 0; + + &.ss-ui-button { + float: left; + } } .description { margin-left: 0; From 96c14bd7bfa50eccbbddb01b74f2720bc07e4fca Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Fri, 12 Jun 2015 19:41:24 +0100 Subject: [PATCH 17/25] Make travis more resilient to composer self-update `composer self-update` has been failing regularly on travis recently. As `composer` is already installed and it's not strictly essential to have the very latest version, this change allows the build to continue, even if composer can't self-update --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0efc2f2fe..6875502cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ matrix: - sudo apt-get install -y tidy before_script: - - composer self-update + - composer self-update || true - phpenv rehash - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support - "if [ \"$BEHAT_TEST\" = \"\" ]; then php ~/travis-support/travis_setup.php --source `pwd` --target ~/builds/ss; fi" From 52a248534ab7ff0be7e00c0b61b5486498684e76 Mon Sep 17 00:00:00 2001 From: David Alexander Date: Sat, 13 Jun 2015 11:08:25 +1200 Subject: [PATCH 18/25] Update index.md Minor typo. --- docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md index b9b7ea418..8d8ca4b6f 100644 --- a/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md +++ b/docs/en/02_Developer_Guides/16_Execution_Pipeline/index.md @@ -11,7 +11,7 @@ SilverStripe needs to boot its core and run through several stages of processing The first step in most environments is a rewrite of a request path into parameters passed to a PHP script. This allows writing friendly URLs instead of linking directly to PHP files. -The implementation depends on your web server, we'll show you the most common one here: +The implementation depends on your web server; we'll show you the most common one here: Apache with [mod_rewrite](http://httpd.apache.org/docs/2.0/mod/mod_rewrite.html). Check our [installation guides](/getting_started/installation) on how other web servers like IIS or nginx handle rewriting. @@ -123,7 +123,7 @@ further filtering before content is sent to the end user The framework provides the ability to hook into the request both before and after it is handled to allow binding custom logic. This can be used -to transform or filter request data, instanciate helpers, execute global logic, +to transform or filter request data, instantiate helpers, execute global logic, or even short-circuit execution (e.g. to enforce custom authentication schemes). The ["Request Filters" documentation](../controllers/requestfilters) shows you how. From ee4fe7c10caa4b97cbe60aa4b41f7a3117098e7a Mon Sep 17 00:00:00 2001 From: David Alexander Date: Sat, 13 Jun 2015 21:34:32 +1200 Subject: [PATCH 19/25] Update 03_Documentation.md Minor typos & rewording. Clearer example needed on line 104. --- docs/en/05_Contributing/03_Documentation.md | 77 ++++++++------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/docs/en/05_Contributing/03_Documentation.md b/docs/en/05_Contributing/03_Documentation.md index f8e4ecf18..69ac3ca2e 100644 --- a/docs/en/05_Contributing/03_Documentation.md +++ b/docs/en/05_Contributing/03_Documentation.md @@ -3,45 +3,35 @@ summary: Writing guide for contributing to SilverStripe developer and CMS user h # Contributing documentation -Documentation for a software project is a continued and collaborative effort, we encourage everybody to contribute, from -simply fixing spelling mistakes, to writing recipes, reviewing existing documentation, and translating the whole thing. +Documentation for a software project is a continued and collaborative effort. We encourage everybody to contribute in any way they can, from simply fixing spelling mistakes, to writing recipes, to reviewing existing documentation and translating it to another language. Modifying documentation requires basic [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) and [Markdown](http://daringfireball.net/projects/markdown/) knowledge, and a GitHub user account. ## Editing online -The easiest way of making a change to the documentation is by clicking the "Edit this page" link at the bottom of the -page you want to edit. Alternatively, you can find the appropriate .md file in the -[github.com/silverstripe/silverstripe-framework](https://github.com/silverstripe/silverstripe-framework/tree/master/docs/) -repository and press the "edit" button. **You will need a free GitHub account to do this**. +The easiest way of editing any documentation is by clicking the "Edit this page" link at the bottom of the +page you want to edit. Alternatively, locate the appropriate .md file in the +[github.com/silverstripe/silverstripe-framework](https://github.com/silverstripe/silverstripe-framework/tree/master/docs/) repository and press the "edit" button. **You will need a free GitHub account to do this**. - * After you have made your change, describe it in the "commit summary" and "extended description" fields below, and - press "Commit Changes". - * After that you will see form to submit a Pull Request. You should be able to adjust the version your document ion change is for and then submit the form. Your changes - will be sent to the core committers for approval. + * After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then press "Commit Changes". + * After that you will see a form to submit a Pull Request. You should be able to adjust the version your document ion changes are for and then submit the form. Your changes will be sent to the core committers for approval.
    -You should make the changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you -found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) -version of the documentation automatically. *Don't submit multiple pull requests*. +You should make your changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) version of the documentation automatically. *Don't submit multiple pull requests*.
    ## Editing on your computer -If you prefer to edit the content on your local machine, you can "[fork](http://help.github.com/forking/)" the +If you prefer to edit content on your local machine, you can "[fork](http://help.github.com/forking/)" the [github.com/silverstripe/silverstripe-framework](http://github.com/silverstripe/silverstripe-framework) and -[github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms) repositories and send us -"[pull requests](http://help.github.com/pull-requests/)". If you have downloaded SilverStripe or a module, chances are -that you already have these repositories on your machine. +[github.com/silverstripe/silverstripe-cms](http://github.com/silverstripe/silverstripe-cms) repositories, and send us "[pull requests](http://help.github.com/pull-requests/)" to incorporate your changes. If you have previously downloaded SilverStripe or a module, chances are that you already have these repositories on your machine. -The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or -CMS folder. +The documentation is kept alongside the source code in the `docs/` subfolder of any SilverStripe module, framework or CMS folder.
    -If you submit a new feature or an API change, we strongly recommend that your patch includes updates to the necessary -documentation. This helps prevent our documentation from getting out of date. +If you submit a new feature or an API change, we strongly recommend that your patch includes updates to the necessary documentation. This helps prevent our documentation from getting out of date.
    ## Repositories @@ -52,44 +42,38 @@ documentation. This helps prevent our documentation from getting out of date. ## Source control -In order to balance editorial control with effective collaboration, we keep documentation alongside the module source -code, e.g. in `framework/docs/`, or as code comments within PHP code. Contributing documentation is the same process as -providing any other patch (see [Contributing code](code)). +In order to balance editorial control with effective collaboration, we keep documentation alongside the module source code, e.g. in `framework/docs/`, or as code comments within PHP code. Contributing documentation is the same process as providing any other patch (see [Contributing code](code)). ## What to write -See [what to write (jacobian.org)](http://jacobian.org/writing/great-documentation/what-to-write/) for an excellent -introduction to the different types of documentation, and -[producing OSS: "documentation"](http://producingoss.com/en/getting-started.html#documentation) for good rules of thumb +See [what to write (jacobian.org)](http://jacobian.org/writing/great-documentation/what-to-write/) for an excellent introduction to the different types of documentation. Also see [producing OSS: "documentation"](http://producingoss.com/en/getting-started.html#documentation) for good rules of thumb for documenting open source software. ## Structure * Keep documentation lines to 120 characters. -* Don't duplicate: Search for existing places to put your documentation. Do you really require a new page, or just a new paragraph -of text somewhere? +* Don't duplicate: Search for existing places to put your documentation. Do you really require a new page, or just a new paragraph of text somewhere? * Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format. -* API and developer guides complement each other: Both forms of documenting source code (API and Developer Guides) are valuable resources. -* Provide context: Give API documentation the "bigger picture" by referring to developer guides inside your PHPDoc. -* Make your documentation findable: Documentation lives by interlinking content, so please make sure your contribution doesn't become an -inaccessible island. Your page should at least be linked on the index page in the same folder. It can also appear +* API and developer guides are two forms of source code documentation that complement each other. +* API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc. +* Make your documentation easy to find: Documentation lives by interlinking content so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your page should on the index page in the same folder. A link to your page can also appear as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`). ## Writing style -* Write in second plural form: Use "we" instead of "I". It gives the text an instructive and collaborative style. +* Write in second person plural form: Use "we" instead of "I". It gives the text an instructive and collaborative style. * It's okay to address the reader: For example "First you'll install a webserver" is good style. * Write in an active and direct voice. -* Mark up correctly: Use preformatted text, emphasis and bold to make technical writing more "scannable". -* Avoid FAQs: FAQs are not a replacement of a coherent, well explained documentation. If you've done a good job +* Mark up correctly: Use preformatted text. Emphasis and bold make technical writing more easily "scannable". +* Avoid FAQs: FAQs are not a replacement for coherent, well explained documentation. If you've done a good job documenting, there shouldn't be any "frequently asked questions" left. * "SilverStripe" should always appear without a space, use two capital S’. * Use simple language and words. Avoid uncommon jargon and overly long words. * Use UK English and not US English. SilverStripe is proudly a New Zealand open source project we use the UK spelling and forms of English. The most common of these differences are -ize vs -ise, or -or vs our (eg color vs colour). -* We use sentence case for titles so only capitalise the first letter of the first word of a title. Only exceptions to this are when using branded (e.g. SilverStripe), acronyms (e.g. PHP) and class names (e.g. ModelAdmin). +* We use sentence case for titles so only capitalise the first letter of the first word of a title. The only exceptions to this are when using brand names (e.g. SilverStripe), acronyms (e.g. PHP) and class names (e.g. ModelAdmin). * Use gender neutral language throughout the document, unless referencing a specific person. Use them, they, their, instead of he and she, his or her. -* URLs: is the end of your sentence is a URL, you don't need to use a full stop. -* Bullet points: Sentence case your bullet points, if it is a full sentence then end with a full stop. If it is a short point or list full stops are not required. +* URLs: if the end of your sentence is a URL then you don't need to use a full stop. +* Bullet points: Sentence case your bullet points. If a bullet point is a full sentence then end with a full stop. If it is a short point or a list, full stops are not required. ## Highlighted blocks @@ -97,8 +81,7 @@ There are several built-in block styles for highlighting a paragraph of text. Pl sparingly.
    -"Tip box": Adds, deepens or accents information in the main text. Can be used for background knowledge, or "see also" -links. +"Tip box": A tip box is great for adding, deepening or accenting information in the main text. They can be used for background knowledge, or to provide links to further information (ie, a "see also" link).
    Code: @@ -108,8 +91,7 @@ Code:
    -"Notification box": Technical notifications relating to the main text. For example, notifying users about a deprecated -feature. +"Notification box": A notification box is goof for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
    Code: @@ -119,8 +101,7 @@ Code:
    -"Warning box": Highlight a severe bug or technical issue requiring a users attention. For example, a code block with -destructive functionality might not have its URL actions secured to keep the code shorter. +"Warning box": A warning box is usefull for highlighting a severe bug or technical issue requiring a user's attention. For example, a code block with destructive functionality might not have its URL actions secured to keep the code shorter (Can a clearer example be given ??).
    Code: @@ -129,14 +110,12 @@ Code: ... -See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restriction +See [markdown extra documentation](http://michelf.com/projects/php-markdown/extra/#html) for more restrictions on placing HTML blocks inside Markdown. ## Translating documentation -Documentation is kept alongside the source code, typically in a module subdirectory like `framework/docs/en/`. Each -language has its own subfolder, which can duplicate parts or the whole body of documentation. German documentation -would for example live in `framework/docs/de/`. The +Documentation is kept alongside the source code, typically in a module subdirectory like `framework/docs/en/`. Each language has its own subfolder, which can duplicate parts of or the entire body of documentation. German documentation would, for example, live in `framework/docs/de/`. The [docsviewer](https://github.com/silverstripe/silverstripe-docsviewer) module that drives [doc.silverstripe.org](http://doc.silverstripe.org) automatically resolves these subfolders into a language dropdown. From e54b7fe4047bdc973f56fc34548663f4b8ca2945 Mon Sep 17 00:00:00 2001 From: David Alexander Date: Sun, 14 Jun 2015 08:29:50 +1200 Subject: [PATCH 20/25] Update 03_Documentation.md Addressed dhensby's comments :) --- docs/en/05_Contributing/03_Documentation.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/en/05_Contributing/03_Documentation.md b/docs/en/05_Contributing/03_Documentation.md index 69ac3ca2e..d920a3677 100644 --- a/docs/en/05_Contributing/03_Documentation.md +++ b/docs/en/05_Contributing/03_Documentation.md @@ -16,7 +16,8 @@ page you want to edit. Alternatively, locate the appropriate .md file in the * After editing the documentation, describe your changes in the "commit summary" and "extended description" fields below then press "Commit Changes". - * After that you will see a form to submit a Pull Request. You should be able to adjust the version your document ion changes are for and then submit the form. Your changes will be sent to the core committers for approval. + * After that you will see a form to submit a Pull Request: "[pull requests](http://help.github.com/pull-requests/)". You should be able to adjust the version your + * documentation changes are for and then submit the form. Your changes will be sent to the core committers for approval.
    You should make your changes in the lowest branch they apply to. For instance, if you fix a spelling issue that you found in the 3.1 documentation, submit your fix to that branch in Github and it'll be copied to the master (3.2) version of the documentation automatically. *Don't submit multiple pull requests*. @@ -56,7 +57,7 @@ for documenting open source software. * Use PHPDoc in source code: Leave low level technical documentation to code comments within PHP, in [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) format. * API and developer guides are two forms of source code documentation that complement each other. * API documentation should provide context, ie, the "bigger picture", by referring to developer guides inside your PHPDoc. -* Make your documentation easy to find: Documentation lives by interlinking content so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your page should on the index page in the same folder. A link to your page can also appear +* Make your documentation easy to find: Documentation lives by interlinking content so please make sure your contribution doesn't become an inaccessible island. At the very least, put a link to your should on the index page in the same folder. A link to your page can also appear as "related content" on other resource (e.g. `/tutorials/site_search` might link to `/developer_guides/forms/introduction`). ## Writing style @@ -67,7 +68,7 @@ as "related content" on other resource (e.g. `/tutorials/site_search` might link * Mark up correctly: Use preformatted text. Emphasis and bold make technical writing more easily "scannable". * Avoid FAQs: FAQs are not a replacement for coherent, well explained documentation. If you've done a good job documenting, there shouldn't be any "frequently asked questions" left. -* "SilverStripe" should always appear without a space, use two capital S’. +* "SilverStripe" should always appear without a space with both "S"s capitalised. * Use simple language and words. Avoid uncommon jargon and overly long words. * Use UK English and not US English. SilverStripe is proudly a New Zealand open source project we use the UK spelling and forms of English. The most common of these differences are -ize vs -ise, or -or vs our (eg color vs colour). * We use sentence case for titles so only capitalise the first letter of the first word of a title. The only exceptions to this are when using brand names (e.g. SilverStripe), acronyms (e.g. PHP) and class names (e.g. ModelAdmin). @@ -91,7 +92,7 @@ Code:
    -"Notification box": A notification box is goof for technical notifications relating to the main text. For example, notifying users about a deprecated feature. +"Notification box": A notification box is good for technical notifications relating to the main text. For example, notifying users about a deprecated feature.
    Code: @@ -101,7 +102,7 @@ Code:
    -"Warning box": A warning box is usefull for highlighting a severe bug or technical issue requiring a user's attention. For example, a code block with destructive functionality might not have its URL actions secured to keep the code shorter (Can a clearer example be given ??). +"Warning box": A warning box is useful for highlighting a severe bug or a technical issue requiring a user's attention. For example, suppose a rare edge case sometimes leads to a variable being overwritten incorrectly. A warning box can be used to alert the user to this case so they can write their own code to handle it.
    Code: From 2b9ccda06b241d2dc49465133f866405ac26aa62 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sun, 14 Jun 2015 12:49:37 +0100 Subject: [PATCH 21/25] Fixing doc block issues --- forms/Form.php | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/forms/Form.php b/forms/Form.php index 6699dfa56..7db923a8c 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -55,27 +55,27 @@ class Form extends RequestHandler { public $IncludeFormTag = true; /** - * @var FieldList + * @var FieldList|null */ protected $fields; /** - * @var FieldList + * @var FieldList|null */ protected $actions; /** - * @var Controller + * @var Controller|null */ protected $controller; /** - * @var string + * @var string|null */ protected $name; /** - * @var Validator + * @var Validator|null */ protected $validator; @@ -90,12 +90,12 @@ class Form extends RequestHandler { protected $strictFormMethodCheck = false; /** - * @var string + * @var string|null */ protected static $current_action; /** - * @var DataObject $record Populated by {@link loadDataFrom()}. + * @var DataObject|null $record Populated by {@link loadDataFrom()}. */ protected $record; @@ -112,7 +112,7 @@ class Form extends RequestHandler { * Useful to open a new window upon * form submission. * - * @var string + * @var string|null */ protected $target; @@ -121,7 +121,7 @@ class Form extends RequestHandler { * element before the
    * in Form.ss template. * - * @var string + * @var string|null */ protected $legend; @@ -131,22 +131,22 @@ class Form extends RequestHandler { * another template for customisation. * * @see Form->setTemplate() - * @var string + * @var string|null */ protected $template; /** - * @var callable + * @var callable|null */ protected $buttonClickedFunc; /** - * @var string + * @var string|null */ protected $message; /** - * @var string + * @var string|null */ protected $messageType; @@ -164,7 +164,7 @@ class Form extends RequestHandler { protected $security = true; /** - * @var SecurityToken + * @var SecurityToken|null */ protected $securityToken = null; @@ -174,7 +174,7 @@ class Form extends RequestHandler { protected $extraClasses = array(); /** - * @var string + * @var string|null */ protected $encType; @@ -196,7 +196,7 @@ class Form extends RequestHandler { /** * @var bool */ - protected $securityTokenAdded; + protected $securityTokenAdded = false; /** * Create a new form, with the given fields an action buttons. @@ -546,9 +546,8 @@ class Form extends RequestHandler { * form on the page upon validation errors in the form or if * they just need to redirect back to the page * - * @param bool $bool + * @param bool $bool Redirect to form on error? * @return $this - * @internal param Redirect $bool to the form */ public function setRedirectToFormOnValidationError($bool) { $this->redirectToFormOnValidationError = $bool; From a569bc982568ae0b57f4d993d96d714388ce08c3 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 15 Jun 2015 10:47:02 +1200 Subject: [PATCH 22/25] Update translations --- lang/fi.yml | 3 +-- lang/nl.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lang/fi.yml b/lang/fi.yml index 2ca08f5cf..6619a76a1 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -258,8 +258,7 @@ fi: many_many_Members: Jäsenet GroupImportForm: Help1: '

    Tuo yksi tai useampi ryhmä CSV-muotoisena (arvot pilkulla erotettuina). Näytä edistyksellinen käyttö

    ' - Help2: "
    \n\t

    Edistynyt käyttö

    \n\t
      \n\t
    • Sallitut palstat: %s
    • \n\t
    • Olemassa olevat ryhmät kohdistetaan niiden uniikin Code arvolla, ja päivitetään uudet arvot tuodusta tiedostosta
    • \n\t
    • Oikeustasot voidaan luoda käyttämällä ParentCode palstaa.
    • \n\t
    • Oikeustasokoodit voidaan kohdistaa PermissionCode palstassa. Olemassaolevia oikeusia ei tyhjennetä.
    • \n\t
    \n\ -
    " + Help2: "
    \n

    Edistynyt käyttö

    \n
      \n
    • Sallitut sarakkeet: %s
    • \n
    • Olemassa olevat rhymes kohdistetaan niiden uniikin Code arvolla, ja päivitetään arvot tuodusta tiedostosta
    • \n
    • Ryhmien hierarkiat voidaan luoda ParentCode sarakkeessa.
    • \n
    • Oikeustasokoodit voidaan kohdistaa PermissionCode sarakkeessa. Olemassa olevia oikeuksia ei tyhjennetä.
    • \n
    \n
    " ResultCreated: 'Luotiin {count} ryhmä(ä)' ResultDeleted: 'Poistettu %d ryhmää' ResultUpdated: 'Päivitetty %d ryhmää' diff --git a/lang/nl.yml b/lang/nl.yml index de4171f3b..241715533 100644 --- a/lang/nl.yml +++ b/lang/nl.yml @@ -392,7 +392,7 @@ nl: Toggle: 'Toon opmaak hulp' MemberImportForm: Help1: '

    Importeer leden in CSV-formaat (comma-separated values). Toon geavanceerd gebruik

    ' - Help2: "
    \n\t

    Advanced usage

    \n\t
      \n\t
    • Allowed columns: %s
    • \n\t
    • Existing users are matched by their unique Code property, and updated with any new values from\n\tthe imported file.
    • \n\t
    • Groups can be assigned by the Groups column. Groups are identified by their Code property,\n\tmultiple groups can be separated by comma. Existing group memberships are not cleared.
    • \n\t
    \n
    " + Help2: "
    \n

    Geavanceerd gebruik

    \n
      \n
    • Toegestane kolommen: %s
    • \n
    • Bestaande groepen worden geïdentificeerd door middel van hun unieke Code-waarde, en aangepast met de nieuwe waarden vanuit het geïmporteerde bestand
    • \n
    • Groepshiërarchiën kunnen aangemaakt worden door een ParentCode-kolom te gebruiken
    • \n
    • Toegangscodeskunnen toegewezen worden met de PermissionCode kolom. Bestaande toegangscodes worden niet verwijderd.
    • \n
    \n
    " ResultCreated: '{count} leden aangemaakt' ResultDeleted: '%d leden verwijderd' ResultNone: 'Geen wijzingen' From 5a1e53818db5c20c4db0cc2e5223d328f4f8a003 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Mon, 15 Jun 2015 10:59:49 +1200 Subject: [PATCH 23/25] Update lithuanian translations --- lang/lt.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lang/lt.yml b/lang/lt.yml index 227386d46..e7c543d2e 100644 --- a/lang/lt.yml +++ b/lang/lt.yml @@ -191,9 +191,7 @@ lt: TEXT2: 'slaptažodžio atstatymo nuoroda' TEXT3: svetainei Form: - CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, - - perkraukite naršyklės langą ir bandykite vėl.' + CSRF_FAILED_MESSAGE: 'Iškilo techninė problema. Prašome paspausti mygtuką Atgal, perkraukite naršyklės langą ir bandykite vėl.' FIELDISREQUIRED: '{name} yra privalomas' SubmitBtnLabel: Vykdyti VALIDATIONCREDITNUMBER: 'Prašome įsitikinti, ar teisingai suvedėte kreditinės kortelės numerį {number}' @@ -260,8 +258,7 @@ lt: many_many_Members: Vartotojai GroupImportForm: Help1: '

    Importuoti vieną ar kelias grupes CSV formatu (kableliu atskirtos reikšmės). Rodyti detalesnį aprašymą

    ' - Help2: "
    \n\t

    Sudėtingesni pasirinkimai

    \n\t
      \n\t
    • Galimi stulpeliai: %s
    • \n\t
    • Esamos grupės yra surišamos su jų unikalia Code reikšme ir atnaujinamos duomenimis iš importuojamos bylos
    • \n\t
    • Grupių hierarchija gali būti sukurta naudojant ParentCode stulpelį.
    • \n\t
    • Leidimų kodai gali būti priskirti naudojant PermissionCode stulpelį. Esami leidimai nebus pakeisti.
    • \n\t
    \n\ -
    " + Help2: "
    \n

    Sudėtingesni pasirinkimai

    \n
      \n
    • Galimi stulpeliai: %s
    • \n
    • Esamos grupės yra surišamos su jų unikalia Code reikšme ir atnaujinamos duomenimis iš importuojamos bylos
    • \n
    • Grupių hierarchija gali būti sukurta naudojant ParentCode stulpelį.
    • \n
    • Leidimų kodai gali būti priskirti naudojant PermissionCode stulpelį. Esami leidimai nebus pakeisti.
    • \n
    \n
    " ResultCreated: 'Sukurta {count} grupių' ResultDeleted: 'Ištrinta %d grupių' ResultUpdated: 'Atnaujinta %d grupių' From 57071869f1a54822c68c90a301195334331d114b Mon Sep 17 00:00:00 2001 From: David Alexander Date: Mon, 15 Jun 2015 14:46:07 +1200 Subject: [PATCH 24/25] Update index.md url syntax fix for lesson 2 added lesson 17 --- docs/en/01_Tutorials/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/01_Tutorials/index.md b/docs/en/01_Tutorials/index.md index 54bb385ff..8403c74e9 100644 --- a/docs/en/01_Tutorials/index.md +++ b/docs/en/01_Tutorials/index.md @@ -11,7 +11,7 @@ These include video screencasts, written tutorials and code examples to get you * [How to set up a local development environment in SilverStripe](https://vimeo.com/108861537) * [Lesson 1: Creating your first theme](http://www.silverstripe.org/learn/lessons/creating-your-first-theme) -* [Lesson 2: Migrating static templates into your theme]http://www.silverstripe.org/learn/lessons/migrating-static-templates-into-your-theme) +* [Lesson 2: Migrating static templates into your theme](http://www.silverstripe.org/learn/lessons/migrating-static-templates-into-your-theme) * [Lesson 3: Adding dynamic content](http://www.silverstripe.org/learn/lessons/adding-dynamic-content) * [Lesson 4: Working with multiple templates](http://www.silverstripe.org/learn/lessons/working-with-multiple-templates) * [Lesson 5: The holder/page pattern](http://www.silverstripe.org/learn/lessons/the-holderpage-pattern) @@ -26,7 +26,7 @@ These include video screencasts, written tutorials and code examples to get you * [Lesson 14: Controller Actions/DataObjects as Pages](http://www.silverstripe.org/learn/lessons/controller-actions-dataobjects-as-pages) * [Lesson 15: Building a Search Form](http://www.silverstripe.org/learn/lessons/building-a-search-form) * [Lesson 16: Lists and Pagination](http://www.silverstripe.org/learn/lessons/lists-and-pagination) - +* [Lesson 17: Ajax Behaviour and Viewable Data](http://www.silverstripe.org/learn/lessons/ajax-behaviour-and-viewabledata) ## Help: If you get stuck From 6169bf2760366b0aebf255c973803621472ce1fb Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 16 Jun 2015 17:38:34 +0100 Subject: [PATCH 25/25] FIX No longer caching has_one after ID change --- model/DataObject.php | 4 ++++ tests/model/DataObjectTest.php | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/model/DataObject.php b/model/DataObject.php index 7f14288f8..09ac68d66 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -2316,6 +2316,10 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return DataObject $this */ public function setField($fieldName, $val) { + //if it's a has_one component, destroy the cache + if (substr($fieldName, -2) == 'ID') { + unset($this->components[substr($fieldName, 0, -2)]); + } // Situation 1: Passing an DBField if($val instanceof DBField) { $val->Name = $fieldName; diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index ecd3f2878..f4b7507ad 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -311,6 +311,7 @@ class DataObjectTest extends SapphireTest { public function testHasOneRelationship() { $team1 = $this->objFromFixture('DataObjectTest_Team', 'team1'); $player1 = $this->objFromFixture('DataObjectTest_Player', 'player1'); + $player2 = $this->objFromFixture('DataObjectTest_Player', 'player2'); // Add a captain to team 1 $team1->setField('CaptainID', $player1->ID); @@ -325,6 +326,14 @@ class DataObjectTest extends SapphireTest { 'Player 1 is the captain'); $this->assertEquals($team1->getComponent('Captain')->FirstName, 'Player 1', 'Player 1 is the captain'); + + $team1->CaptainID = $player2->ID; + $team1->write(); + + $this->assertEquals($player2->ID, $team1->Captain()->ID); + $this->assertEquals($player2->ID, $team1->getComponent('Captain')->ID); + $this->assertEquals('Player 2', $team1->Captain()->FirstName); + $this->assertEquals('Player 2', $team1->getComponent('Captain')->FirstName); } /**