From 88366bf3c8a8ff0609a42266b86ca1f7704f462e Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 7 Nov 2012 11:28:36 +1300 Subject: [PATCH 01/39] Adding additional test for populateDefaults() in DataObjectTest --- tests/model/DataObjectTest.php | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 829c4986f..fe0889584 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -767,8 +767,14 @@ class DataObjectTest extends SapphireTest { $obj = new DataObjectTest_Fixture(); $this->assertEquals( $obj->MyFieldWithDefault, - "Default Value", - "Defaults are populated for in-memory object from \$defaults array" + 'Default Value', + 'Defaults are populated for in-memory object from $defaults array' + ); + + $this->assertEquals( + $obj->MyFieldWithAltDefault, + 'Default Value', + 'Defaults are populated from overloaded populateDefaults() method' ); } @@ -1126,7 +1132,6 @@ class DataObjectTest extends SapphireTest { DataObject::get(); } - } @@ -1191,18 +1196,25 @@ class DataObjectTest_Fixture extends DataObject implements TestOnly { 'Data' => 'Varchar', 'Duplicate' => 'Varchar', 'DbObject' => 'Varchar', - - // Field with default - 'MyField' => 'Varchar', - + // Field types - "DateField" => "Date", - "DatetimeField" => "Datetime", + 'DateField' => 'Date', + 'DatetimeField' => 'Datetime', + + 'MyFieldWithDefault' => 'Varchar', + 'MyFieldWithAltDefault' => 'Varchar' ); static $defaults = array( - 'MyFieldWithDefault' => 'Default Value', + 'MyFieldWithDefault' => 'Default Value', ); + + public function populateDefaults() { + parent::populateDefaults(); + + $this->MyFieldWithAltDefault = 'Default Value'; + } + } class DataObjectTest_SubTeam extends DataObjectTest_Team implements TestOnly { @@ -1294,6 +1306,7 @@ class DataObjectTest_TeamComment extends DataObject { static $has_one = array( 'Team' => 'DataObjectTest_Team' ); + } DataObject::add_extension('DataObjectTest_Team', 'DataObjectTest_Team_Extension'); From 6ff5e8f39dd0a487655ae1338874873573e04000 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 7 Nov 2012 11:34:51 +1300 Subject: [PATCH 02/39] Adding ability to translate "Edit" text in GridFieldEditButton --- templates/Includes/GridFieldEditButton.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Includes/GridFieldEditButton.ss b/templates/Includes/GridFieldEditButton.ss index bfcc265b0..5ad45ea7a 100644 --- a/templates/Includes/GridFieldEditButton.ss +++ b/templates/Includes/GridFieldEditButton.ss @@ -1 +1 @@ -edit \ No newline at end of file +<% _t('EDIT', 'Edit') %> From aec59de955b5afb6dffb213e9c184fab3f9c25b5 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 7 Nov 2012 11:41:48 +1300 Subject: [PATCH 03/39] Adding title to CMSProfileController so translations get default --- admin/code/CMSProfileController.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/admin/code/CMSProfileController.php b/admin/code/CMSProfileController.php index d55dd311c..10ab33440 100644 --- a/admin/code/CMSProfileController.php +++ b/admin/code/CMSProfileController.php @@ -2,6 +2,9 @@ class CMSProfileController extends LeftAndMain { static $url_segment = 'myprofile'; + + static $menu_title = 'My Profile'; + static $required_permission_codes = false; public function index($request) { From 50d6296a7d9fbb5a37d4d507c144ac88880f4570 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 7 Nov 2012 11:42:08 +1300 Subject: [PATCH 04/39] Updating default en.yml translations --- lang/en.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lang/en.yml b/lang/en.yml index 3c1d2561b..f5b439933 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -1,6 +1,7 @@ en: AssetAdmin: - ALLOWEDEXTS: 'Allowed extensions' + ADDFILES: 'Add files' + EditOrgMenu: 'Edit & organize' NEWFOLDER: NewFolder AssetTableField: CREATED: 'First uploaded' @@ -238,6 +239,8 @@ en: Deleted: 'Deleted %s %s' Save: Save Saved: 'Saved %s %s' + GridFieldEditButton.ss: + EDIT: Edit GridFieldItemEditView.ss: 'Go back': 'Go back' Group: @@ -368,6 +371,7 @@ en: NEWPASSWORD: 'New Password' PASSWORD: Password PLURALNAME: Members + PROFILESAVESUCCESS: 'Successfully saved.' REMEMBERME: 'Remember me next time?' SINGULARNAME: Member SUBJECTPASSWORDCHANGED: 'Your password has been changed' From fdcd7a2e60d4e06fca5b09efd9eaa6af9ec04912 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Wed, 7 Nov 2012 17:23:36 +1300 Subject: [PATCH 05/39] Fixing performance of DataObject::custom_database_fields() On sites with lots of modules, and pages with plenty of database queries, DataObject::custom_database_fields() can be called thousands of times, and slow down page render times. This fixes it so the fields are cached by class in a static variable, and are cleared when reset() is called on the DataObject. --- model/DataObject.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/model/DataObject.php b/model/DataObject.php index 114808240..29bb0a701 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -147,7 +147,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public static $cache_has_own_table_field = array(); protected static $_cache_get_one; protected static $_cache_get_class_ancestry; - protected static $_cache_composite_fields = array(); + protected static $_cache_composite_fields = array(); + protected static $_cache_custom_database_fields = array(); protected static $_cache_field_labels = array(); /** @@ -226,8 +227,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return array Map of fieldname to specification, similiar to {@link DataObject::$db}. */ public static function custom_database_fields($class) { + if(isset(self::$_cache_custom_database_fields[$class])) { + return self::$_cache_custom_database_fields[$class]; + } + $fields = Config::inst()->get($class, 'db', Config::UNINHERITED); - + foreach(self::composite_fields($class, false) as $fieldName => $fieldClass) { // Remove the original fieldname, it's not an actual database column unset($fields[$fieldName]); @@ -244,8 +249,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if($hasOne) foreach(array_keys($hasOne) as $field) { $fields[$field . 'ID'] = 'ForeignKey'; } - - return (array)$fields; + + $output = (array) $fields; + + self::$_cache_custom_database_fields[$class] = $output; + + return $output; } /** @@ -2899,6 +2908,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity DataObject::$cache_has_own_table_field = array(); DataObject::$_cache_get_one = array(); DataObject::$_cache_composite_fields = array(); + DataObject::$_cache_custom_database_fields = array(); DataObject::$_cache_get_class_ancestry = array(); DataObject::$_cache_field_labels = array(); } From edb4ecd4d9cfeefb13a799b41f3d38a7578f0cdf Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 7 Nov 2012 13:05:48 +0100 Subject: [PATCH 06/39] Note about backtick in entwine shortcuts --- docs/en/reference/cms-architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/cms-architecture.md b/docs/en/reference/cms-architecture.md index c20fdf067..b6cdbe674 100644 --- a/docs/en/reference/cms-architecture.md +++ b/docs/en/reference/cms-architecture.md @@ -127,9 +127,9 @@ in jQuery.entwine, we're trying to reuse library code wherever possible. The most prominent example of this is the usage of [jQuery UI](http://jqueryui.com) for dialogs and buttons. -The CMS includes the jQuery.entwine inspector. Press Ctrl+` to bring down the inspector. +The CMS includes the jQuery.entwine inspector. Press Ctrl+` ("backtick") to bring down the inspector. You can then click on any element in the CMS to see which entwine methods are bound to -any particular element. +any particular element. ## JavaScript and CSS dependencies via Requirements and Ajax From f69c2b04953b9aceb35f9d0e74001169b4e48e59 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 8 Nov 2012 10:38:16 +1300 Subject: [PATCH 07/39] Improve performance of DataObject::db() with caching In a usual CMS request, DataObject::db() is called potentially thousands of times, calling Config::get() constantly for the same uninherited statics, which is slow. This improves performance by caching those into DataObject::$_cache_db --- model/DataObject.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/model/DataObject.php b/model/DataObject.php index 29bb0a701..e3418aba8 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -145,6 +145,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public static $cache_has_own_table = array(); public static $cache_has_own_table_field = array(); + protected static $_cache_db = array(); protected static $_cache_get_one; protected static $_cache_get_class_ancestry; protected static $_cache_composite_fields = array(); @@ -1578,23 +1579,28 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity continue; } + if(isset(self::$_cache_db[$class])) { + $dbItems = self::$_cache_db[$class]; + } else { + $dbItems = (array) Config::inst()->get($class, 'db', Config::UNINHERITED); + self::$_cache_db[$class] = $dbItems; + } + if($fieldName) { - $db = Config::inst()->get($class, 'db', Config::UNINHERITED); - - if(isset($db[$fieldName])) { - return $db[$fieldName]; + if(isset($dbItems[$fieldName])) { + return $dbItems[$fieldName]; } } else { - $newItems = (array)Config::inst()->get($class, 'db', Config::UNINHERITED); // Validate the data - foreach($newItems as $k => $v) { + foreach($dbItems as $k => $v) { if(!is_string($k) || is_numeric($k) || !is_string($v)) { user_error("$class::\$db has a bad entry: " . var_export($k,true). " => " . var_export($v,true) . ". Each map key should be a" . " property name, and the map value should be the property type.", E_USER_ERROR); } } - $items = isset($items) ? array_merge((array)$items, $newItems) : $newItems; + + $items = isset($items) ? array_merge((array) $items, $dbItems) : $dbItems; } } @@ -2906,6 +2912,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity public static function reset() { DataObject::$cache_has_own_table = array(); DataObject::$cache_has_own_table_field = array(); + DataObject::$_cache_db = array(); DataObject::$_cache_get_one = array(); DataObject::$_cache_composite_fields = array(); DataObject::$_cache_custom_database_fields = array(); From 68826357cccc287c59682d18a39f4138eb2957c4 Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Thu, 8 Nov 2012 21:28:05 +1300 Subject: [PATCH 08/39] BUG Fixing non-object on file upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upload::load() assumes that a parent Folder always exists for a file upload, but that's not always the case, and a non-object error is given if no parent folder. Check the folder exists first before getting the ID. --- filesystem/Upload.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/filesystem/Upload.php b/filesystem/Upload.php index ca87a8627..e3c3fa8d4 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -159,7 +159,7 @@ class Upload extends Controller { } if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) { - $this->file->ParentID = $parentFolder->ID; + $this->file->ParentID = $parentFolder ? $parentFolder->ID : 0; // This is to prevent it from trying to rename the file $this->file->Name = basename($relativeFilePath); $this->file->write(); From f0f5dcb966da9123e88dc1f8acc71065cfd7c8d8 Mon Sep 17 00:00:00 2001 From: Tim Klein Date: Fri, 9 Nov 2012 16:44:48 +1300 Subject: [PATCH 09/39] fixed mediaform urls in modeladmin see http://open.silverstripe.org/ticket/8013 --- forms/HtmlEditorField.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index 436c975d1..e577e4cef 100644 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -278,8 +278,8 @@ class HtmlEditorField_Toolbar extends RequestHandler { public function forTemplate() { return sprintf( '
', - Controller::join_links($this->controller->Link($this->name), 'LinkForm', 'forTemplate'), - Controller::join_links($this->controller->Link($this->name), 'MediaForm', 'forTemplate') + Controller::join_links($this->controller->Link(), $this->name, 'LinkForm', 'forTemplate'), + Controller::join_links($this->controller->Link(), $this->name, 'MediaForm', 'forTemplate') ); } From e43a4f22fb74913118a0e7686f44e11b2ef316a8 Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Fri, 9 Nov 2012 19:08:13 +1300 Subject: [PATCH 10/39] Update docs/en/installation/composer.md DOC Corrected a spelling error on the example of advanced usage. --- docs/en/installation/composer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/installation/composer.md b/docs/en/installation/composer.md index 27b5c6c08..a789e10b8 100644 --- a/docs/en/installation/composer.md +++ b/docs/en/installation/composer.md @@ -107,7 +107,7 @@ This is how you do it: * **Install the module as you would normally.** Use the regular composer function - there are no special flags to use a fork. Your fork will be used in place of the package version. - php composer.phar require silverstipre/advancedworklow + php composer.phar require silverstripe/advancedworkflow Composer will scan all of the repositories you list, collect meta-data about the packages within them, and use them in favour of the packages listed on packagist. To switch back to using the mainline version of the package, just remove your the `repositories` section from `composer.json` and run `php composer.phar update`. From 06ad5f5c696ac54c1c1b69b0970055b1d539de02 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 9 Nov 2012 10:13:01 +0100 Subject: [PATCH 11/39] Added Simplifie Chinese to i18n::$common_locales See https://github.com/silverstripe/silverstripe-translatable/issues/66 --- i18n/i18n.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/i18n/i18n.php b/i18n/i18n.php index f03b44850..98f1c6904 100644 --- a/i18n/i18n.php +++ b/i18n/i18n.php @@ -654,6 +654,7 @@ class i18n extends Object implements TemplateGlobalProvider { 'zh_yue' => array('Chinese (Cantonese)', '廣東話 [广东话]'), 'zh_cmn' => array('Chinese (Mandarin)', '普通話 [普通话]'), 'hr' => array('Croatian', 'Hrvatski'), + 'zh' => array('Chinese','中国的'), 'cs' => array('Czech', 'čeština'), 'cy' => array('Welsh', 'Welsh/Cymraeg'), 'da' => array('Danish', 'dansk'), @@ -744,6 +745,7 @@ class i18n extends Object implements TemplateGlobalProvider { 'bn_BD' => array('Bengali', 'বাংলা'), 'bg_BG' => array('Bulgarian', 'български'), 'ca_ES' => array('Catalan', 'català'), + 'zh_CN' => array('Chinese','中国的'), 'zh_yue' => array('Chinese (Cantonese)', '廣東話 [广东话]'), 'zh_cmn' => array('Chinese (Mandarin)', '普通話 [普通话]'), 'hr_HR' => array('Croatian', 'Hrvatski'), From ea2dc9da0e2fa2d47adcea6c8aefd7e206c96372 Mon Sep 17 00:00:00 2001 From: Loz Calver Date: Wed, 24 Oct 2012 14:42:48 +0100 Subject: [PATCH 12/39] ENHANCEMENT: Add ability to change URL for SS logo in CMS Menu --- admin/code/LeftAndMain.php | 24 ++++++++++++++++++++ admin/templates/Includes/LeftAndMain_Menu.ss | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index fff104714..80993759e 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -1370,6 +1370,30 @@ class LeftAndMain extends Controller implements PermissionProvider { public function SiteConfig() { return (class_exists('SiteConfig')) ? SiteConfig::current_site_config() : null; } + + /** + * The href for the anchor on the Silverstripe logo. + * Set by calling LeftAndMain::set_application_link() + * + * @var String + */ + static $application_link = 'http://www.silverstripe.org/'; + + /** + * Sets the href for the anchor on the Silverstripe logo in the menu + * + * @param String $link + */ + public static function set_application_link($link) { + self::$application_link = $link; + } + + /** + * @return String + */ + public function ApplicationLink() { + return self::$application_link; + } /** * The application name. Customisable by calling diff --git a/admin/templates/Includes/LeftAndMain_Menu.ss b/admin/templates/Includes/LeftAndMain_Menu.ss index 815c0a507..469bb40a9 100644 --- a/admin/templates/Includes/LeftAndMain_Menu.ss +++ b/admin/templates/Includes/LeftAndMain_Menu.ss @@ -1,7 +1,7 @@
+ +## Updating dependencies + +Except for the control code of the Voyager space probe, every piece of code in the universe gets updated from time to time. SilverStripe modules are no exception. How do you download these updates into your site? + +To get the latest updates of the modules in your project, run this command: + + composer update + +Updates to the required modules will be installed, and the `composer.lock` file will get updated with the specific commits of each of those. + +## Deploying projects with Composer + +When deploying projects with composer, you could just push the code and run `composer update`. However, this is risky. In particular, if you were referencing development dependencies and a change was made between your testing and your depoyment to production, you would end up deploying untested code. Not cool! + +The `composer.lock` file helps with this. It references the specific commits that have been checked out, rather than the version string. You can run `composer install` to install dependencies from this rather than `composer.json`. + +So, your deployment process, as it relates to Composer, should be as follows: + + * Run `composer update` on your development version before you start whatever testing you have planned. Perform all the necessary testing. + + * Check `composer.lock` into your repository. + + * Deploy your project code base, using the deployment tool of your choice. + + * Run the following command on your production version. + + composer install # Advanced usage From cf11892ab201e9a6c63b535b58e0d44bafc63db2 Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Tue, 13 Nov 2012 22:32:31 +1300 Subject: [PATCH 17/39] Updated license in readme to include year 2012 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a1f9833df..a92d3c8bf 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ For other ways to contribute, see the [code contribution guidelines](http://doc. ## License ## - Copyright (c) 2007-2011, SilverStripe Limited - www.silverstripe.com + Copyright (c) 2007-2012, SilverStripe Limited - www.silverstripe.com All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: From 84692bf4991c6696ceaf4509cf4e07074f8cddea Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Tue, 13 Nov 2012 22:57:02 +1300 Subject: [PATCH 18/39] Corrections to composer docs for update and install --- docs/en/installation/composer.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/installation/composer.md b/docs/en/installation/composer.md index 1ebbd681a..23fdeeb72 100644 --- a/docs/en/installation/composer.md +++ b/docs/en/installation/composer.md @@ -24,7 +24,7 @@ Or [download composer.phar](http://getcomposer.org/composer.phar) manually, and You can then run Composer commands by calling `composer`. For example: - compser help + composer help
It is also possible to keep `composer.phar` out of your path, for example, to put it in your project root. Every command would then start with `php composer.phar` instead of `composer`. This is handy if need to keep your installation isolated from the rest of your computer's set-up, but we recommend putting composer into the path for most people. @@ -45,7 +45,7 @@ browser, and the installation process will be completed. ## Adding modules to your project -Composer isn't only used to download SilverStripe CMS: it can also be used to manage all the modules. Installing a module can be done with the following command: +Composer isn't only used to download SilverStripe CMS, it can also be used to manage all SilverStripe modules. Installing a module can be done with the following command: composer require silverstripe/forum:* @@ -55,7 +55,7 @@ This command has two parts. First is `silverstripe/forum`. This is the name of This will return a list of package names of the forum `vendor/package`. If you prefer, you can search for pacakges on [packagist.org](https://packagist.org/search/?q=silverstripe). -The second part, `*`, is a version string. `*` is a good default: it will give you the latest version that works with the other modules you have installed. Alternatively, you can specificy a specific version, or a constraint such as `>=3.0`. For more information, read the [Composer documentation](http://getcomposer.org/doc/01-basic-usage.md#the-require-key). +The second part after the colon, `*`, is a version string. `*` is a good default: it will give you the latest version that works with the other modules you have installed. Alternatively, you can specificy a specific version, or a constraint such as `>=3.0`. For more information, read the [Composer documentation](http://getcomposer.org/doc/01-basic-usage.md#the-require-key).
`master` is not a legal version string - it's a branch name. These are different things. The version string that would get you the branch is `dev-master`. The version string that would get you a numeric branch is a little different. The version string for the `3.0` branch is `3.0.x-dev`. But, frankly, maybe you should just use `*`. @@ -63,7 +63,7 @@ The second part, `*`, is a version string. `*` is a good default: it will give ## Updating dependencies -Except for the control code of the Voyager space probe, every piece of code in the universe gets updated from time to time. SilverStripe modules are no exception. How do you download these updates into your site? +Except for the control code of the Voyager space probe, every piece of code in the universe gets updated from time to time. SilverStripe modules are no exception. To get the latest updates of the modules in your project, run this command: @@ -174,7 +174,7 @@ Open `composer.json`, and find the module's `require`. Then put `as (core versi ... } -What is means is that when the `myproj` branch is checked out into a project, this will satisfy any dependencies that 3.0.x-dev would meet. So, if another module has `"silverstripe/framework": ">=3.0.0"` in its dependency list, it won't get a conflict. +What this means is that when the `myproj` branch is checked out into a project, this will satisfy any dependencies that 3.0.x-dev would meet. So, if another module has `"silverstripe/framework": ">=3.0.0"` in its dependency list, it won't get a conflict. Both the version and the alias are specified as Composer versions, not branch names. For the relationship between branch/tag names and Composer vesrions, read [the relevant Composer documentation](http://getcomposer.org/doc/02-libraries.md#specifying-the-version). From c6fcb080a90f340496e20b252d3f1f9d37eee635 Mon Sep 17 00:00:00 2001 From: stojg Date: Thu, 15 Nov 2012 15:49:03 +1300 Subject: [PATCH 19/39] BUG Video embed from Add Media Feature no longer works (open #8033) Since the tinymce upgrade from 3.5.6 to 3.5.7 it seems like data attributes are forbidden on tags. This fix tells tinymce to allow data* properties on img tags --- admin/_config.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/_config.php b/admin/_config.php index 1297c918e..f446d8157 100644 --- a/admin/_config.php +++ b/admin/_config.php @@ -13,7 +13,7 @@ HtmlEditorConfig::get('cms')->setOptions(array( 'use_native_selects' => false, 'valid_elements' => "@[id|class|style|title],a[id|rel|rev|dir|tabindex|accesskey|type|name|href|target|title" . "|class],-strong/-b[class],-em/-i[class],-strike[class],-u[class],#p[id|dir|class|align|style],-ol[class]," - . "-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align]," + . "-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align|data*]," . "-sub[class],-sup[class],-blockquote[dir|class]," . "-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|dir|id|style]," . "-tr[id|dir|class|rowspan|width|height|align|valign|bgcolor|background|bordercolor|style]," @@ -25,7 +25,7 @@ HtmlEditorConfig::get('cms')->setOptions(array( . "-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|dir|class|align|style],hr[class]," . "dd[id|class|title|dir],dl[id|class|title|dir],dt[id|class|title|dir],@[id,style,class]", 'extended_valid_elements' => "img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name" - . "|usemap],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling]," + . "|usemap|data*],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling]," . "object[width|height|data|type],param[name|value],map[class|name|id],area[shape|coords|href|target|alt]", 'spellchecker_rpc_url' => THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php' )); From 0a580deb3f78696cc458cb2a35099146578547a2 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 8 Nov 2012 17:16:43 +0100 Subject: [PATCH 20/39] Documenting PHPUnit install through composer --- docs/en/howto/phpunit-configuration.md | 8 ++++++-- docs/en/topics/testing/index.md | 27 +++++++++++--------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/en/howto/phpunit-configuration.md b/docs/en/howto/phpunit-configuration.md index be9841827..197a3ec58 100644 --- a/docs/en/howto/phpunit-configuration.md +++ b/docs/en/howto/phpunit-configuration.md @@ -21,8 +21,8 @@ If you're using [phpUnderControl](http://phpundercontrol.org/) or a similar tool you will most likely need the `--log-junit` and `--coverage-xml` flags that are not available through `sake`. All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/manual/current/en/textui.html). - -## Usage of "phpunit" executable +phpunit +## Usage of "" executable * `phpunit`: Runs all tests in all folders * `phpunit framework/tests/`: Run all tests of the framework module @@ -30,6 +30,10 @@ All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/ * `phpunit framework/tests/filesystem/FolderTest.php`: Run a single test * `phpunit framework/tests '' flush=all`: Run tests with optional `$_GET` parameters (you need an empty second argument) +Note that if you have installed PHPUnit through Composer rather than PEAR +([instructions](/topics/installation/composer)), the binary will be placed +in `vendor/bin/phpunit` instead of `phpunit`. + ## Coverage reports * `phpunit --coverage-html assets/coverage-report`: Generate coverage report for the whole project diff --git a/docs/en/topics/testing/index.md b/docs/en/topics/testing/index.md index 6c833904b..6e2f4ea10 100644 --- a/docs/en/topics/testing/index.md +++ b/docs/en/topics/testing/index.md @@ -16,31 +16,25 @@ fundamental concepts that we build on in this documentation. If you're more familiar with unit testing, but want a refresher of some of the concepts and terminology, you can browse the [Testing Glossary](#glossary). - To get started now, follow the installation instructions below, and check [Troubleshooting](/topics/testing/testing-guide-troubleshooting) in case you run into any problems. ## Installation -The framework has a required dependency on [PHPUnit](http://www.phpunit.de/) and an optional dependency on -[SimpleTest](http://simpletest.org/), the two premiere PHP testing frameworks. +Unit tests are not included in the normal SilverStripe downloads, +you are expected to work with local git repositories +([installation instructions](/topics/installation/composer)). -To run SilverStripe tests, you'll need to be able to access PHPUnit on your include path. First, you'll need to make sure -that you have the PEAR command line client installed. To test this out, type `pear help` at your prompt. You should -see a bunch of generic PEAR info. If it's not installed, you'll need to set it up first (see: [Getting Started with -PEAR](http://www.sitepoint.com/article/getting-started-with-pear/)) or else manually install PHPUnit (see: [Installation -instructions](http://www.phpunit.de/pocket_guide/3.3/en/installation.html)). +Once you've got the project up and running, +check out the additional requirements to run unit tests: -The PHPUnit installation via PEAR is very straightforward. -You might have to perform the following commands as root or super user (sudo). + composer update --dev -We need a specific version of PHPUnit (3.3.x), as 3.4 or higher breaks our test runner (see [#4573](http://open.silverstripe.com/ticket/4573)) +The will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency, +which is the framework we're building our unit tests on. -At your prompt, type the following commands: - - pear channel-discover pear.phpunit.de - pear channel-discover pear.symfony-project.com - pear install phpunit/PHPUnit +Alternatively, you can check out phpunit globally via the PEAR packanage manager +([instructions](https://github.com/sebastianbergmann/phpunit/)). ## Running Tests @@ -57,6 +51,7 @@ their own: /path/to/project$ sake dev/tests/all +Alternatively, you can use the `phpunit` binary to run tests, see [PHPUnit Configuration](phpunit-configuration) for details. ### Partial Test Runs From e4d71c2a201f75793799a142ac49a7010d7ff420 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 8 Nov 2012 17:46:33 +0100 Subject: [PATCH 21/39] Add Composer autoloader Mainly to get PHPUnit going as a composer requirement rather than through PEAR (which is easier to set up). --- core/Core.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/Core.php b/core/Core.php index ec24ea678..c58ae9729 100644 --- a/core/Core.php +++ b/core/Core.php @@ -275,10 +275,16 @@ $flush = (isset($_GET['flush']) || isset($_REQUEST['url']) && ( )); $manifest = new SS_ClassManifest(BASE_PATH, false, $flush); +// Register SilverStripe's class map autoload $loader = SS_ClassLoader::instance(); $loader->registerAutoloader(); $loader->pushManifest($manifest); +// Fall back to Composer's autoloader (e.g. for PHPUnit), if composer is used +if(file_exists(BASE_PATH . '/vendor/autoload.php')) { + require_once BASE_PATH . '/vendor/autoload.php'; +} + // Now that the class manifest is up, load the configuration $configManifest = new SS_ConfigManifest(BASE_PATH, false, $flush); Config::inst()->pushConfigManifest($configManifest); From 4fcdfe8d64ec13584df88f8102a22cb4bd57c8a9 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 15 Nov 2012 14:15:46 +0100 Subject: [PATCH 22/39] Testing docs recommend "phpunit" over "sake" - Moved some docs around to reflect this change - Described how to symlink from vendor/bin/phpunit - Added note about browser-runs not being recommended - Added more examples on how to run through "sake", to complement the existing descriptions for "phpunit" --- docs/en/howto/phpunit-configuration.md | 75 ++++------------- docs/en/topics/testing/index.md | 107 ++++++++++++++++++------- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/docs/en/howto/phpunit-configuration.md b/docs/en/howto/phpunit-configuration.md index 197a3ec58..fde20de6d 100644 --- a/docs/en/howto/phpunit-configuration.md +++ b/docs/en/howto/phpunit-configuration.md @@ -3,55 +3,26 @@ This guide helps you to run [PHPUnit](http://phpunit.de) tests in your SilverStripe project. See "[Testing](/topics/testing)" for an overview on how to create unit tests. -## Should I execute through "sake dev/tests" or "phpunit"? - -Short answer: Both are valid ways. - -The `sake` executable that comes with SilverStripe can trigger a customized -"[api:TestRunner]" class that handles the PHPUnit configuration and output formatting. -It's tyically invoked to run all tests through `sake dev/tests/all`, -a single test with `sake dev/tests/MyTestClass`, or tests for a module with `sake dev/tests/module/mymodulename`. -While the custom test runner a handy tool, its also more limited than using `phpunit` directly, -particularly around formatting test output. - -The `phpunit` executable uses a SilverStripe bootstrapper to autoload classes, -but handles its own test class retrieval, output formatting and other configuration. -It can format output in common structured formats used by "continuous integration" servers. -If you're using [phpUnderControl](http://phpundercontrol.org/) or a similar tool, -you will most likely need the `--log-junit` and `--coverage-xml` flags that are not available through `sake`. - -All command-line arguments are documented on [phpunit.de](http://www.phpunit.de/manual/current/en/textui.html). -phpunit -## Usage of "" executable - - * `phpunit`: Runs all tests in all folders - * `phpunit framework/tests/`: Run all tests of the framework module - * `phpunit framework/tests/filesystem`: Run all filesystem tests within the framework module - * `phpunit framework/tests/filesystem/FolderTest.php`: Run a single test - * `phpunit framework/tests '' flush=all`: Run tests with optional `$_GET` parameters (you need an empty second argument) - -Note that if you have installed PHPUnit through Composer rather than PEAR -([instructions](/topics/installation/composer)), the binary will be placed -in `vendor/bin/phpunit` instead of `phpunit`. - ## Coverage reports +PHPUnit can generate code coverage reports for you ([docs](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html)): + * `phpunit --coverage-html assets/coverage-report`: Generate coverage report for the whole project * `phpunit --coverage-html assets/coverage-report mysite/tests/`: Generate coverage report for the "mysite" module -## Customizing phpunit.xml.dist +Typically, only your own custom PHP code in your project should be regarded when +producing these reports. Here's how you would exclude some `thirdparty/` directories: -The `phpunit` executable can be configured by commandline arguments or through an XML file. -File-based configuration has the advantage of enforcing certain rules across -test executions (e.g. excluding files from code coverage reports), and of course this -information can be version controlled and shared with other team members. - -SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point. -Copy the file into a new `phpunit.xml` and customize to your needs - PHPUnit will auto-detect -its existence, and prioritize it over the default file. - -There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in [PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)). -For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below). + + + framework/dev/ + framework/thirdparty/ + cms/thirdparty/ + + + mysite/thirdparty/ + + ## Running unit and functional tests separately @@ -80,24 +51,6 @@ You can run with this XML configuration simply by invoking `phpunit --configurat The same effect can be achieved with the `--group` argument and some PHPDoc (see [phpunit.de](http://www.phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.groups)). -## Adding/removing files for code coverage reports - -Not all PHP code in your project should be regarded when producing [code coverage reports](http://www.phpunit.de/manual/current/en/code-coverage-analysis.html). -This applies for all thirdparty code - - - - framework/dev/ - framework/thirdparty/ - cms/thirdparty/ - - - mysite/thirdparty/ - - - -See [phpunit.de](http://www.phpunit.de/manual/current/en/appendixes.configuration.html#appendixes.configuration.blacklist-whitelist) for more information. - ## Speeding up your test execution with the SQLite3 module Test execution can easily take a couple of minutes for a full run, diff --git a/docs/en/topics/testing/index.md b/docs/en/topics/testing/index.md index 6e2f4ea10..9d6ae83d6 100644 --- a/docs/en/topics/testing/index.md +++ b/docs/en/topics/testing/index.md @@ -21,6 +21,8 @@ To get started now, follow the installation instructions below, and check ## Installation +### Via Composer + Unit tests are not included in the normal SilverStripe downloads, you are expected to work with local git repositories ([installation instructions](/topics/installation/composer)). @@ -32,45 +34,74 @@ check out the additional requirements to run unit tests: The will install (among other things) the [PHPUnit](http://www.phpunit.de/) dependency, which is the framework we're building our unit tests on. +Composer installs it alongside the required PHP classes into the `vendor/bin/` directory. +You can either use it through its full path (`vendor/bin/phpunit`), or symlink it +into the root directory of your website: + + ln -s vendor/bin/phpunit phpunit + +### Via PEAR Alternatively, you can check out phpunit globally via the PEAR packanage manager ([instructions](https://github.com/sebastianbergmann/phpunit/)). + pear config-set auto_discover 1 + pear install pear.phpunit.de/PHPUnit + ## Running Tests +### Via the "phpunit" Binary on Command Line + +The `phpunit` binary should be used from the root directory of your website. + + # Runs all tests defined in phpunit.xml + phpunit + + # Run all tests of a specific module + phpunit framework/tests/ + + # Run specific tests within a specific module + phpunit framework/tests/filesystem + + # Run a specific test + phpunit framework/tests/filesystem/FolderTest.php + + # Run tests with optional `$_GET` parameters (you need an empty second argument) + phpunit framework/tests '' flush=all + +All command-line arguments are documented on +[phpunit.de](http://www.phpunit.de/manual/current/en/textui.html). + +### Via the "sake" Wrapper on Command Line + +The [sake](/topics/commandline) executable that comes with SilverStripe can trigger a customized +"[api:TestRunner]" class that handles the PHPUnit configuration and output formatting. +While the custom test runner a handy tool, its also more limited than using `phpunit` directly, +particularly around formatting test output. + + # Run all tests + sake dev/tests/all + + # Run all tests of a specific module (comma-separated) + sake dev/tests/module/framework,cms + + # Run specific tests (comma-separated) + sake dev/tests/FolderTest,OtherTest + + # Run tests with optional `$_GET` parameters + sake dev/tests/all flush=all + + # Skip some tests + sake dev/tests/all SkipTests=MySkippedTest + ### Via Web Browser -Go to the main test URL which will give you options for running various available test suites or individual tests on -their own: +Executing tests from the command line is recommended, since it most closely reflects +test runs in any automated testing environments. If for some reason you don't have +access to the command line, you can also run tests through the browser. http://localhost/dev/tests -### Via Command Line - -`cd` to the root level of your project and run [sake](/topics/commandline) (SilverStripe Make) to execute the tests: - - /path/to/project$ sake dev/tests/all - -Alternatively, you can use the `phpunit` binary to run tests, see [PHPUnit Configuration](phpunit-configuration) for details. - -### Partial Test Runs - - -Run specific tests: - - dev/tests/MyTest,MyOtherTest - - -Run all tests in a module folder, e.g. "framework" - - dev/tests/module/ - - -Skip certain tests - - dev/tests/all SkipTests=MySkippedTest - - ## Writing Tests Tests are written by creating subclasses of `[api:SapphireTest]`. You should put tests for your site in the @@ -87,14 +118,30 @@ You will generally write two different kinds of test classes. Some people may note that we have used the same naming convention as Ruby on Rails. -## How To - Tutorials and recipes for creating tests using the SilverStripe framework: * **[Create a SilverStripe Test](/topics/testing/create-silverstripe-test)** * **[Create a Functional Test](/topics/testing/create-functional-test)** * **[Test Outgoing Email Sending](/topics/testing/email-sending)** +## Configuration + +### phpunit.xml + +The `phpunit` executable can be configured by commandline arguments or through an XML file. +File-based configuration has the advantage of enforcing certain rules across +test executions (e.g. excluding files from code coverage reports), and of course this +information can be version controlled and shared with other team members. + +**Note: This doesn't apply for running tests through the "sake" wrapper** + +SilverStripe comes with a default `phpunit.xml.dist` that you can use as a starting point. +Copy the file into a new `phpunit.xml` and customize to your needs - PHPUnit will auto-detect +its existence, and prioritize it over the default file. + +There's nothing stopping you from creating multiple XML files (see the `--configuration` flag in [PHPUnit documentation](http://www.phpunit.de/manual/current/en/textui.html)). +For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-tests.xml` file (see below). + ## Glossary {#glossary} **Assertion:** A predicate statement that must be true when a test runs. From b6017a7c902e5e66835dfbd4884924a56ac9a370 Mon Sep 17 00:00:00 2001 From: Andrew O'Neil Date: Mon, 12 Nov 2012 16:25:55 +1300 Subject: [PATCH 23/39] BUGFIX: ArrayList now discards keys of the array passed in and keeps the numerically indexed array sequential. This fixes FirstLast and EvenOdd in templates, and makes ArrayList more consistent, as several methods already discarded the keys. --- model/ArrayList.php | 26 +++++-- tests/model/ArrayListTest.php | 124 +++++++++++++++++----------------- 2 files changed, 81 insertions(+), 69 deletions(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index 4335b405c..0a4f63b70 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -19,7 +19,7 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta * @param array $items - an initial array to fill this object with */ public function __construct(array $items = array()) { - $this->items = $items; + $this->items = array_values($items); parent::__construct(); } @@ -138,9 +138,14 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta * @param mixed $item */ public function remove($item) { + $renumberKeys = false; foreach ($this->items as $key => $value) { - if ($item === $value) unset($this->items[$key]); + if ($item === $value) { + $renumberKeys = true; + unset($this->items[$key]); + } } + if($renumberKeys) $this->items = array_values($this->items); } /** @@ -177,16 +182,20 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta */ public function removeDuplicates($field = 'ID') { $seen = array(); + $renumberKeys = false; foreach ($this->items as $key => $item) { $value = $this->extractValue($item, $field); if (array_key_exists($value, $seen)) { + $renumberKeys = true; unset($this->items[$key]); } $seen[$value] = true; } + + if($renumberKeys) $this->items = array_values($this->items); } /** @@ -474,7 +483,6 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta } } - $itemsToKeep = array(); $hitsRequiredToRemove = count($removeUs); $matches = array(); @@ -489,13 +497,17 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta } $keysToRemove = array_keys($matches,$hitsRequiredToRemove); - // TODO 3.1: This currently mutates existing array - $list = /* clone */ $this; - foreach($keysToRemove as $itemToRemoveIdx){ - $list->remove($this->items[$itemToRemoveIdx]); + $itemsToKeep = array(); + foreach($this->items as $key => $value) { + if(!in_array($key, $keysToRemove)) { + $itemsToKeep[] = $value; + } } + // TODO 3.1: This currently mutates existing array + $list = /* clone */ $this; + $list->items = $itemsToKeep; return $list; } diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index 4a73a376e..abcf662be 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -428,15 +428,15 @@ class ArrayListTest extends SapphireTest { */ public function testSimpleExclude() { $list = new ArrayList(array( - 0=>array('Name' => 'Steve'), - 1=>array('Name' => 'Bob'), - 2=>array('Name' => 'John') + array('Name' => 'Steve'), + array('Name' => 'Bob'), + array('Name' => 'John') )); $list->exclude('Name', 'Bob'); $expected = array( - 0=>array('Name' => 'Steve'), - 2=>array('Name' => 'John') + array('Name' => 'Steve'), + array('Name' => 'John') ); $this->assertEquals(2, $list->count()); $this->assertEquals($expected, $list->toArray(), 'List should not contain Bob'); @@ -466,12 +466,12 @@ class ArrayListTest extends SapphireTest { */ public function testSimpleExcludeWithArray() { $list = new ArrayList(array( - 0=>array('Name' => 'Steve'), - 1=>array('Name' => 'Bob'), - 2=>array('Name' => 'John') + array('Name' => 'Steve'), + array('Name' => 'Bob'), + array('Name' => 'John') )); $list->exclude('Name', array('Steve','John')); - $expected = array(1=>array('Name' => 'Bob')); + $expected = array(array('Name' => 'Bob')); $this->assertEquals(1, $list->count()); $this->assertEquals($expected, $list->toArray(), 'List should only contain Bob'); } @@ -481,16 +481,16 @@ class ArrayListTest extends SapphireTest { */ public function testExcludeWithTwoArrays() { $list = new ArrayList(array( - 0=>array('Name' => 'Bob' , 'Age' => 21), - 1=>array('Name' => 'Bob' , 'Age' => 32), - 2=>array('Name' => 'John', 'Age' => 21) + array('Name' => 'Bob' , 'Age' => 21), + array('Name' => 'Bob' , 'Age' => 32), + array('Name' => 'John', 'Age' => 21) )); $list->exclude(array('Name' => 'Bob', 'Age' => 21)); $expected = array( - 1=>array('Name' => 'Bob', 'Age' => 32), - 2=>array('Name' => 'John', 'Age' => 21) + array('Name' => 'Bob', 'Age' => 32), + array('Name' => 'John', 'Age' => 21) ); $this->assertEquals(2, $list->count()); @@ -502,23 +502,23 @@ class ArrayListTest extends SapphireTest { */ public function testMultipleExclude() { $list = new ArrayList(array( - 0 => array('Name' => 'bob', 'Age' => 10), - 1 => array('Name' => 'phil', 'Age' => 11), - 2 => array('Name' => 'bob', 'Age' => 12), - 3 => array('Name' => 'phil', 'Age' => 12), - 4 => array('Name' => 'bob', 'Age' => 14), - 5 => array('Name' => 'phil', 'Age' => 14), - 6 => array('Name' => 'bob', 'Age' => 16), - 7 => array('Name' => 'phil', 'Age' => 16) + array('Name' => 'bob', 'Age' => 10), + array('Name' => 'phil', 'Age' => 11), + array('Name' => 'bob', 'Age' => 12), + array('Name' => 'phil', 'Age' => 12), + array('Name' => 'bob', 'Age' => 14), + array('Name' => 'phil', 'Age' => 14), + array('Name' => 'bob', 'Age' => 16), + array('Name' => 'phil', 'Age' => 16) )); $list->exclude(array('Name'=>array('bob','phil'),'Age'=>array(10, 16))); $expected = array( - 1 => array('Name' => 'phil', 'Age' => 11), - 2 => array('Name' => 'bob', 'Age' => 12), - 3 => array('Name' => 'phil', 'Age' => 12), - 4 => array('Name' => 'bob', 'Age' => 14), - 5 => array('Name' => 'phil', 'Age' => 14), + array('Name' => 'phil', 'Age' => 11), + array('Name' => 'bob', 'Age' => 12), + array('Name' => 'phil', 'Age' => 12), + array('Name' => 'bob', 'Age' => 14), + array('Name' => 'phil', 'Age' => 14), ); $this->assertEquals($expected, $list->toArray()); } @@ -528,26 +528,26 @@ class ArrayListTest extends SapphireTest { */ public function testMultipleExcludeNoMatch() { $list = new ArrayList(array( - 0 => array('Name' => 'bob', 'Age' => 10), - 1 => array('Name' => 'phil', 'Age' => 11), - 2 => array('Name' => 'bob', 'Age' => 12), - 3 => array('Name' => 'phil', 'Age' => 12), - 4 => array('Name' => 'bob', 'Age' => 14), - 5 => array('Name' => 'phil', 'Age' => 14), - 6 => array('Name' => 'bob', 'Age' => 16), - 7 => array('Name' => 'phil', 'Age' => 16) + array('Name' => 'bob', 'Age' => 10), + array('Name' => 'phil', 'Age' => 11), + array('Name' => 'bob', 'Age' => 12), + array('Name' => 'phil', 'Age' => 12), + array('Name' => 'bob', 'Age' => 14), + array('Name' => 'phil', 'Age' => 14), + array('Name' => 'bob', 'Age' => 16), + array('Name' => 'phil', 'Age' => 16) )); $list->exclude(array('Name'=>array('bob','phil'),'Age'=>array(10, 16),'Bananas'=>true)); $expected = array( - 0 => array('Name' => 'bob', 'Age' => 10), - 1 => array('Name' => 'phil', 'Age' => 11), - 2 => array('Name' => 'bob', 'Age' => 12), - 3 => array('Name' => 'phil', 'Age' => 12), - 4 => array('Name' => 'bob', 'Age' => 14), - 5 => array('Name' => 'phil', 'Age' => 14), - 6 => array('Name' => 'bob', 'Age' => 16), - 7 => array('Name' => 'phil', 'Age' => 16) + array('Name' => 'bob', 'Age' => 10), + array('Name' => 'phil', 'Age' => 11), + array('Name' => 'bob', 'Age' => 12), + array('Name' => 'phil', 'Age' => 12), + array('Name' => 'bob', 'Age' => 14), + array('Name' => 'phil', 'Age' => 14), + array('Name' => 'bob', 'Age' => 16), + array('Name' => 'phil', 'Age' => 16) ); $this->assertEquals($expected, $list->toArray()); } @@ -557,29 +557,29 @@ class ArrayListTest extends SapphireTest { */ public function testMultipleExcludeThreeArguments() { $list = new ArrayList(array( - 0 => array('Name' => 'bob', 'Age' => 10, 'HasBananas'=>false), - 1 => array('Name' => 'phil','Age' => 11, 'HasBananas'=>true), - 2 => array('Name' => 'bob', 'Age' => 12, 'HasBananas'=>true), - 3 => array('Name' => 'phil','Age' => 12, 'HasBananas'=>true), - 4 => array('Name' => 'bob', 'Age' => 14, 'HasBananas'=>false), - 4 => array('Name' => 'ann', 'Age' => 14, 'HasBananas'=>true), - 5 => array('Name' => 'phil','Age' => 14, 'HasBananas'=>false), - 6 => array('Name' => 'bob', 'Age' => 16, 'HasBananas'=>false), - 7 => array('Name' => 'phil','Age' => 16, 'HasBananas'=>true), - 8 => array('Name' => 'clair','Age' => 16, 'HasBananas'=>true) + array('Name' => 'bob', 'Age' => 10, 'HasBananas'=>false), + array('Name' => 'phil','Age' => 11, 'HasBananas'=>true), + array('Name' => 'bob', 'Age' => 12, 'HasBananas'=>true), + array('Name' => 'phil','Age' => 12, 'HasBananas'=>true), + array('Name' => 'bob', 'Age' => 14, 'HasBananas'=>false), + array('Name' => 'ann', 'Age' => 14, 'HasBananas'=>true), + array('Name' => 'phil','Age' => 14, 'HasBananas'=>false), + array('Name' => 'bob', 'Age' => 16, 'HasBananas'=>false), + array('Name' => 'phil','Age' => 16, 'HasBananas'=>true), + array('Name' => 'clair','Age' => 16, 'HasBananas'=>true) )); $list->exclude(array('Name'=>array('bob','phil'),'Age'=>array(10, 16),'HasBananas'=>true)); $expected = array( - 0 => array('Name' => 'bob', 'Age' => 10, 'HasBananas'=>false), - 1 => array('Name' => 'phil','Age' => 11, 'HasBananas'=>true), - 2 => array('Name' => 'bob', 'Age' => 12, 'HasBananas'=>true), - 3 => array('Name' => 'phil','Age' => 12, 'HasBananas'=>true), - 4 => array('Name' => 'bob', 'Age' => 14, 'HasBananas'=>false), - 4 => array('Name' => 'ann', 'Age' => 14, 'HasBananas'=>true), - 5 => array('Name' => 'phil','Age' => 14, 'HasBananas'=>false), - 6 => array('Name' => 'bob', 'Age' => 16, 'HasBananas'=>false), - 8 => array('Name' => 'clair','Age' => 16, 'HasBananas'=>true) + array('Name' => 'bob', 'Age' => 10, 'HasBananas'=>false), + array('Name' => 'phil','Age' => 11, 'HasBananas'=>true), + array('Name' => 'bob', 'Age' => 12, 'HasBananas'=>true), + array('Name' => 'phil','Age' => 12, 'HasBananas'=>true), + array('Name' => 'bob', 'Age' => 14, 'HasBananas'=>false), + array('Name' => 'ann', 'Age' => 14, 'HasBananas'=>true), + array('Name' => 'phil','Age' => 14, 'HasBananas'=>false), + array('Name' => 'bob', 'Age' => 16, 'HasBananas'=>false), + array('Name' => 'clair','Age' => 16, 'HasBananas'=>true) ); $this->assertEquals($expected, $list->toArray()); } From 8c2e3230c885802a7d0d02e14e66c0ba5eae9f6e Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 15 Nov 2012 22:14:09 +0100 Subject: [PATCH 24/39] Added ModelAdmin customization docs --- docs/en/reference/modeladmin.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/en/reference/modeladmin.md b/docs/en/reference/modeladmin.md index 75f479725..fda699538 100644 --- a/docs/en/reference/modeladmin.md +++ b/docs/en/reference/modeladmin.md @@ -130,6 +130,19 @@ For example, we might want to have a checkbox which limits search results to exp } } +To alter how the results are displayed (via `[api:GridField]`), you can also overload the `getEditForm()` method. For example, to add a new component. + + :::php + class MyAdmin extends ModelAdmin { + // ... + public function getEditForm($id = null, $fields = null) { + $form = parent::getEditForm($id, $fields); + $gridField = $form->Fields()->fieldByName($this->sanitiseClassName($this->modelClass)); + $gridField->getConfig()->addComponent(new GridFieldFilterHeader()); + return $form; + } + } + ## Managing Relationships Has-one relationships are simply implemented as a `[api:DropdownField]` by default. From 0dd97a38f6ef9c46232f1f2d2a4857e7e94d4e27 Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Fri, 16 Nov 2012 11:47:32 +1300 Subject: [PATCH 25/39] API: Form#loadDataFrom 2nd arg now sets how existing field data is merged with new data --- forms/Form.php | 108 +++++++++++++++++++++++++++------------ tests/forms/FormTest.php | 32 +++++++++++- 2 files changed, 104 insertions(+), 36 deletions(-) diff --git a/forms/Form.php b/forms/Form.php index c93d1e453..d1cf61b03 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -1080,6 +1080,10 @@ class Form extends RequestHandler { return true; } + const MERGE_DEFAULT = 0; + const MERGE_CLEAR_MISSING = 1; + const MERGE_IGNORE_FALSEISH = 2; + /** * Load data from the given DataObject or array. * It will call $object->MyField to get the value of MyField. @@ -1098,20 +1102,43 @@ class Form extends RequestHandler { * @uses FormField->setValue() * * @param array|DataObject $data - * @param boolean $clearMissingFields By default, fields which don't match - * a property or array-key of the passed {@link $data} argument are "left alone", - * meaning they retain any previous values (if present). If this flag is set to true, - * those fields are overwritten with null regardless if they have a match in {@link $data}. - * @param $fieldList An optional list of fields to process. This can be useful when you have a + * @param int $mergeStrategy + * For every field, {@link $data} is interogated 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 + * value, even if that value is null/false/etc. Fields which don't match any property/key in {@link $data} are + * "left alone", meaning they retain any previous value. + * + * You can pass a bitmask here to change this behaviour. + * + * Passing CLEAR_MISSING means that any fields that don't match any property/key in + * {@link $data} are cleared. + * + * Passing IGNORE_FALSEISH means that any false-ish value in {@link $data} won't replace + * a field's value. + * + * 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 * form that has some fields that save to one object, and some that save to another. * @return Form */ - public function loadDataFrom($data, $clearMissingFields = false, $fieldList = null) { + public function loadDataFrom($data, $mergeStrategy = 0, $fieldList = null) { if(!is_object($data) && !is_array($data)) { user_error("Form::loadDataFrom() not passed an array or an object", E_USER_WARNING); return $this; } + // Handle the backwards compatible case of passing "true" as the second argument + if ($mergeStrategy === true) { + $mergeStrategy = self::MERGE_CLEAR_MISSING; + } + else if ($mergeStrategy === false) { + $mergeStrategy = 0; + } + // if an object is passed, save it for historical reference through {@link getRecord()} if(is_object($data)) $this->record = $data; @@ -1125,37 +1152,50 @@ class Form extends RequestHandler { // 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; - - // get value in different formats - $hasObjectValue = false; - if( - is_object($data) - && ( - isset($data->$name) - || $data->hasMethod($name) - || ($data->hasMethod('hasField') && $data->hasField($name)) - ) - ) { - // We don't actually call the method because it might be slow. - // In a later release, relation methods will just return references to the query that should be - // executed, and so we will be able to safely pass the return value of the relation method to the - // first argument of setValue - $val = $data->__get($name); - $hasObjectValue = true; - } else if(strpos($name,'[') && is_array($data) && !isset($data[$name])) { - // if field is in array-notation, we need to resolve the array-structure PHP creates from query-strings - preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', urldecode(http_build_query($data)), $matches); - $val = isset($matches[1]) ? $matches[1] : null; - } elseif(is_array($data) && array_key_exists($name, $data)) { - // else we assume its a simple keyed array - $val = $data[$name]; - } else { - $val = null; + + // Does this property exist on $data? + $exists = false; + // The value from $data for this field + $val = null; + + if(is_object($data)) { + $exists = ( + isset($data->$name) || + $data->hasMethod($name) || + ($data->hasMethod('hasField') && $data->hasField($name)) + ); + + if ($exists) { + $val = $data->__get($name); + } + } + else if(is_array($data)){ + if(array_key_exists($name, $data)) { + $exists = true; + $val = $data[$name]; + } + // If field is in array-notation we need to access nested data + else if(strpos($name,'[')) { + // First encode data using PHP's method of converting nested arrays to form data + $flatData = urldecode(http_build_query($data)); + // Then pull the value out from that flattened string + preg_match('/' . addcslashes($name,'[]') . '=([^&]*)/', $flatData, $matches); + + if (isset($matches[1])) { + $exists = true; + $val = $matches[1]; + } + } } // save to the field if either a value is given, or loading of blank/undefined values is forced - if(isset($val) || $hasObjectValue || $clearMissingFields) { - // pass original data as well so composite fields can act on the additional information + if($exists){ + if ($val != false || ($mergeStrategy & self::MERGE_IGNORE_FALSEISH) != self::MERGE_IGNORE_FALSEISH){ + // pass original data as well so composite fields can act on the additional information + $field->setValue($val, $data); + } + } + else if(($mergeStrategy & self::MERGE_CLEAR_MISSING) == self::MERGE_CLEAR_MISSING){ $field->setValue($val, $data); } } diff --git a/tests/forms/FormTest.php b/tests/forms/FormTest.php index fc356d40b..dbba1aa03 100644 --- a/tests/forms/FormTest.php +++ b/tests/forms/FormTest.php @@ -153,7 +153,7 @@ class FormTest extends FunctionalTest { $captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails'); $team2 = $this->objFromFixture('FormTest_Team', 'team2'); $form->loadDataFrom($captainWithDetails); - $form->loadDataFrom($team2, true); + $form->loadDataFrom($team2, Form::MERGE_CLEAR_MISSING); $this->assertEquals( $form->getData(), array( @@ -166,7 +166,35 @@ class FormTest extends FunctionalTest { 'LoadDataFrom() overwrites fields not found in the object with $clearMissingFields=true' ); } - + + public function testLoadDataFromIgnoreFalseish() { + $form = new Form( + new Controller(), + 'Form', + new FieldList( + new TextField('Biography', 'Biography', 'Custom Default') + ), + new FieldList() + ); + + $captainNoDetails = $this->objFromFixture('FormTest_Player', 'captainNoDetails'); + $captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails'); + + $form->loadDataFrom($captainNoDetails, Form::MERGE_IGNORE_FALSEISH); + $this->assertEquals( + $form->getData(), + array('Biography' => 'Custom Default'), + 'LoadDataFrom() doesn\'t overwrite fields when MERGE_IGNORE_FALSEISH set and values are false-ish' + ); + + $form->loadDataFrom($captainWithDetails, Form::MERGE_IGNORE_FALSEISH); + $this->assertEquals( + $form->getData(), + array('Biography' => 'Bio 1'), + 'LoadDataFrom() does overwrite fields when MERGE_IGNORE_FALSEISH set and values arent false-ish' + ); + } + public function testFormMethodOverride() { $form = $this->getStubForm(); $form->setFormMethod('GET'); From 7315be4531975998f683e41b48781d3ed7d9c74f Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Fri, 16 Nov 2012 11:48:31 +1300 Subject: [PATCH 26/39] FIX default values from DataObject not showing in GridField details form --- forms/gridfield/GridFieldDetailForm.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index 4aeef7931..d6000e10d 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -325,9 +325,8 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { $actions, $this->component->getValidator() ); - if($this->record->ID !== 0) { - $form->loadDataFrom($this->record); - } + + $form->loadDataFrom($this->record, $this->record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT); // TODO Coupling with CMS $toplevelController = $this->getToplevelController(); From 76c63fe4a48da8d46a41441eafad1cce1bfcdb2a Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Fri, 16 Nov 2012 13:27:51 +1300 Subject: [PATCH 27/39] BUG Fixed issue with SQLQuery::lastRow crashing on empty set. Added test cases for lastRow and firstRow. Quoted table / column names to make test cases work in postgres BUG Fixed issue with SQLQuery::lastRow crashing on empty set. Added test cases for lastRow and firstRow. Quoted table / column names to make test cases work in postgres Merge branch '3.0-sqlquery-lastrow-fix' of github.com:tractorcow/sapphire into 3.0-sqlquery-lastrow-fix --- model/SQLQuery.php | 5 ++- tests/model/SQLQueryTest.php | 68 +++++++++++++++++++++++++++++++++++- tests/model/SQLQueryTest.yml | 7 ++++ 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/model/SQLQueryTest.yml diff --git a/model/SQLQuery.php b/model/SQLQuery.php index c419586cf..020473101 100644 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -1080,7 +1080,10 @@ class SQLQuery { public function lastRow() { $query = clone $this; $offset = $this->limit ? $this->limit['start'] : 0; - $query->setLimit(1, $this->count() + $offset - 1); + + // Limit index to start in case of empty results + $index = max($this->count() + $offset - 1, 0); + $query->setLimit(1, $index); return $query; } diff --git a/tests/model/SQLQueryTest.php b/tests/model/SQLQueryTest.php index 892018c34..8e2b25984 100755 --- a/tests/model/SQLQueryTest.php +++ b/tests/model/SQLQueryTest.php @@ -2,7 +2,7 @@ class SQLQueryTest extends SapphireTest { - static $fixture_file = null; + public static $fixture_file = 'SQLQueryTest.yml'; protected $extraDataObjects = array( 'SQLQueryTest_DO', @@ -300,6 +300,72 @@ class SQLQueryTest extends SapphireTest { $query->setWhereAny(array("Monkey = 'Chimp'", "Color = 'Brown'")); $this->assertEquals("SELECT * FROM MyTable WHERE (Monkey = 'Chimp' OR Color = 'Brown')",$query->sql()); } + + public function testSelectFirst() { + + // Test first from sequence + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $result = $query->firstRow()->execute(); + + $this->assertCount(1, $result); + foreach($result as $row) { + $this->assertEquals('Object 1', $row['Name']); + } + + // Test first from empty sequence + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $query->setWhere(array("\"Name\" = 'Nonexistent Object'")); + $result = $query->firstRow()->execute(); + $this->assertCount(0, $result); + + // Test that given the last item, the 'first' in this list matches the last + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $query->setLimit(1, 1); + $result = $query->firstRow()->execute(); + $this->assertCount(1, $result); + foreach($result as $row) { + $this->assertEquals('Object 2', $row['Name']); + } + } + + public function testSelectLast() { + + // Test last in sequence + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $result = $query->lastRow()->execute(); + + $this->assertCount(1, $result); + foreach($result as $row) { + $this->assertEquals('Object 2', $row['Name']); + } + + // Test last from empty sequence + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $query->setWhere(array("\"Name\" = 'Nonexistent Object'")); + $result = $query->lastRow()->execute(); + $this->assertCount(0, $result); + + // Test that given the first item, the 'last' in this list matches the first + $query = new SQLQuery(); + $query->setFrom('"SQLQueryTest_DO"'); + $query->setOrderBy('"Name"'); + $query->setLimit(1); + $result = $query->lastRow()->execute(); + $this->assertCount(1, $result); + foreach($result as $row) { + $this->assertEquals('Object 1', $row['Name']); + } + } } diff --git a/tests/model/SQLQueryTest.yml b/tests/model/SQLQueryTest.yml new file mode 100644 index 000000000..725b7dee4 --- /dev/null +++ b/tests/model/SQLQueryTest.yml @@ -0,0 +1,7 @@ +SQLQueryTest_DO: + test1: + Name: 'Object 1' + Meta: 'Details 1' + test2: + Name: 'Object 2' + Meta: 'Details 2' \ No newline at end of file From 78ab9d3bf6855a29f0abf3b577e20d2e026e523e Mon Sep 17 00:00:00 2001 From: stojg Date: Thu, 15 Nov 2012 15:49:03 +1300 Subject: [PATCH 28/39] BUG Video embed from Add Media Feature no longer works (open #8033) Since the tinymce upgrade from 3.5.6 to 3.5.7 it seems like data attributes are forbidden on tags. This fix tells tinymce to allow data* properties on img tags --- admin/_config.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/_config.php b/admin/_config.php index 1297c918e..f446d8157 100644 --- a/admin/_config.php +++ b/admin/_config.php @@ -13,7 +13,7 @@ HtmlEditorConfig::get('cms')->setOptions(array( 'use_native_selects' => false, 'valid_elements' => "@[id|class|style|title],a[id|rel|rev|dir|tabindex|accesskey|type|name|href|target|title" . "|class],-strong/-b[class],-em/-i[class],-strike[class],-u[class],#p[id|dir|class|align|style],-ol[class]," - . "-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align]," + . "-ul[class],-li[class],br,img[id|dir|longdesc|usemap|class|src|border|alt=|title|width|height|align|data*]," . "-sub[class],-sup[class],-blockquote[dir|class]," . "-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|dir|id|style]," . "-tr[id|dir|class|rowspan|width|height|align|valign|bgcolor|background|bordercolor|style]," @@ -25,7 +25,7 @@ HtmlEditorConfig::get('cms')->setOptions(array( . "-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|dir|class|align|style],hr[class]," . "dd[id|class|title|dir],dl[id|class|title|dir],dt[id|class|title|dir],@[id,style,class]", 'extended_valid_elements' => "img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name" - . "|usemap],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling]," + . "|usemap|data*],iframe[src|name|width|height|align|frameborder|marginwidth|marginheight|scrolling]," . "object[width|height|data|type],param[name|value],map[class|name|id],area[shape|coords|href|target|alt]", 'spellchecker_rpc_url' => THIRDPARTY_DIR . '/tinymce-spellchecker/rpc.php' )); From fb7db6de6d89849af62de81ac76ce57159cb2452 Mon Sep 17 00:00:00 2001 From: Hamish Friedlander Date: Fri, 16 Nov 2012 14:45:20 +1300 Subject: [PATCH 29/39] Add 3.0.3-rc2 changelog --- docs/en/changelogs/rc/3.0.3-rc2.md | 44 ++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 docs/en/changelogs/rc/3.0.3-rc2.md diff --git a/docs/en/changelogs/rc/3.0.3-rc2.md b/docs/en/changelogs/rc/3.0.3-rc2.md new file mode 100644 index 000000000..c442faec8 --- /dev/null +++ b/docs/en/changelogs/rc/3.0.3-rc2.md @@ -0,0 +1,44 @@ +# 3.0.3-rc2 (2012-11-16) + +## Overview + +3.0.3 provides security fixes, bugfixes and a number of minor enhancements since 3.0.2. + +Upgrading from 3.0.x should be a straightforward matter of dropping in the new release, +with the exception noted below. + +## Upgrading + +Impact of the upgrade: + +* Reset password email links generated prior to 3.0.3 will cease to work. +* Users who use the "remember me" login feature will have to log in again. + +API changes related to the below security patch: + +* `Member::generateAutologinHash` is deprecated. You can no longer get the autologin token from `AutoLoginHash` field in `Member`. Instead use the return value of the `Member::generateAutologinTokenAndStoreHash` and do not persist it. +* `Security::getPasswordResetLink` now requires `Member` object as the first parameter. The password reset URL GET parameters have changed from only `h` (for hash) to `m` (for member ID) and `t` (for plaintext token). +* `RandomGenerator::generateHash` will be deprecated with 3.1. Rename the function call to `RandomGenerator::randomToken`. + +### Security: Hash autologin tokens before storing in the database. + +Severity: Moderate + +Autologin tokens (remember me and reset password) are stored in the database as a plain text. +If attacker obtained the database he would be able to gain access to accounts that have requested a password change, or have "remember me" enabled. + +## Changelog + +### API Changes + + * 2012-11-16 [0dd97a3](https://github.com/silverstripe/sapphire/commit/0dd97a3) Form#loadDataFrom 2nd arg now sets how existing field data is merged with new data (Hamish Friedlander) + * 2012-11-08 [a8b0e44](https://github.com/silverstripe/sapphire/commit/a8b0e44) Hash autologin tokens before storing in the database. (Mateusz Uzdowski) + +### Bugfixes + + * 2012-11-16 [7315be4](https://github.com/silverstripe/sapphire/commit/7315be4) default values from DataObject not showing in GridField details form (Hamish Friedlander) + * 2012-11-15 [78ab9d3](https://github.com/silverstripe/sapphire/commit/78ab9d3) Video embed from Add Media Feature no longer works (open #8033) (stojg) + +### Other + + * 2012-11-09 [05a44e8](https://github.com/silverstripe/sapphire/commit/05a44e8) Correct branch for Travis build status image (Ingo Schommer) From 32f829d09426912f803d0920b06dede5c04a577f Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 9 Nov 2012 19:16:16 +0100 Subject: [PATCH 30/39] NEW Support for Behat tests, and initial set of tests --- composer.json | 3 + .../topics/testing/create-functional-test.md | 3 + docs/en/topics/testing/index.md | 3 + tests/behat/README.md | 1 + tests/behat/_manifest_exclude | 0 .../features/bootstrap/FeatureContext.php | 38 +++ .../Test/Behaviour/CmsFormsContext.php | 72 +++++ .../Framework/Test/Behaviour/CmsUiContext.php | 256 ++++++++++++++++++ tests/behat/features/files/file1.jpg | Bin 0 -> 2292 bytes tests/behat/features/files/file2.jpg | Bin 0 -> 3886 bytes tests/behat/features/files/testfile.jpg | Bin 0 -> 4477 bytes tests/behat/features/login.feature | 20 ++ tests/behat/features/manage-files.feature | 84 ++++++ tests/behat/features/manage-users.feature | 87 ++++++ 14 files changed, 567 insertions(+) create mode 100644 tests/behat/README.md create mode 100644 tests/behat/_manifest_exclude create mode 100644 tests/behat/features/bootstrap/FeatureContext.php create mode 100644 tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php create mode 100644 tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php create mode 100644 tests/behat/features/files/file1.jpg create mode 100644 tests/behat/features/files/file2.jpg create mode 100644 tests/behat/features/files/testfile.jpg create mode 100644 tests/behat/features/login.feature create mode 100644 tests/behat/features/manage-files.feature create mode 100644 tests/behat/features/manage-users.feature diff --git a/composer.json b/composer.json index 9246df6c6..4f1e052a9 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,8 @@ "require": { "php": ">=5.3.2", "composer/installers": "*" + }, + "autoload": { + "classmap": ["tests/behat/features/bootstrap"] } } \ No newline at end of file diff --git a/docs/en/topics/testing/create-functional-test.md b/docs/en/topics/testing/create-functional-test.md index 14bcdd1c5..68465a351 100644 --- a/docs/en/topics/testing/create-functional-test.md +++ b/docs/en/topics/testing/create-functional-test.md @@ -63,3 +63,6 @@ We can use string processing on the body of the response to then see if it fits If you're testing for natural language responses like error messages, make sure to use [i18n](/topics/i18n) translations through the *_t()* method to avoid tests failing when i18n is enabled. + +Note that for a more highlevel testing approach, SilverStripe also supports +[behaviour-driven testing through Behat](https://github.com/silverstripe-labs/silverstripe-behat-extension). It interacts directly with your website or CMS interface by remote controlling an actual browser, driven by natural language assertions. \ No newline at end of file diff --git a/docs/en/topics/testing/index.md b/docs/en/topics/testing/index.md index 9d6ae83d6..ec29bb2ca 100644 --- a/docs/en/topics/testing/index.md +++ b/docs/en/topics/testing/index.md @@ -146,6 +146,9 @@ For example, you could have a `phpunit-unit-tests.xml` and `phpunit-functional-t **Assertion:** A predicate statement that must be true when a test runs. +**Behat:** A behaviour-driven testing library used with SilverStripe as a higher-level +alternative to the `FunctionalTest` API, see [http://behat.org](http://behat.org). + **Test Case:** The atomic class type in most unit test frameworks. New unit tests are created by inheriting from the base test case. diff --git a/tests/behat/README.md b/tests/behat/README.md new file mode 100644 index 000000000..e072aec9f --- /dev/null +++ b/tests/behat/README.md @@ -0,0 +1 @@ +See https://github.com/silverstripe-labs/silverstripe-behat-extension \ No newline at end of file diff --git a/tests/behat/_manifest_exclude b/tests/behat/_manifest_exclude new file mode 100644 index 000000000..e69de29bb diff --git a/tests/behat/features/bootstrap/FeatureContext.php b/tests/behat/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..ea6f10054 --- /dev/null +++ b/tests/behat/features/bootstrap/FeatureContext.php @@ -0,0 +1,38 @@ +useContext('BasicContext', new BasicContext($parameters)); + $this->useContext('LoginContext', new LoginContext($parameters)); + $this->useContext('CmsFormsContext', new CmsFormsContext($parameters)); + $this->useContext('CmsUiContext', new CmsUiContext($parameters)); + + parent::__construct($parameters); + } +} diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php new file mode 100644 index 000000000..62974c029 --- /dev/null +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsFormsContext.php @@ -0,0 +1,72 @@ +context = $parameters; + } + + /** + * Get Mink session from MinkContext + */ + public function getSession($name = null) + { + return $this->getMainContext()->getSession($name); + } + + /** + * @Then /^I should see an edit page form$/ + */ + public function stepIShouldSeeAnEditPageForm() + { + $page = $this->getSession()->getPage(); + + $form = $page->find('css', '#Form_EditForm'); + assertNotNull($form, 'I should see an edit page form'); + } + + /** + * @When /^I fill in the content form with "([^"]*)"$/ + */ + public function stepIFillInTheContentFormWith($content) + { + $this->getSession()->evaluateScript("tinyMCE.get('Form_EditForm_Content').setContent('$content')"); + } + + /** + * @Then /^the content form should contain "([^"]*)"$/ + */ + public function theContentFormShouldContain($content) + { + $this->getMainContext()->assertElementContains('#Form_EditForm_Content', $content); + } +} diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php new file mode 100644 index 000000000..c5f333056 --- /dev/null +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php @@ -0,0 +1,256 @@ +context = $parameters; + } + + /** + * Get Mink session from MinkContext + */ + public function getSession($name = null) + { + return $this->getMainContext()->getSession($name); + } + + /** + * @Then /^I should see the CMS$/ + */ + public function iShouldSeeTheCms() + { + $page = $this->getSession()->getPage(); + $cms_element = $page->find('css', '.cms'); + assertNotNull($cms_element, 'CMS not found'); + } + + /** + * @Then /^I should see a "([^"]*)" notice$/ + */ + public function iShouldSeeANotice($notice) + { + $this->getMainContext()->assertElementContains('.notice-wrap', $notice); + } + + /** + * @Then /^I should see a "([^"]*)" message$/ + */ + public function iShouldSeeAMessage($message) + { + $this->getMainContext()->assertElementContains('.message', $message); + } + + protected function getCmsTabsElement() + { + $this->getSession()->wait(5000, "window.jQuery('.cms-content-header-tabs').size() > 0"); + + $page = $this->getSession()->getPage(); + $cms_content_header_tabs = $page->find('css', '.cms-content-header-tabs'); + assertNotNull($cms_content_header_tabs, 'CMS tabs not found'); + + return $cms_content_header_tabs; + } + + protected function getCmsContentToolbarElement() + { + $this->getSession()->wait( + 5000, + "window.jQuery('.cms-content-toolbar').size() > 0 " + . "&& window.jQuery('.cms-content-toolbar').children().size() > 0" + ); + + $page = $this->getSession()->getPage(); + $cms_content_toolbar_element = $page->find('css', '.cms-content-toolbar'); + assertNotNull($cms_content_toolbar_element, 'CMS content toolbar not found'); + + return $cms_content_toolbar_element; + } + + protected function getCmsTreeElement() + { + $this->getSession()->wait(5000, "window.jQuery('.cms-tree').size() > 0"); + + $page = $this->getSession()->getPage(); + $cms_tree_element = $page->find('css', '.cms-tree'); + assertNotNull($cms_tree_element, 'CMS tree not found'); + + return $cms_tree_element; + } + + protected function getGridfieldTable($title) + { + $page = $this->getSession()->getPage(); + $table_elements = $page->findAll('css', '.ss-gridfield-table'); + assertNotNull($table_elements, 'Table elements not found'); + + $table_element = null; + foreach ($table_elements as $table) { + $table_title_element = $table->find('css', '.title'); + if ($table_title_element->getText() === $title) { + $table_element = $table; + break; + } + } + assertNotNull($table_element, sprintf('Table `%s` not found', $title)); + + return $table_element; + } + + /** + * @Given /^I should see a "([^"]*)" button in CMS Content Toolbar$/ + */ + public function iShouldSeeAButtonInCmsContentToolbar($text) + { + $cms_content_toolbar_element = $this->getCmsContentToolbarElement(); + + $element = $cms_content_toolbar_element->find('named', array('link_or_button', "'$text'")); + assertNotNull($element, sprintf('%s button not found', $text)); + } + + /** + * @When /^I should see "([^"]*)" in CMS Tree$/ + */ + public function stepIShouldSeeInCmsTree($text) + { + $cms_tree_element = $this->getCmsTreeElement(); + + $element = $cms_tree_element->find('named', array('content', "'$text'")); + assertNotNull($element, sprintf('%s not found', $text)); + } + + /** + * @When /^I should not see "([^"]*)" in CMS Tree$/ + */ + public function stepIShouldNotSeeInCmsTree($text) + { + $cms_tree_element = $this->getCmsTreeElement(); + + $element = $cms_tree_element->find('named', array('content', "'$text'")); + assertNull($element, sprintf('%s found', $text)); + } + + /** + * @When /^I expand the "([^"]*)" CMS Panel$/ + */ + public function iExpandTheCmsPanel() + { + // TODO Make dynamic, currently hardcoded to first panel + $page = $this->getSession()->getPage(); + + $panel_toggle_element = $page->find('css', '.cms-content > .cms-panel > .cms-panel-toggle > .toggle-expand'); + assertNotNull($panel_toggle_element, 'Panel toggle not found'); + + if ($panel_toggle_element->isVisible()) { + $panel_toggle_element->click(); + } + } + + /** + * @When /^I click the "([^"]*)" CMS tab$/ + */ + public function iClickTheCmsTab($tab) + { + $cms_tabs_element = $this->getCmsTabsElement(); + + $tab_element = $cms_tabs_element->find('named', array('link_or_button', "'$tab'")); + assertNotNull($tab_element, sprintf('%s tab not found', $tab)); + + $tab_element->click(); + } + + /** + * @Then /^the "([^"]*)" table should contain "([^"]*)"$/ + */ + public function theTableShouldContain($table, $text) + { + $table_element = $this->getGridfieldTable($table); +var_dump($table_element); + $element = $table_element->find('named', array('content', "'$text'")); + assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $table)); + } + + /** + * @Then /^the "([^"]*)" table should not contain "([^"]*)"$/ + */ + public function theTableShouldNotContain($table, $text) + { + $table_element = $this->getGridfieldTable($table); + + $element = $table_element->find('named', array('content', "'$text'")); + assertNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $table)); + } + + /** + * @Given /^I click on "([^"]*)" in the "([^"]*)" table$/ + */ + public function iClickOnInTheTable($text, $table) + { + $table_element = $this->getGridfieldTable($table); + + $element = $table_element->find('xpath', sprintf('//*[count(*)=0 and contains(.,"%s")]', $text)); + assertNotNull($element, sprintf('Element containing `%s` not found', $text)); + $element->click(); + } + + /** + * @Then /^I can see the preview panel$/ + */ + public function iCanSeeThePreviewPanel() + { + $this->getMainContext()->assertElementOnPage('.cms-preview'); + } + + /** + * @Given /^the preview contains "([^"]*)"$/ + */ + public function thePreviewContains($content) + { + $driver = $this->getSession()->getDriver(); + $driver->switchToIFrame('cms-preview-iframe'); + + $this->getMainContext()->assertPageContainsText($content); + $driver->switchToWindow(); + } + + /** + * @Given /^the preview does not contain "([^"]*)"$/ + */ + public function thePreviewDoesNotContain($content) + { + $driver = $this->getSession()->getDriver(); + $driver->switchToIFrame('cms-preview-iframe'); + + $this->getMainContext()->assertPageNotContainsText($content); + $driver->switchToWindow(); + } +} diff --git a/tests/behat/features/files/file1.jpg b/tests/behat/features/files/file1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..beb5a91b0aba93d3b54dadc12cff0104307d718e GIT binary patch literal 2292 zcmbV~c|6qJ9>;%UHjExS$z>*csbTDTlq4fO5~U_1OJm>1u4{=xiK_^q9w8LDu4RZU z8557($~v-CGowgKGnQ!>GxN;U>viux_m6w;=e)k(*IB;jygu)9j^K@e2FN&K9k2ih z1OOmm0|b2FS6eqbn=>wMhfslj2ScL#15xo9v>{3_E+#l45ar-#k8(U^XOD_EGun3m zMU3+e3yw`f9Y$H2N($xxTR;pXk|>Lb1ObaO6aoQ5U{DzBN5Cb-VQ>jJ3?_~cmyi@m z*pos?N{I%dLw;@*gMz_ONjMDtqvU@|f@grNIA8=Afk7I8m@Eh^3ldNOq!1GN!=8}z zr$Az02owev+CT^kNHRkEV6f0MROnb(og!QZAhJ-oy%-yqysIxV&@@|zq+B&*=`}UidnweWz9W@cqyyPi`}c)O^$RrQ0K+Pa62>RVddNbMa@I=jdNl;?v( ze+<7EpO~DYPQRI%r85>jd|X`mwEUU1w!X2+{?6Ij7IA?9@J}pZ{*PR;LM}1U8^A?e zAh85tfMp@jy%?CBjVs)jD6e6ZF0OFwR%LU)gr>0@Q}JT-n52@H30<2dLi<7X-+^WP zUu6FR`#0AtAO!{q9}g@GSOYtk7S!~}8_$uB+tPfA6|UScm#}l9_X_1u&tL^TQrcdj z>+Q>0tL3s|#Fe0ldmx9gHPI4kJ)~!b1g|z(yjN8XLXuJS` zbkD^*z5G3tsmzY%^l%+H!&dUleDZ1USK^@~9a{$<->ldybWF6F1{a6<+N$d#8AAgp zF*E@{=}DkRUa!e;;(^bU4KCsXbS(Fn>-RVSUwuF-0BsG#=yT*B;m`NAB6s9h`d>5U z*+I!zwFZ}WM9j}@ilS9JY$m9{% zsipE$tY6wzt5%oilB;$vxZ$B)^iwIL(cE|@3X!9 zbDTr53I6Tz7Vf2L?$z^&yIy=qePSj`q;Sh*>J)8~lG@3xHx8B!&?D3*4f z(16K+;)d7>XX`?zlMS|;WkY<4M_lzC8P1ia)Nya69S5E28twV6Tr)4-C%HIWYv;`p zTFU*5sQNAVW%oH)uPc)2`u@gP@{ucS0`FF`%&cKK#iMe(X=8+7xbU!S*y$YfG} zd!*;~%s9DPl~}k|wAj%YqC~`n?CF_g;&br>rUQqLThBn~X_7ouUO~4nSB>9a(PEyR zYQ=fM82>hyYA~Vx>0tfi*&?c+Ki5RkW%Z(S<%R3+=sK-?4c_)yg9C<->6uQuAmw+P z;wM73KYb_lJxGnY^I^(JN-JdXWKvPmD@={^;Mhq_QF-2Mi*u)0MeW*Q7fLY8e88IN z^iAG|>;KtsJ~5jw^VZJ5gK<0w(h{lU`qbdX1UyJtlhZtGsk?^cL-gmUr7TrLk#9c^FourW>LL;vTauCJy%?{X4MKQ47hT0+*~|arf=qg$=ZZpIf=5s9ayg_A#PN4J0T-0M|ZS11V(s}GqPVIYIU|8(Oode6acclYO}%9{6kE$M-OZo=C-yyXY&dv9s5YCdN`?_7VMjq_Tz_D9N2jX zzNGAes%lE4CzY=jq&&-;{#(iOONt{y(Z$;lRgq|eJwDf?YF;?+-#s=ml&N7?n6uuL zpVn4EF_<;__FFG&W&dz3x&Q30liRolrQ=!!#9c2b&2z2S2nX6%6as@lfIEU1*V(9j zzpO4?`p6JhBgZU%O|S6;>{IIPs}&x4_u)R5T+^5B6oBju%(2V%8aaBvvke7TJ0MLk F^Cu<`6}$id literal 0 HcmV?d00001 diff --git a/tests/behat/features/files/file2.jpg b/tests/behat/features/files/file2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fc834a452aa216fd7c7816e71877b9d10da6f7c4 GIT binary patch literal 3886 zcmbVO3p`Y58-LH7Gc!cvk|Nz^UCN5SaY<^e%P@wL%eaJ1#h78%7@QfD*h-~R3A=5( zDlO@r?WRrbCSkE{rCf5!+Cn6$2}z>)-ZM1l*YDf!``-D@d;ar0|L1xC&-0%1o{=@k z9s%{`&aTb?K@i{qKS0(>Ep&BsT?Vrs4=$A?;Ps{}XQG)iUP%$?W z;`IQKh1_Ud0sy5Q+O6Xy(GXK-L#%~EfeRkO%){o1c({N|Cwk$DNAU$hp4>4y{eyb~^FP5zVJxg!KIXz%RUF~H z0$z=X+QV@d&KUlQ8_e>C*Z|rqqa{qjJ|E)eu@Sxw5E}zP(&XdLzN37SO+vWODu`!5 zd#FI->N93f5QVsVKn!C^xe-w-h&%+Rk}GzEy##s?w}kUti9LpTP!P& zBB4967OWSe@x<~PVNUd^B-{tyr$j7fCBc2hVzEHJz}a<-pBOEa--}QimWRi32yHMw zb}txbu_1c#b zW{oWcbj%L3$80etXkChZp-|vCK_?xAfi9?YG*1kOg)0*}`~WjuH`YiILr=!n3biG8X_{7pyUq1R!Bt7}v%5`o7*^_SdEeoC%` zb0fKVJf8fJ+zdaBzya=nP_8HvsuIX|LagAKPO9N1WJ*9VoF7A86x8}b>7*Uyi_&$+ zJwzvXJKP7{L9)rXl~K>=Vbn2l7vHp9QpeVnNc-vldJ^b%NOv>x;JhMj0NDV7yxa{ahnl56Q>^mpwNmN zi^q-TgD>7Y0Eqb(=JMZ&DifYfEizdjJg=yk0E}eFWbblivXP@Oe=`8ZLfK=mnYCWP z2Vmt&;?xI9fQ)FugBrdJ2yKWiAj>fT%aK_M%t4ICI3xge2mm(5Acwc8Z^_6}(q~?{ zIh}J~YG4t9Co4~xp{+Ah*KqyKDSkg#>z_LLnvcco^2``!NU5B%_BR_@WfW5-XNJaziwrGm@9Tq(SIt)#T< z_wtI$KWef_To-VDASBI-f_I$D;ZvXAQ0fOU~D z48^EKT?i>2eo+mK{Fxa=)4_`x95YSNe2bD6Gb881O=W!xj&ynmet*gg1Ix#TuZYs* zmHl^xCI4TQDGD2}s|l#02;4kW1K5G#b;#0PZhrO<+4LwV; zE4jur$jXy)Z{zLTb}3_&$g{XZ@f?fPgAHm7DYeu?XL#kStnhEt?swMyY%S=pGqJk0 zDUEGo)V-y)A7n37`#oE?NV=))PJGGG(UPH*0~-y`Rw*0K$w+;{vA4FatfW%Y)us>a zc%{--RZ`Vhb5nEJW-#EW;M#$mJzMpg9%`;D_*Ta>#>n8$`Rs+6&Fl8J7MTrhkj`q2 z@1dTF$hLaN+PioEU&EO}&xUrJ@9N|?w_36c9$i^Yo`Y1<4Jx=Dhg3J-U&|a08p*Py z57Vos1=RVK21?tkoofp^`2PNf$Qk|=w|;6yCIz=Ym%X9tEBy_i*MaT9n2hfxFRT#Ih6EQ>9&1$_gI@$gnu247orY&su9XU1} zpfx3jTPU}B5siF!d^DKEN!uSaOao|?2CQJK0HI+@cO|{UT?_i_b(Pd zObM|!Q8Re_wTZ=p*AA_RE#51$Tb%|QZC2Upy}x-a;P#7rdc29L@6Jb+I%Vcxv}NKR zZ4t9wqS=9Oj_kE7Sg^OqB4Ul-3FEH0+9^Q;38rWG2Kt+Ot&j|9LOSR~aPgzTw@G53 z{pn@}oX{aJAN^eEfs|Me-9&F`{nmoTSCS%v>LU9ek_&$f*>lHcw^s1sLf__6lLT5s zL&r$g@J5rO`8!IlEDPHsDsatG*&S(I%5m*H99Gk_N4+9OFwLe#bV8Kvhe|6%Q?#Ym zjR)o|DI1E*i83{UbX)k%QI7}&yYgW%ev^!<<(ww=iPN?YxH;IzgwHgt}9r{ zc8gRgnGrXw>v;M7kVuD)^PZM9su!A9#P!*v7ws45SzdI#b@65V=c+e}Wx3!|=oOtB& z$WcLieoujw;QdZ=X6e*hP6MY+@=NkgK8XuuSnL1ZUzHj^*}Fy9cC{&rgCwO|DppiolOin zIb92w-6bWvucyyOZ$E39Q{O-9E9KYu+TPVUMZ51@+W2eSUVhN&Mvi^et*3vpz3(L_ z#r@eJ!f=R*UqHN!4+TO@L2<|own1^g4EC0CJgL@@gK8xOGU-7O z$3v_eu)rR&0MBTE44B8B!DaASfn>B7?tES_i_MTaW~Tq*UcmgHAcq|ZYnBcTILiyd zJe=WG8?Bwmcc9MVGl6tl4~W-5dqcR;3bn6=_*G=sc1wtL0Kh5*^X<0J@UfeAGd*`e ztPJfzETN<4tbI2(z|{?67>msf<2j>wP)`TBU^_TT!8 ze}aFGZ-;Xi{?U9qz6bvZe!74q+yS;!5eKSbNq0gp$J6_yh8oBe0y>=gA&&>OepLF& zjs|moP@DIlonRbv9diZyb?2>=PRbCaosvVzqrCiNjrok9bP35oijf;g8B&fk0WwmJ z)F4&JZA64r%;o(2-p^#2+k0ufs127bUE%XUsf$dNU3q(UMA9fY(Fc-watP7u$S*17) zoB_`9A9t-OdM|%eL9iv*5G(ntb;x?INQ#tpLufT7@t(YSipi^ znaiUmz3b3>6^)L9y4g@SL&%7M?+yU0dEv2qRtQr_Hl$ECk}ct@j6rtf2I}jQ>1;Mx z>JpIo3;}~5#R$>|=z9>21K=%H`hLSGx6a#yE&yzP4bQB5^EO%}0KWH`^o$0)&r8omrrZ5TCxmtp{%FzR!diqW3sU;#MD0=QWYdgh}8 zJ!@g~>=kJ(Zad^6lg9r2}I* zoahUVxiKp(9nOs?=O8(ySn7cGDzDbR*SvXh>zMXF)t^pZnCjyC`+k|6sF5=Miv*CS zpNjU>AFRD@pZR13gMF8BvC%I5dUi{rRt2Xhop$X$?dqLrmH0Iy&Hg-JgwT+}Y#Zx#(eQQ!U-vU{S0{RqVfQ*uJi6sn}>odI!hDs-0`xQ>K@y z8k8RM?9V_v;++K@1SDEq&A#p59p^dXUmNFJwZ!M)uFy`S(B4%Y z1-93Bs1!6BH~dh2Xd*4UDyhRfUsds1WraaMjdE<0GHEnIz42Tb^~H%Zi zj25H~td~)?$@IO)rN!v|Y{#%Uxl766%@*}B)i=*8#woP-M=I?N?naoZy*{G(#g$#g z2`S|XtH(4n26kTBT=Q$NZ_?pVPEVHpUoV9d8`{66mmQqo2=g> zYjq}Xn3V6f5UY2+3@IF$OsF~Gs@q^-X0GE($vqD}-TkzdmT~p0(Hu%{wtO`!7s$8Ytl)vKS z{zKa9&NP~?OkOu4>UiAstj^+U>#Mp6oH%-*B#Eb6oSodOTYTJ2^Dg^C-qF;PISRgK zhbUVTm6aUA)7Eg_R%%*)i1I(})e_bc6a3q~v>ViD$C7o+&t7>N?XUiZbMAgZ%XR0P z(j&|74yLq*?AToAdC^sMx2wMKps7`EYB=>LVd|+L^G?5uDi>@PZTJ5|oG>kuG03!F zUd$~UQH$}KmhIWbC_fo;w0rZ0Am;d`=0A%oSBf32np6!$H)$U>j_sW~H%;Di&u{gC z4V?$AhdGDan)Afmy2kwk@`dyr)kaMUL%r3QvN0XvzxMD&KMeYj&z>BhtLc@tc~Lo) zoEy)+;|^WgeWU9`$iCi{!SP0!0WX6?6-23*bgrCta}tV*R#*PP?(Y7o(vP)ln{B+$ zL_~XxN=B+x;ako*{iz2 zY>Rq@7y-3yk`>*bTE;HWGj1mCEGez5NSUx{>uu@&y4jqVKEy5VVP-wdzmvGaS@Ynv zjT)G|-X2!3PTK~#=5=nJLwel7tnUn`{`8G|;3a?W4n5t>hKunsXh_>1RsNIWSlXck zp6HOdiF1E`OkA}IzcJ659vF9C{}o^Mkeq|0W;# z!uJvNVVTDsCx|(tZKBbluD5;?&^bC4{eF6V*Wln({l22KWtPK6gYvy*kOB3HnKYclFj4RI0VLH@1Wq6Hfi4qGYod+!BAF{?p z7h-r@i%XT;cJ>-=xi&%FHFD=S7o#3W`H`K!F16NWz1vhZOik-Hc~)2NfBlqIhIi_1 z#{oSPXBTVlw!YtzecdD=*{ZwSzm8*aBCw5~bSq+6ftSzYrnd}M*wgpZlucE@n3s5O za`Uj3=pg&&fxn(4N16p!RUU79b@P3eZ>REGea#<#$*qz9o5veVDo~O+>pH1a6X*80 zo!{Y!9pZmGjU?1nt!V9YBZg)xOfp8+ z-mUKrF5HwE#_Q+Mx<(zn23op;vGzX2%Z0v~S@K(yy+a-?iXFdQl_mj-)8^t+*6q`) z!c@8sbev)~tmXu|mz5m<(~dAO@~`r;tgn66_9!qfE6Xvw1HJnWX)jOI^bVGQXyu|^ nySw~5MVyX~s~5f!B};&RdsWlOH`=*na*MQc_b&WeNV@(5;FjR~ literal 0 HcmV?d00001 diff --git a/tests/behat/features/login.feature b/tests/behat/features/login.feature new file mode 100644 index 000000000..b7a8388fd --- /dev/null +++ b/tests/behat/features/login.feature @@ -0,0 +1,20 @@ +# features/login.feature +Feature: Log in + As an site owner + I want to access to the CMS to be secure + So that only my team can make content changes + + Scenario: Bad login + Given I log in with "bad@example.com" and "badpassword" + Then I will see a bad log-in message + + Scenario: Valid login + Given I am logged in with "ADMIN" permissions + When I go to "/admin/" + Then I should see the CMS + + Scenario: /admin/ redirect for not logged in user + # disable automatic redirection so we can use the profiler + When I go to "/admin/" without redirection + Then I should be redirected to "/Security/login" + And I should see a log-in form \ No newline at end of file diff --git a/tests/behat/features/manage-files.feature b/tests/behat/features/manage-files.feature new file mode 100644 index 000000000..a0c09985d --- /dev/null +++ b/tests/behat/features/manage-files.feature @@ -0,0 +1,84 @@ +@javascript @assets +Feature: Manage files + As a cms author + I want to upload and manage files within the CMS + So that I can insert them into my content efficiently + + Background: + # Idea: We could weave the database reset into this through + # saying 'Given there are ONLY the following...'. + Given there are the following Folder records + """ + folder1: + Filename: assets/folder1 + folder1.1: + Filename: assets/folder1/folder1.1 + Parent: =>Folder.folder1 + folder2: + Filename: assets/folder2 + Name: folder2 + """ + And there are the following File records + """ + file1: + Filename: assets/folder1/file1.jpg + Name: file1.jpg + Parent: =>Folder.folder1 + file2: + Filename: assets/folder1/folder1.1/file2.jpg + Name: file2.jpg + Parent: =>Folder.folder1.1 + """ + And I am logged in with "ADMIN" permissions + # Alternative fixture shortcuts, with their titles + # as shown in admin/security rather than technical permission codes. + # Just an idea for now, could be handled by YAML fixtures as well +# And I am logged in with the following permissions +# - Access to 'Pages' section +# - Access to 'Files' section + And I go to "/admin/assets" + + @modal + Scenario: I can add a new folder + Given I press the "Add folder" button + And I type "newfolder" into the dialog + And I confirm the dialog + Then the "Files" table should contain "newfolder" + + Scenario: I can list files in a folder + Given I click on "folder1" in the "Files" table + Then the "folder1" table should contain "file1" + And the "folder1" table should not contain "file1.1" + + Scenario: I can upload a file to a folder + Given I click on "folder1" in the "Files" table + And I press the "Upload" button + And I attach the file "testfile.jpg" to "AssetUploadField" with HTML5 + And I wait for 5 seconds + And I press the "Back to folder" button + Then the "folder1" table should contain "testfile" + + Scenario: I can edit a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I fill in "renamedfile" for "Title" + And I press the "Save" button + And I press the "Back" button + Then the "folder1" table should not contain "testfile" + And the "folder1" table should contain "renamedfile" + + Scenario: I can delete a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I press the "Delete" button + Then the "folder1" table should not contain "file1" + + Scenario: I can change the folder of a file + Given I click on "folder1" in the "Files" table + And I click on "file1" in the "folder1" table + And I fill in =>Folder.folder2 for "ParentID" + And I press the "Save" button + # /show/0 is to ensure that we are on top level folder + And I go to "/admin/assets/show/0" + And I click on "folder2" in the "Files" table + And the "folder2" table should contain "file1" \ No newline at end of file diff --git a/tests/behat/features/manage-users.feature b/tests/behat/features/manage-users.feature new file mode 100644 index 000000000..117e6bc14 --- /dev/null +++ b/tests/behat/features/manage-users.feature @@ -0,0 +1,87 @@ +@database-defaults +Feature: Manage users + As a site administrator + I want to create and manage user accounts on my site + So that I can control access to the CMS + + Background: + Given there are the following Permission records + """ + admin: + Code: ADMIN + security-admin: + Code: CMS_ACCESS_SecurityAdmin + """ + And there are the following Group records + """ + admingroup: + Title: Admin Group + Code: admin + Permissions: =>Permission.admin + staffgroup: + Title: Staff Group + Code: staffgroup + """ + And there are the following Member records + """ + admin: + FirstName: Admin + Email: admin@test.com + Groups: =>Group.admingroup + staffmember: + FirstName: Staff + Email: staffmember@test.com + Groups: =>Group.staffgroup + """ + And I am logged in with "ADMIN" permissions + And I go to "/admin/security" + + @javascript + Scenario: I can list all users regardless of group + When I click the "Users" CMS tab + Then I should see "admin@test.com" in the "#Root_Users" element + And I should see "staffmember@test.com" in the "#Root_Users" element + + @javascript + Scenario: I can list all users in a specific group + When I click the "Groups" CMS tab + # TODO Please check how performant this is + And I click "Admin Group" in the "#Root_Groups" element + Then I should see "admin@test.com" in the "#Root_Members" element + And I should not see "staffmember@test.com" in the "#Root_Members" element + + @javascript + Scenario: I can add a user to the system + When I click the "Users" CMS tab + And I press the "Add Member" button + And I fill in the following: + | First Name | John | + | Surname | Doe | + | Email | john.doe@test.com | + And I press the "Create" button + Then I should see a "Saved member" message + + When I go to "admin/security/" + Then I should see "john.doe@test.com" in the "#Root_Users" element + + @javascript + Scenario: I can edit an existing user and add him to an existing group + When I click the "Users" CMS tab + And I click "staffmember@test.com" in the "#Root_Users" element + And I select "Admin Group" from "Groups" + And I additionally select "Administrators" from "Groups" + And I press the "Save" button + Then I should see a "Saved Member" message + + When I go to "admin/security" + And I click the "Groups" CMS tab + And I click "Admin Group" in the "#Root_Groups" element + Then I should see "staffmember@test.com" + + @javascript + Scenario: I can delete an existing user + When I click the "Users" CMS tab + And I click "staffmember@test.com" in the "#Root_Users" element + And I press the "Delete" button + Then I should see "admin@test.com" + And I should not see "staffmember@test.com" \ No newline at end of file From bd0e597ac61a3e5b4d9a5466191b18319cd6f7dc Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 9 Nov 2012 23:04:40 +0100 Subject: [PATCH 31/39] Use button tag for delete button in GridFieldEditForm --- forms/gridfield/GridFieldDetailForm.php | 1 + 1 file changed, 1 insertion(+) diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index d6000e10d..c3ae9baef 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -296,6 +296,7 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { ->setAttribute('data-icon', 'accept')); $actions->push(FormAction::create('doDelete', _t('GridFieldDetailForm.Delete', 'Delete')) + ->setUseButtonTag(true) ->addExtraClass('ss-ui-action-destructive')); }else{ // adding new record From 434759cc83e902dce660bf1b130672246ac73f57 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 9 Nov 2012 23:05:06 +0100 Subject: [PATCH 32/39] BUGFIX Correct redirection URL on deletion in GridFieldDetailForm --- forms/gridfield/GridFieldDetailForm.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forms/gridfield/GridFieldDetailForm.php b/forms/gridfield/GridFieldDetailForm.php index c3ae9baef..0f1c37fe5 100644 --- a/forms/gridfield/GridFieldDetailForm.php +++ b/forms/gridfield/GridFieldDetailForm.php @@ -377,10 +377,10 @@ class GridFieldDetailForm_ItemRequest extends RequestHandler { } elseif($this->popupController->hasMethod('Breadcrumbs')) { $parents = $this->popupController->Breadcrumbs(false)->items; $backlink = array_pop($parents)->Link; - } else { - $backlink = $toplevelController->Link(); - } + } } + if(!$backlink) $backlink = $toplevelController->Link(); + return $backlink; } From d86ad20e7283b11742ea1788202620ad591b5fa6 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 13 Nov 2012 18:18:20 +0100 Subject: [PATCH 33/39] More flexible tabs selection in behat steps --- .../Framework/Test/Behaviour/CmsUiContext.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php index c5f333056..ed7deb48c 100644 --- a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php @@ -180,9 +180,17 @@ class CmsUiContext extends BehatContext */ public function iClickTheCmsTab($tab) { - $cms_tabs_element = $this->getCmsTabsElement(); + $this->getSession()->wait(5000, "window.jQuery('.ui-tabs-nav').size() > 0"); - $tab_element = $cms_tabs_element->find('named', array('link_or_button', "'$tab'")); + $page = $this->getSession()->getPage(); + $tabsets = $page->findAll('css', '.ui-tabs-nav'); + assertNotNull($tabsets, 'CMS tabs not found'); + + $tab_element = null; + foreach($tabsets as $tabset) { + if($tab_element) continue; + $tab_element = $tabset->find('named', array('link_or_button', "'$tab'")); + } assertNotNull($tab_element, sprintf('%s tab not found', $tab)); $tab_element->click(); From e9d999d6485168f112c0796fdf842b696257ba8a Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Tue, 13 Nov 2012 18:18:49 +0100 Subject: [PATCH 34/39] Support for chosen.js drop downs in behat steps --- .../Framework/Test/Behaviour/CmsUiContext.php | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php index ed7deb48c..85200c835 100644 --- a/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php +++ b/tests/behat/features/bootstrap/SilverStripe/Framework/Test/Behaviour/CmsUiContext.php @@ -202,7 +202,7 @@ class CmsUiContext extends BehatContext public function theTableShouldContain($table, $text) { $table_element = $this->getGridfieldTable($table); -var_dump($table_element); + $element = $table_element->find('named', array('content', "'$text'")); assertNotNull($element, sprintf('Element containing `%s` not found in `%s` table', $text, $table)); } @@ -261,4 +261,54 @@ var_dump($table_element); $this->getMainContext()->assertPageNotContainsText($content); $driver->switchToWindow(); } + + /** + * Workaround for chosen.js dropdowns which hide the original dropdown field. + * + * @When /^(?:|I )fill in "(?P(?:[^"]|\\")*)" dropdown with "(?P(?:[^"]|\\")*)"$/ + * @When /^(?:|I )fill in "(?P(?:[^"]|\\")*)" for "(?P(?:[^"]|\\")*)" dropdown$/ + */ + public function theIFillInTheDropdownWith($field, $value) + { + $field = $this->fixStepArgument($field); + $value = $this->fixStepArgument($value); + + $inputField = $this->getSession()->getPage()->findField($field); + if(null === $inputField) { + throw new ElementNotFoundException(sprintf( + 'Chosen.js dropdown named "%s" not found', + $field + )); + } + + $container = $inputField->getParent()->getParent(); + if(null === $container) throw new ElementNotFoundException('Chosen.js field container not found'); + + $linkEl = $container->find('xpath', './/a'); + if(null === $linkEl) throw new ElementNotFoundException('Chosen.js link element not found'); + $linkEl->click(); + $this->getSession()->wait(100); // wait for dropdown overlay to appear + + $listEl = $container->find('xpath', sprintf('.//li[contains(normalize-space(string(.)), \'%s\')]', $value)); + if(null === $listEl) + { + throw new ElementNotFoundException(sprintf( + 'Chosen.js list element with title "%s" not found', + $value + )); + } + $listEl->click(); + } + + /** + * Returns fixed step argument (with \\" replaced back to "). + * + * @param string $argument + * + * @return string + */ + protected function fixStepArgument($argument) + { + return str_replace('\\"', '"', $argument); + } } From df4fde38648d180eaa18d712af76e9f389462174 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sun, 18 Nov 2012 23:48:43 +0100 Subject: [PATCH 35/39] Pjax docs --- docs/en/reference/cms-architecture.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/en/reference/cms-architecture.md b/docs/en/reference/cms-architecture.md index b6cdbe674..b7e6416d4 100644 --- a/docs/en/reference/cms-architecture.md +++ b/docs/en/reference/cms-architecture.md @@ -250,6 +250,11 @@ Keep in mind that the returned view isn't always decided upon when the Ajax requ is fired, so the server might decide to change it based on its own logic, sending back different `X-Pjax` headers and content. +On the client, you can set your preference through the `data-pjax-target` attributes +on links or through the `X-Pjax` header. For firing off an Ajax request that is +tracked in the browser history, use the `pjax` attribute on the state data. + + $('.cms-container').loadPanel('admin/pages', null, {pjax: 'Content'}); ## Ajax Redirects From 8f89aa917158a554ce90bec4804c325d29c81db1 Mon Sep 17 00:00:00 2001 From: Sander van Dragt Date: Tue, 20 Nov 2012 09:22:51 +0000 Subject: [PATCH 36/39] BUG only call filemtime if file exists Added file_exists check before calling filemtime as this results in 'filemtime(): stat failed' --- view/Requirements.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/view/Requirements.php b/view/Requirements.php index b2122fb63..84cdbfd76 100644 --- a/view/Requirements.php +++ b/view/Requirements.php @@ -1023,7 +1023,9 @@ class Requirements_Backend { // file exists, check modification date of every contained file $srcLastMod = 0; foreach($fileList as $file) { - $srcLastMod = max(filemtime($base . $file), $srcLastMod); + if(file_exists($base . $file)) { + $srcLastMod = max(filemtime($base . $file), $srcLastMod); + } } $refresh = $srcLastMod > filemtime($combinedFilePath); } else { From 2657a275735d920bd7bbd7e4ebe523cf2f4cc8fa Mon Sep 17 00:00:00 2001 From: Mateusz Uzdowski Date: Mon, 12 Nov 2012 16:38:18 +1300 Subject: [PATCH 37/39] BUG Adjust the handler to jQuery UI 1.9 API change. Settings.url no longer contains the URL, as a result navigating around tabs in IE (browsers that do not support History API) becomes broken. For example when the admin is opened on "Pages" section it is impossible to navigate to specific page, or if the admin is opened on a tab, it's not possible to navigate to another tab. --- admin/javascript/LeftAndMain.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/javascript/LeftAndMain.js b/admin/javascript/LeftAndMain.js index 06e78b617..7d3879615 100644 --- a/admin/javascript/LeftAndMain.js +++ b/admin/javascript/LeftAndMain.js @@ -922,14 +922,14 @@ jQuery.noConflict(); if(!this.data('uiTabs')) this.tabs({ active: (activeTab.index() != -1) ? activeTab.index() : 0, - beforeLoad: function(e, settings) { + beforeLoad: function(e, ui) { // Overwrite ajax loading to use CMS logic instead var makeAbs = $.path.makeUrlAbsolute, baseUrl = $('base').attr('href'), - isSame = (makeAbs(settings.url, baseUrl) == makeAbs(document.location.href)); + isSame = (makeAbs(ui.ajaxSettings.url, baseUrl) == makeAbs(document.location.href)); - if(!isSame) $('.cms-container').loadPanel(settings.url); - $(this).tabs('select', settings.tab.index()); + if(!isSame) $('.cms-container').loadPanel(ui.ajaxSettings.url); + $(this).tabs('select', ui.tab.index()); return false; }, From ecd921cc0a0819683fcf8f8b416fe3c6a0dce802 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 22 Nov 2012 17:03:02 +1300 Subject: [PATCH 38/39] Fixed glitches in the sample composer.json shown in the docs --- docs/en/installation/composer.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/en/installation/composer.md b/docs/en/installation/composer.md index 23fdeeb72..898acd4e6 100644 --- a/docs/en/installation/composer.md +++ b/docs/en/installation/composer.md @@ -100,14 +100,15 @@ To remove dependencies, or if you prefer seeing all your dependencies in a text "description": "The SilverStripe Framework Installer", "require": { "php": ">=5.3.2", - "silverstripe/cms": "3.0.3", - "silverstripe/framework": "3.0.3", + "silverstripe/cms": "3.0.2.1", + "silverstripe/framework": "3.0.2.1", "silverstripe-themes/simple": "*" }, "require-dev": { "silverstripe/compass": "*", "silverstripe/docsviewer": "*" }, + "minimum-stability": "dev" } To add modules, you should add more entries into the `"require"` section. For example, we might add the blog and forum modules. Be careful with the commas at the end of the lines! @@ -167,7 +168,7 @@ Open `composer.json`, and find the module's `require`. Then put `as (core versi ... "require": { "php": ">=5.3.2", - "silverstripe/cms": "3.0.3", + "silverstripe/cms": "3.0.2.1", "silverstripe/framework": "dev-myproj as 3.0.x-dev", "silverstripe-themes/simple": "*" }, From 96acd5068129b9098768ee5ab8431fcfa34db9d7 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Thu, 22 Nov 2012 15:10:10 +0100 Subject: [PATCH 39/39] Update composer contributing instructions --- docs/en/installation/composer.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/en/installation/composer.md b/docs/en/installation/composer.md index 898acd4e6..1a472caba 100644 --- a/docs/en/installation/composer.md +++ b/docs/en/installation/composer.md @@ -183,11 +183,14 @@ This is not the only way to set things up in Composer. For more information on t ## Setting up an environment for contributing to SilverStripe -So you want to contribute to SilverStripe? Fantastic! There are a couple modules that will help you, that aren't installed by default: +So you want to contribute to SilverStripe? Fantastic! You have to initialize your project from the latest development branch, +rather than a release tag. The process will take a bit longer, since all modules are checked out as full git repositories which you can work on. + + composer create-project silverstripe/installer --dev ./my/website/folder 3.0.x-dev + +The `--dev` flag will add a couple modules which are useful for SilverStripe development: * The `compass` module will regenerate CSS if you update the SCSS files * The `docsviewer` module will let you preview changes to the project documentation -By default, these modules aren't installed, but you can install them with a special version of composer's update command: - - composer update --dev +Note that you can also include those into an existing project by running `composer update --dev`. \ No newline at end of file