From b7fa2669a88c10e8a59910652c6fb0a33ffddf51 Mon Sep 17 00:00:00 2001 From: Nemanja Karadzic Date: Thu, 9 Oct 2014 21:00:34 +0200 Subject: [PATCH 01/31] Added check for "convert_urls" in TinyMCE config. --- javascript/HtmlEditorField.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/javascript/HtmlEditorField.js b/javascript/HtmlEditorField.js index 9fd1f8487..ad12b58e8 100644 --- a/javascript/HtmlEditorField.js +++ b/javascript/HtmlEditorField.js @@ -213,11 +213,12 @@ ss.editorWrappers.tinyMCE = (function() { * {DOMElement} */ cleanLink: function(href, node) { - var cb = tinyMCE.settings['urlconverter_callback']; + var cb = tinyMCE.settings['urlconverter_callback'], + cu = tinyMCE.settings['convert_urls']; if(cb) href = eval(cb + "(href, node, true);"); - // Turn into relative - if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) { + // Turn into relative, if set in TinyMCE config + if(cu && href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) { href = RegExp.$1; } From 521b8eefd0ae5b4ce3142d19b9edf5c8f47b5e78 Mon Sep 17 00:00:00 2001 From: Nemanja Karadzic Date: Sun, 12 Oct 2014 06:53:40 +0200 Subject: [PATCH 02/31] Casted return of count() to integer. Count should ALWAYS be integer, not a string. --- model/DataList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/DataList.php b/model/DataList.php index e55c0a2f3..696c5f46a 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -709,7 +709,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * @return int */ public function count() { - return $this->dataQuery->count(); + return (int)$this->dataQuery->count(); } /** From 95b66d19b2210b1f80fbe1b5b419f9f6526fbc08 Mon Sep 17 00:00:00 2001 From: Stephan van Diepen Date: Mon, 14 Jul 2014 11:06:47 +0200 Subject: [PATCH 03/31] Added MySQL support for Bigint. Conflicts: model/MySQLDatabase.php --- model/connect/MySQLSchemaManager.php | 16 ++++++++++++++++ model/fieldtypes/Bigint.php | 25 +++++++++++++++++++++++++ tests/model/DBFieldTest.php | 5 +++++ tests/model/DataObjectTest.php | 12 +++++++++++- 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 model/fieldtypes/Bigint.php diff --git a/model/connect/MySQLSchemaManager.php b/model/connect/MySQLSchemaManager.php index 8acae7a58..8bb0f4424 100644 --- a/model/connect/MySQLSchemaManager.php +++ b/model/connect/MySQLSchemaManager.php @@ -486,6 +486,22 @@ class MySQLSchemaManager extends DBSchemaManager { return "int(11) not null" . $this->defaultClause($values); } + /** + * Return a bigint type-formatted string + * + * @param array $values Contains a tokenised list of info about this data type + * @return string + */ + public function bigint($values) { + //For reference, this is what typically gets passed to this function: + //$parts=Array('datatype'=>'bigint', 'precision'=>20, 'null'=>'not null', 'default'=>$this->defaultVal, + // 'arrayValue'=>$this->arrayValue); + //$values=Array('type'=>'bigint', 'parts'=>$parts); + //DB::requireField($this->tableName, $this->name, $values); + + return 'bigint(20) not null' . $this->defaultClause($values); + } + /** * Return a datetime type-formatted string * For MySQL, we simply return the word 'datetime', no other parameters are necessary diff --git a/model/fieldtypes/Bigint.php b/model/fieldtypes/Bigint.php new file mode 100644 index 000000000..482d069ce --- /dev/null +++ b/model/fieldtypes/Bigint.php @@ -0,0 +1,25 @@ + 'bigint', + 'precision' => 8, + 'null' => 'not null', + 'default' => $this->defaultVal, + 'arrayValue' => $this->arrayValue + ); + + $values = array('type' => 'bigint', 'parts' => $parts); + DB::require_field($this->tableName, $this->name, $values); + } +} diff --git a/tests/model/DBFieldTest.php b/tests/model/DBFieldTest.php index 41d410f5e..68c071447 100644 --- a/tests/model/DBFieldTest.php +++ b/tests/model/DBFieldTest.php @@ -160,6 +160,11 @@ class DBFieldTest extends SapphireTest { $this->assertEquals("00:00:00", $time->getValue()); $time->setValue('00:00:00'); $this->assertEquals("00:00:00", $time->getValue()); + + /* BigInt behaviour */ + $bigInt = singleton('BigInt'); + $bigInt->setValue(PHP_INT_MAX); + $this->assertEquals(PHP_INT_MAX, $bigInt->getValue()); } public function testExists() { diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 3ca70a887..add8c7ed2 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -1700,6 +1700,13 @@ class DataObjectTest extends SapphireTest { } + public function testBigIntField() { + $staff = new DataObjectTest_Staff(); + $staff->Salary = PHP_INT_MAX; + $staff->write(); + $this->assertEquals(PHP_INT_MAX, DataObjectTest_Staff::get()->byID($staff->ID)->Salary); + } + } class DataObjectTest_Player extends Member implements TestOnly { @@ -1913,11 +1920,14 @@ class DataObjectTest_EquipmentCompany extends DataObjectTest_Company implements class DataObjectTest_SubEquipmentCompany extends DataObjectTest_EquipmentCompany implements TestOnly { private static $db = array( - 'SubclassDatabaseField' => 'Varchar' + 'SubclassDatabaseField' => 'Varchar', ); } class DataObjectTest_Staff extends DataObject implements TestOnly { + private static $db = array( + 'Salary' => 'BigInt', + ); private static $has_one = array ( 'CurrentCompany' => 'DataObjectTest_Company', 'PreviousCompany' => 'DataObjectTest_Company' From 24f8f2715c0a864fe3279530ccf9a4412535e515 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Mon, 3 Oct 2016 09:36:47 -0700 Subject: [PATCH 04/31] DOCS Introduce TemplateGlobalProvider --- .../02_Developer_Guides/01_Templates/02_Common_Variables.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md index af2d798f2..373abdda0 100644 --- a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -8,7 +8,9 @@ exhaustive list. From your template you can call any method, database field, or currently in scope as well as its' subclasses or extensions. Knowing what methods you can call can be tricky, but the first step is to understand the scope you're in. Scope is -explained in more detail on the [syntax](syntax#scope) page. +explained in more detail on the [syntax](syntax#scope) page. Many of the methods listed below can be called from any +scope, and you can specify additional static methods to be available globally in templates by implementing the +[api:TemplateGlobalProvider] interface.
Want a quick way of knowing what scope you're in? Try putting `$ClassName` in your template. You should see a string From 3d8db488d44029c6442dd0db471e942007a2f4ec Mon Sep 17 00:00:00 2001 From: Thomas Portelange Date: Tue, 4 Oct 2016 12:24:29 +0200 Subject: [PATCH 05/31] Have a clear error message Because it's really annoying not knowing which field causes the error --- forms/FieldList.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/forms/FieldList.php b/forms/FieldList.php index 83be3f75c..68a1f9922 100644 --- a/forms/FieldList.php +++ b/forms/FieldList.php @@ -550,7 +550,12 @@ class FieldList extends ArrayList { public function makeFieldReadonly($field) { $fieldName = ($field instanceof FormField) ? $field->getName() : $field; $srcField = $this->dataFieldByName($fieldName); - $this->replaceField($fieldName, $srcField->performReadonlyTransformation()); + if($srcField) { + $this->replaceField($fieldName, $srcField->performReadonlyTransformation()); + } + else { + user_error("Trying to make field '$fieldName' readonly, but it does not exist in the list",E_USER_WARNING); + } } /** From 6dde5ce5718911d8e405eb590c68036ceaa6e608 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 4 Oct 2016 14:21:32 +0100 Subject: [PATCH 06/31] FIX Absolute alternate_base_url no longer breaks session cookies --- control/Session.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/control/Session.php b/control/Session.php index 9725783b2..e40018a4f 100644 --- a/control/Session.php +++ b/control/Session.php @@ -359,6 +359,15 @@ class Session { $path = Config::inst()->get('Session', 'cookie_path'); if(!$path) $path = Director::baseURL(); $domain = Config::inst()->get('Session', 'cookie_domain'); + // Director::baseURL can return absolute domain names - this extracts the relevant parts + // for the session otherwise we can get broken session cookies + if (Director::is_absolute_url($path)) { + $urlParts = parse_url($path); + $path = $urlParts['path']; + if (!$domain) { + $domain = $urlParts['host']; + } + } $secure = Director::is_https() && Config::inst()->get('Session', 'cookie_secure'); $session_path = Config::inst()->get('Session', 'session_store_path'); $timeout = Config::inst()->get('Session', 'timeout'); From 797be6ac82f6938af06c24c99150648ff214f797 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Tue, 4 Oct 2016 11:14:16 -0700 Subject: [PATCH 07/31] FIX Revert natural sort More backwards compatible and more consistent with ORM sorting (fixes #6124) --- model/ArrayList.php | 2 +- tests/model/ArrayListTest.php | 105 +++++++++++++++++++-------------- tests/model/DataListTest.php | 29 +++++++++ tests/model/DataObjectTest.php | 7 +++ tests/model/DataObjectTest.yml | 22 +++++++ 5 files changed, 119 insertions(+), 46 deletions(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index 903b04421..784fb55ce 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -448,7 +448,7 @@ class ArrayList extends ViewableData implements SS_List, SS_Filterable, SS_Sorta // First argument is the direction to be sorted, $multisortArgs[] = &$sortDirection[$column]; if ($firstRun) { - $multisortArgs[] = defined('SORT_NATURAL') ? SORT_NATURAL : SORT_STRING; + $multisortArgs[] = SORT_REGULAR; } $firstRun = false; } diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index 7e2e4630d..1f98fa086 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -314,67 +314,46 @@ class ArrayListTest extends SapphireTest { ), $list->toArray()); } - public function testNaturalSort() { - //natural sort is only available in 5.4+ - if (version_compare(phpversion(), '5.4.0', '<')) { - $this->markTestSkipped(); - } - $list = new ArrayList(array( - array('Name' => 'Steve'), + public function testMixedCaseSort() { + // Note: Natural sorting is not expected, so if 'bonny10' were included + // below we would expect it to appear between bonny1 and bonny2. That's + // undesirable though so we're not enforcing it in tests. + $original = array( + array('Name' => 'Steve'), (object) array('Name' => 'Bob'), array('Name' => 'John'), array('Name' => 'bonny'), array('Name' => 'bonny1'), - array('Name' => 'bonny10'), + //array('Name' => 'bonny10'), array('Name' => 'bonny2'), - )); + ); + + $list = new ArrayList($original); + + $expected = array( + (object) array('Name' => 'Bob'), + array('Name' => 'bonny'), + array('Name' => 'bonny1'), + //array('Name' => 'bonny10'), + array('Name' => 'bonny2'), + array('Name' => 'John'), + array('Name' => 'Steve'), + ); // Unquoted name $list1 = $list->sort('Name'); - $this->assertEquals(array( - (object) array('Name' => 'Bob'), - array('Name' => 'bonny'), - array('Name' => 'bonny1'), - array('Name' => 'bonny2'), - array('Name' => 'bonny10'), - array('Name' => 'John'), - array('Name' => 'Steve'), - ), $list1->toArray()); + $this->assertEquals($expected, $list1->toArray()); // Quoted name name $list2 = $list->sort('"Name"'); - $this->assertEquals(array( - (object) array('Name' => 'Bob'), - array('Name' => 'bonny'), - array('Name' => 'bonny1'), - array('Name' => 'bonny2'), - array('Name' => 'bonny10'), - array('Name' => 'John'), - array('Name' => 'Steve'), - ), $list2->toArray()); + $this->assertEquals($expected, $list2->toArray()); // Array (non-associative) $list3 = $list->sort(array('"Name"')); - $this->assertEquals(array( - (object) array('Name' => 'Bob'), - array('Name' => 'bonny'), - array('Name' => 'bonny1'), - array('Name' => 'bonny2'), - array('Name' => 'bonny10'), - array('Name' => 'John'), - array('Name' => 'Steve'), - ), $list3->toArray()); + $this->assertEquals($expected, $list3->toArray()); // Check original list isn't altered - $this->assertEquals(array( - array('Name' => 'Steve'), - (object) array('Name' => 'Bob'), - array('Name' => 'John'), - array('Name' => 'bonny'), - array('Name' => 'bonny1'), - array('Name' => 'bonny10'), - array('Name' => 'bonny2'), - ), $list->toArray()); + $this->assertEquals($original, $list->toArray()); } @@ -472,6 +451,42 @@ class ArrayListTest extends SapphireTest { )); } + public function testSortNumeric() { + $list = new ArrayList(array( + array('Sort' => 0), + array('Sort' => -1), + array('Sort' => 1), + array('Sort' => -2), + array('Sort' => 2), + array('Sort' => -10), + array('Sort' => 10) + )); + + // Sort descending + $list1 = $list->sort('Sort', 'DESC'); + $this->assertEquals(array( + array('Sort' => 10), + array('Sort' => 2), + array('Sort' => 1), + array('Sort' => 0), + array('Sort' => -1), + array('Sort' => -2), + array('Sort' => -10) + ), $list1->toArray()); + + // Sort ascending + $list1 = $list->sort('Sort', 'ASC'); + $this->assertEquals(array( + array('Sort' => -10), + array('Sort' => -2), + array('Sort' => -1), + array('Sort' => 0), + array('Sort' => 1), + array('Sort' => 2), + array('Sort' => 10) + ), $list1->toArray()); + } + public function testReverse() { $list = new ArrayList(array( array('Name' => 'John'), diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 1a4353127..87a695f17 100755 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -23,6 +23,7 @@ class DataListTest extends SapphireTest { 'DataObjectTest_EquipmentCompany', 'DataObjectTest_SubEquipmentCompany', 'DataObjectTest\NamespacedClass', + 'DataObjectTest_Sortable', 'DataObjectTest_Company', 'DataObjectTest_Fan', 'ManyManyListTest_Product', @@ -533,6 +534,34 @@ class DataListTest extends SapphireTest { $this->assertEquals('Phil', $list->last()->Name, 'Last comment should be from Phil'); } + public function testSortNumeric() { + $list = DataObjectTest_Sortable::get(); + $list1 = $list->sort('Sort', 'ASC'); + $this->assertEquals(array( + -10, + -2, + -1, + 0, + 1, + 2, + 10 + ), $list1->column('Sort')); + } + + public function testSortMixedCase() { + $list = DataObjectTest_Sortable::get(); + $list1 = $list->sort('Name', 'ASC'); + $this->assertEquals(array( + 'Bob', + 'bonny', + 'jane', + 'John', + 'sam', + 'Steve', + 'steven' + ), $list1->column('Name')); + } + /** * Test DataList->canFilterBy() */ diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 3ca70a887..79a685981 100644 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -1702,6 +1702,13 @@ class DataObjectTest extends SapphireTest { } +class DataObjectTest_Sortable extends DataObject implements TestOnly { + private static $db = array( + 'Sort' => 'Int', + 'Name' => 'Varchar', + ); +} + class DataObjectTest_Player extends Member implements TestOnly { private static $db = array( 'IsRetired' => 'Boolean', diff --git a/tests/model/DataObjectTest.yml b/tests/model/DataObjectTest.yml index 19065154e..16c145971 100644 --- a/tests/model/DataObjectTest.yml +++ b/tests/model/DataObjectTest.yml @@ -1,3 +1,25 @@ +DataObjectTest_Sortable: + numeric1: + Sort: 0 + Name: steven + numeric2: + Sort: -1 + Name: bonny + numeric3: + Sort: 1 + Name: sam + numeric4: + Sort: -2 + Name: Bob + numeric5: + Sort: 2 + Name: jane + numeric6: + Sort: -10 + Name: Steve + numeric7: + Sort: 10 + Name: John DataObjectTest_EquipmentCompany: equipmentcompany1: Name: Company corp From 5a2591ec7d378334d9865c933b05051b69981e15 Mon Sep 17 00:00:00 2001 From: Matt Peel Date: Thu, 6 Oct 2016 14:01:38 +1300 Subject: [PATCH 08/31] Make __call() clearer that a method may be non-public Resolves #6151 --- core/Object.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Object.php b/core/Object.php index 6f75896ea..8d6c0b41b 100755 --- a/core/Object.php +++ b/core/Object.php @@ -777,7 +777,7 @@ abstract class Object { } else { // Please do not change the exception code number below. $class = get_class($this); - throw new Exception("Object->__call(): the method '$method' does not exist on '$class'", 2175); + throw new Exception("Object->__call(): the method '$method' does not exist on '$class', or the method is not public.", 2175); } } From 7368deca8f409c5aba94a6b646d7c0ac4fbd452f Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 11 Oct 2016 14:56:32 +1300 Subject: [PATCH 09/31] BUG Fix issue with SS_List as datasource for dropdown field BUG Fix validation issue with CheckboxSetField Fixes #6166 --- forms/CheckboxSetField.php | 7 ++-- forms/DropdownField.php | 19 ++++++--- tests/forms/CheckboxSetFieldTest.php | 61 ++++++++++++++++++++++------ tests/forms/DropdownFieldTest.php | 31 ++++++++++++-- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index f1ef4bbaa..acd205809 100644 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -333,21 +333,22 @@ class CheckboxSetField extends OptionsetField { return true; } $sourceArray = $this->getSourceAsArray(); + $validValues = array_keys($sourceArray); if (is_array($values)) { - if (!array_intersect_key($sourceArray, $values)) { + if (!array_intersect($validValues, $values)) { $validator->validationError( $this->name, _t( 'CheckboxSetField.SOURCE_VALIDATION', "Please select a value within the list provided. '{value}' is not a valid option", - array('value' => implode(' and ', array_diff($sourceArray, $values))) + array('value' => implode(' and ', $values)) ), "validation" ); return false; } } else { - if (!in_array($this->value, $sourceArray)) { + if (!in_array($this->value, $validValues)) { $validator->validationError( $this->name, _t( diff --git a/forms/DropdownField.php b/forms/DropdownField.php index 83fa88edf..1aa6c28f5 100644 --- a/forms/DropdownField.php +++ b/forms/DropdownField.php @@ -318,13 +318,22 @@ class DropdownField extends FormField { public function getSourceAsArray() { $source = $this->getSource(); + + // Simplify source if presented as dataobject list + if ($source instanceof SS_List) { + $source = $source->map(); + } + if ($source instanceof SS_Map) { + $source = $source->toArray(); + } + if (is_array($source)) { return $source; - } else { - $sourceArray = array(); - foreach ($source as $key => $value) { - $sourceArray[$key] = $value; - } + } + + $sourceArray = array(); + foreach ($source as $key => $value) { + $sourceArray[$key] = $value; } return $sourceArray; } diff --git a/tests/forms/CheckboxSetFieldTest.php b/tests/forms/CheckboxSetFieldTest.php index 94cb4e171..e90a31d4c 100644 --- a/tests/forms/CheckboxSetFieldTest.php +++ b/tests/forms/CheckboxSetFieldTest.php @@ -177,28 +177,65 @@ class CheckboxSetFieldTest extends SapphireTest { $tag1 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag1'); $tag2 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag2'); $tag3 = $this->objFromFixture('CheckboxSetFieldTest_Tag', 'tag3'); - $field = CheckboxSetField::create('Test', 'Testing', $checkboxTestArticle->Tags() ->map()); + $field = CheckboxSetField::create('Test', 'Testing', $checkboxTestArticle->Tags()); $validator = new RequiredFields(); - $field->setValue(array( - $tag1->ID => $tag1->ID, - $tag2->ID => $tag2->ID - )); + $field->setValue(array( $tag1->ID, $tag2->ID )); + $isValid = $field->validate($validator); $this->assertTrue( - $field->validate($validator), + $isValid, 'Validates values in source map' ); - //invalid value should fail + + // Invalid value should fail + $validator = new RequiredFields(); $fakeID = CheckboxSetFieldTest_Tag::get()->max('ID') + 1; - $field->setValue(array($fakeID => $fakeID)); + $field->setValue(array($fakeID)); $this->assertFalse( $field->validate($validator), 'Field does not valid values outside of source map' ); - //non valid value included with valid options should succeed + $errors = $validator->getErrors(); + $error = reset($errors); + $this->assertEquals( + "Please select a value within the list provided. '$fakeID' is not a valid option", + $error['message'] + ); + + // Multiple invalid values should fail + $validator = new RequiredFields(); + $fakeID = CheckboxSetFieldTest_Tag::get()->max('ID') + 1; + $field->setValue(array($fakeID, $tag3->ID)); + $this->assertFalse( + $field->validate($validator), + 'Field does not valid values outside of source map' + ); + $errors = $validator->getErrors(); + $error = reset($errors); + $this->assertEquals( + "Please select a value within the list provided. '{$fakeID} and {$tag3->ID}' is not a valid option", + $error['message'] + ); + + // Invalid value with non-array value + $validator = new RequiredFields(); + $field->setValue($fakeID); + $this->assertFalse( + $field->validate($validator), + 'Field does not valid values outside of source map' + ); + $errors = $validator->getErrors(); + $error = reset($errors); + $this->assertEquals( + "Please select a value within the list provided. '{$fakeID}' is not a valid option", + $error['message'] + ); + + // non valid value included with valid options should succeed + $validator = new RequiredFields(); $field->setValue(array( - $tag1->ID => $tag1->ID, - $tag2->ID => $tag2->ID, - $tag3->ID => $tag3->ID + $tag1->ID, + $tag2->ID, + $tag3->ID )); $this->assertTrue( $field->validate($validator), diff --git a/tests/forms/DropdownFieldTest.php b/tests/forms/DropdownFieldTest.php index a43f8ee09..912aa041c 100644 --- a/tests/forms/DropdownFieldTest.php +++ b/tests/forms/DropdownFieldTest.php @@ -6,12 +6,37 @@ class DropdownFieldTest extends SapphireTest { public function testGetSource() { - $source = array(1=>'one'); + $source = array(1=>'one', 2 => 'two'); $field = new DropdownField('Field', null, $source); $this->assertEquals( - $field->getSource(), + $source, + $field->getSource() + ); + $this->assertEquals( + $source, + $field->getSourceAsArray() + ); + + $items = new ArrayList([ + [ 'ID' => 1, 'Title' => 'ichi', 'OtherField' => 'notone' ], + [ 'ID' => 2, 'Title' => 'ni', 'OtherField' => 'nottwo' ], + ]); + $field->setSource($items); + $this->assertEquals( + $field->getSourceAsArray(), array( - 1 => 'one' + 1 => 'ichi', + 2 => 'ni', + ) + ); + + $map = new SS_Map($items, 'ID', 'OtherField'); + $field->setSource($map); + $this->assertEquals( + $field->getSourceAsArray(), + array( + 1 => 'notone', + 2 => 'nottwo', ) ); } From a893e2aa0f7eb85250e9e94c03e5dc2e8d379fed Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Thu, 13 Oct 2016 09:37:52 -0700 Subject: [PATCH 10/31] DOCS How to increase partial cache expiry Closes #3649 --- .../08_Performance/00_Partial_Caching.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index 5c7d3a281..b28dc7b42 100644 --- a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -236,3 +236,16 @@ Can be re-written as: <% end_cached %> <% end_cached %> + +## Cache expiry + +The default expiry for partial caches is 10 minutes. The advantage of a short cache expiry is that if you have a problem +with your caching logic, the window in which stale content may be shown is short. The disadvantage, particularly for +low-traffic sites, is that cache blocks may expire before they can be utilised. If you're confident that you're caching +logic is sound, you could increase the expiry dramatically. + +**mysite/_config.php** + + :::php + // Set partial cache expiry to 7 days + SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7); From e9a75a54d97213343e6a09cd08ed537de5ec44bf Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Thu, 13 Oct 2016 09:38:49 -0700 Subject: [PATCH 11/31] DOCS Partial caching of relationships Fixes #6177 --- .../08_Performance/00_Partial_Caching.md | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index b28dc7b42..e0ca6e19a 100644 --- a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -74,34 +74,36 @@ Note the use of both `.max('LastEdited')` and `.count()` - this takes care of bo edited since the cache was last built, and also when an object has been deleted since the cache was last built.
-We can also calculate aggregates on relationships. A block that shows the current member's favorites needs to update -whenever the relationship `Member::$has_many = array('Favourites' => Favourite')` changes. - - :::ss - <% cached 'favourites', $CurrentMember.ID, $CurrentMember.Favourites.max('LastEdited') %> +We can also calculate aggregates on relationships. The logic for that can get a bit complex, so we can extract that on +to the controller so it's not cluttering up our template. ## Cache key calculated in controller -In the previous example the cache key is getting a bit large, and is complicating our template up. Better would be to -extract that logic into the controller. +If your caching logic is complex or re-usable, you can define a method on your controller to generate a cache key +fragment. + +For example, a block that shows a collection of rotating slides needs to update whenever the relationship +`Page::$many_many = array('Slides' => 'Slide')` changes. In Page_Controller: :::php - public function FavouriteCacheKey() { - $member = Member::currentUser(); - - return implode('_', array( - 'favourites', - $member->ID, - $member->Favourites()->max('LastEdited') - )); + public function SliderCacheKey() { + $fragments = array( + 'Page-Slides', + $this->ID, + // identify which objects are in the list and their sort order + implode('-', $this->Slides()->Column('ID')), + $this->Slides()->max('LastEdited') + ); + return implode('-_-', $fragments); } -Then using that function in the cache key: +Then reference that function in the cache key: :::ss - <% cached $FavouriteCacheKey %> + <% cached $SliderCacheKey %> +The example above would work for both a has_many and many_many relationship. ## Cache blocks and template changes From bfdac2b7b6d07ebc06e97d1d1dc0b5f28180f742 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Thu, 13 Oct 2016 09:39:10 -0700 Subject: [PATCH 12/31] DOCS Template debugging --- .../07_Debugging/03_Template_debugging.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md diff --git a/docs/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md b/docs/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md new file mode 100644 index 000000000..fe1cf8654 --- /dev/null +++ b/docs/en/02_Developer_Guides/07_Debugging/03_Template_debugging.md @@ -0,0 +1,19 @@ +title: Template debugging +summary: Track down which template rendered a piece of html + +# Debugging templates + +## Source code comments + +If there is a problem with the rendered html your page is outputting you may need +to track down a template or two. The template engine can help you along by displaying +source code comments indicating which template is responsible for rendering each +block of html on your page. + + ::::yaml + --- + Only: + environment: 'dev' + --- + SSViewer: + source_file_comments: true From 646d34ec48637aff893029b33dea4f6f63dd63b5 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Thu, 13 Oct 2016 09:39:46 -0700 Subject: [PATCH 13/31] DOCS Non-extendable classes Closes #6129 --- docs/en/02_Developer_Guides/05_Extending/01_Extensions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md b/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md index e872a092c..1ece0cd4a 100644 --- a/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md +++ b/docs/en/02_Developer_Guides/05_Extending/01_Extensions.md @@ -10,6 +10,11 @@ or even their own code to make it more reusable. Extensions are defined as subclasses of either [api:DataExtension] for extending a [api:DataObject] subclass or the [api:Extension] class for non DataObject subclasses (such as [api:Controllers]) +
+For performance reasons a few classes are excluded from receiving extensions, including `Object`, `ViewableData` +and `RequestHandler`. You can still apply extensions to descendants of these classes. +
+ **mysite/code/extensions/MyMemberExtension.php** :::php From d2c0b98bc5869e442a16c59e4669c45bb551dde7 Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Thu, 13 Oct 2016 14:52:41 -0700 Subject: [PATCH 14/31] DOCS Clarify nested cache block restrictions Fixes #6078 --- .../08_Performance/00_Partial_Caching.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md index e0ca6e19a..ddb63074b 100644 --- a/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md +++ b/docs/en/02_Developer_Guides/08_Performance/00_Partial_Caching.md @@ -209,8 +209,8 @@ could also write the last example as: <% end_cached %>
-Currently cached blocks can not be contained within if or loop blocks. The template engine will throw an error -letting you know if you've done this. You can often get around this using aggregates. +Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error +letting you know if you've done this. You can often get around this using aggregates or by un-nesting the block.
Failing example: @@ -219,7 +219,7 @@ Failing example: <% cached $LastEdited %> <% loop $Children %> - <% cached LastEdited %> + <% cached $LastEdited %> $Name <% end_cached %> <% end_loop %> @@ -239,6 +239,19 @@ Can be re-written as: <% end_cached %> +Or: + + :::ss + <% cached $LastEdited %> + (other code) + <% end_cached %> + + <% loop $Children %> + <% cached $LastEdited %> + $Name + <% end_cached %> + <% end_loop %> + ## Cache expiry The default expiry for partial caches is 10 minutes. The advantage of a short cache expiry is that if you have a problem From b0445f72e4cce324308bb32384d578e43753cd6d Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Tue, 18 Oct 2016 20:49:47 -0700 Subject: [PATCH 15/31] FIX Ambiguous column SQL error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specify the table for the field we’re fetching, in case a joined table has a field with the same name --- model/DataQuery.php | 12 ++++++++---- tests/model/DataListTest.php | 11 +++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/model/DataQuery.php b/model/DataQuery.php index fe7cb7d62..cf8fe839b 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -392,7 +392,8 @@ class DataQuery { * automatically so must not contain double quotes. */ public function max($field) { - return $this->aggregate("MAX(\"$field\")"); + $table = ClassInfo::table_for_object_field($this->dataClass, $field); + return $this->aggregate("MAX(\"$table\".\"$field\")"); } /** @@ -402,7 +403,8 @@ class DataQuery { * automatically so must not contain double quotes. */ public function min($field) { - return $this->aggregate("MIN(\"$field\")"); + $table = ClassInfo::table_for_object_field($this->dataClass, $field); + return $this->aggregate("MIN(\"$table\".\"$field\")"); } /** @@ -412,7 +414,8 @@ class DataQuery { * automatically so must not contain double quotes. */ public function avg($field) { - return $this->aggregate("AVG(\"$field\")"); + $table = ClassInfo::table_for_object_field($this->dataClass, $field); + return $this->aggregate("AVG(\"$table\".\"$field\")"); } /** @@ -422,7 +425,8 @@ class DataQuery { * automatically so must not contain double quotes. */ public function sum($field) { - return $this->aggregate("SUM(\"$field\")"); + $table = ClassInfo::table_for_object_field($this->dataClass, $field); + return $this->aggregate("SUM(\"$table\".\"$field\")"); } /** diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 87a695f17..21299c88e 100755 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -363,6 +363,17 @@ class DataListTest extends SapphireTest { $this->assertEquals($otherExpected, $otherMap); } + public function testAmbiguousAggregate() { + // Test that we avoid ambiguity error when a field exists on two joined tables + // Fetch the sponsors in a round-about way to simulate this + $teamID = $this->idFromFixture('DataObjectTest_Team','team2'); + $sponsors = DataObjectTest_EquipmentCompany::get()->filter('SponsoredTeams.ID', $teamID); + $this->assertNotNull($sponsors->Max('ID')); + $this->assertNotNull($sponsors->Min('ID')); + $this->assertNotNull($sponsors->Avg('ID')); + $this->assertNotNull($sponsors->Sum('ID')); + } + public function testEach() { $list = DataObjectTest_TeamComment::get(); From 4d327f81fd9fbf3538c55b64f4c68ae484e0d826 Mon Sep 17 00:00:00 2001 From: Nicola Fontana Date: Fri, 21 Oct 2016 18:15:27 +0200 Subject: [PATCH 16/31] DOCS Specify that the selectors change the scope (#6213) Follow up of issue #4015. --- .../01_Templates/02_Common_Variables.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md index af2d798f2..9a4e9295d 100644 --- a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -326,7 +326,7 @@ When in a particular scope, `$Up` takes the scope back to the previous level. <% end_loop %> Given the following structure, it will output the text. - + My Page | +-+ Child 1 @@ -341,6 +341,16 @@ Given the following structure, it will output the text. Page 'Grandchild 1' is a grandchild of 'My Page' Page 'Child 2' is a child of 'MyPage' +
+Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect. +
+ + :::ss +

Children of '$Title'

+ <% loop $Children.Sort('Title').First %> + <%-- We have two additional selectors in the loop expression so... --%> +

Page '$Title' is a child of '$Up.Up.Up.Title'

+ <% end_loop %> ### Top From 7778357b03c2e295d8c4760f18472b6d4fc7e1cc Mon Sep 17 00:00:00 2001 From: Matthew Hailwood Date: Tue, 25 Oct 2016 13:26:56 +1300 Subject: [PATCH 17/31] Switch Mandrill recommendation to sparkpost. (#6196) Now that Mandrill has become a paid part of Campaign Monitor it makes sense for us to recommend the free alternative SparkPost. The package I've linked to is by the same author as the original Mandrill package. --- docs/en/02_Developer_Guides/10_Email/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/02_Developer_Guides/10_Email/index.md b/docs/en/02_Developer_Guides/10_Email/index.md index 1c495e704..59948212d 100644 --- a/docs/en/02_Developer_Guides/10_Email/index.md +++ b/docs/en/02_Developer_Guides/10_Email/index.md @@ -9,7 +9,7 @@ covers how to create an `Email` instance, customise it with a HTML template, the Out of the box, SilverStripe will use the built-in PHP `mail()` command. If you are not running an SMTP server, you will need to either configure PHP's SMTP settings (see [PHP documentation](http://php.net/mail) to include your mail -server configuration or use one of the third party SMTP services like [Mandrill](https://github.com/lekoala/silverstripe-mandrill) +server configuration or use one of the third party SMTP services like [SparkPost](https://github.com/lekoala/silverstripe-sparkpost) and [Postmark](https://github.com/fullscreeninteractive/silverstripe-postmarkmailer). ## Usage From c4aed0c081ef6d56ce1375228cd8110db329f168 Mon Sep 17 00:00:00 2001 From: zauberfisch Date: Wed, 26 Oct 2016 11:35:52 +0000 Subject: [PATCH 18/31] Added $showSearchForm to ModelAdmin --- admin/code/ModelAdmin.php | 19 +++++++++++++--- .../templates/Includes/ModelAdmin_Content.ss | 4 +++- admin/templates/Includes/ModelAdmin_Tools.ss | 22 ++++++++++--------- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 2a807059c..652beb737 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -65,10 +65,17 @@ abstract class ModelAdmin extends LeftAndMain { /** * Change this variable if you don't want the Import from CSV form to appear. * This variable can be a boolean or an array. - * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClasstwo') + * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo') */ public $showImportForm = true; + /** + * Change this variable if you don't want the search form to appear. + * This variable can be a boolean or an array. + * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo') + */ + public $showSearchForm = true; + /** * List of all {@link DataObject}s which can be imported through * a subclass of {@link BulkLoader} (mostly CSV data). @@ -182,9 +189,14 @@ abstract class ModelAdmin extends LeftAndMain { } /** - * @return Form + * @return Form|bool */ public function SearchForm() { + if(!$this->showSearchForm || + (is_array($this->showSearchForm) && !in_array($this->modelClass, $this->showSearchForm)) + ) { + return false; + } $context = $this->getSearchContext(); $form = new Form($this, "SearchForm", $context->getSearchFields(), @@ -326,7 +338,7 @@ abstract class ModelAdmin extends LeftAndMain { /** * Generate a CSV import form for a single {@link DataObject} subclass. * - * @return Form + * @return Form|bool */ public function ImportForm() { $modelSNG = singleton($this->modelClass); @@ -402,6 +414,7 @@ abstract class ModelAdmin extends LeftAndMain { * @param array $data * @param Form $form * @param SS_HTTPRequest $request + * @return bool|null */ public function import($data, $form, $request) { if(!$this->showImportForm || (is_array($this->showImportForm) diff --git a/admin/templates/Includes/ModelAdmin_Content.ss b/admin/templates/Includes/ModelAdmin_Content.ss index 1a86d17e9..6179c0758 100644 --- a/admin/templates/Includes/ModelAdmin_Content.ss +++ b/admin/templates/Includes/ModelAdmin_Content.ss @@ -16,7 +16,9 @@
- + <% if $SearchForm || $ImportForm %> + + <% end_if %>
    <% loop $ManagedModelTabs %>
  • diff --git a/admin/templates/Includes/ModelAdmin_Tools.ss b/admin/templates/Includes/ModelAdmin_Tools.ss index 753c81f5a..9909449c3 100644 --- a/admin/templates/Includes/ModelAdmin_Tools.ss +++ b/admin/templates/Includes/ModelAdmin_Tools.ss @@ -1,11 +1,13 @@ -
    - <% if $SearchForm %> -

    <%t ModelAdmin_Tools_ss.FILTER 'Filter' %>

    - $SearchForm - <% end_if %> +<% if $SearchForm || $ImportForm %> +
    + <% if $SearchForm %> +

    <%t ModelAdmin_Tools_ss.FILTER 'Filter' %>

    + $SearchForm + <% end_if %> - <% if $ImportForm %> -

    <%t ModelAdmin_Tools_ss.IMPORT 'Import' %>

    - $ImportForm - <% end_if %> -
    + <% if $ImportForm %> +

    <%t ModelAdmin_Tools_ss.IMPORT 'Import' %>

    + $ImportForm + <% end_if %> +
    +<% end_if %> From d1c29d65954a3b66a2f1106a44adc8fde0edb82e Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 27 Oct 2016 09:06:11 +1300 Subject: [PATCH 19/31] Remove double quotes so example is not parsed. [Notice] Undefined variable: map Since it's using double quotes it tries to process $map, $key and $value --- model/Map.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/Map.php b/model/Map.php index 2d4ca6b08..1b6e59380 100644 --- a/model/Map.php +++ b/model/Map.php @@ -196,7 +196,7 @@ class SS_Map implements ArrayAccess, Countable, IteratorAggregate { } user_error( - "SS_Map is read-only. Please use $map->push($key, $value) to append values", + 'SS_Map is read-only. Please use $map->push($key, $value) to append values', E_USER_ERROR ); } From 747bd4cac00383fffea66dea75f7e21e13df7088 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Sun, 30 Oct 2016 12:04:07 +0000 Subject: [PATCH 20/31] FIX filterAny error message now refers to correct method name --- model/DataList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/DataList.php b/model/DataList.php index d50f20e13..305be0fbb 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -434,7 +434,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab } elseif($numberFuncArgs == 2) { $whereArguments[func_get_arg(0)] = func_get_arg(1); } else { - throw new InvalidArgumentException('Incorrect number of arguments passed to exclude()'); + throw new InvalidArgumentException('Incorrect number of arguments passed to filterAny()'); } return $this->alterDataQuery(function($query, $list) use ($whereArguments) { From fe816076fc5a2b3b1e497b8c51c76430311eea2c Mon Sep 17 00:00:00 2001 From: Nicola Fontana Date: Sun, 16 Oct 2016 17:48:41 +0200 Subject: [PATCH 21/31] BUG Make simplexml_load_file work on shared php-fpm PHP #62577 [1] together with PHP #64938 [2] make simplexml_load_file() fails when the "disable load external entities" flag is set. As a workaround, manually enable the entity loader in the bootstrap code. We are loading internal XML files after all, so no security implications should arise. [1] https://bugs.php.net/bug.php?id=62577 [2] https://bugs.php.net/bug.php?id=64938 Fix #6174. --- main.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.php b/main.php index 3c62c9514..e9fa011ee 100644 --- a/main.php +++ b/main.php @@ -62,6 +62,9 @@ if(!empty($_SERVER['HTTP_X_ORIGINAL_URL'])) { $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL']; } +// Enable the entity loader to be able to load XML in Zend_Locale_Data +libxml_disable_entity_loader(false); + /** * Figure out the request URL */ From 0b850e0819831b9cbfbf4632f2932825a042a1b8 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 31 Oct 2016 13:33:59 +0000 Subject: [PATCH 22/31] DOCS Docblock fixes for ValidationResult --- model/ValidationResult.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/model/ValidationResult.php b/model/ValidationResult.php index 75cdee244..b014d4b20 100644 --- a/model/ValidationResult.php +++ b/model/ValidationResult.php @@ -8,19 +8,21 @@ */ class ValidationResult extends Object { /** - * Boolean - is the result valid or not + * @var bool - is the result valid or not */ protected $isValid; /** - * Array of errors + * @var array of errors */ protected $errorList = array(); /** * Create a new ValidationResult. * By default, it is a successful result. Call $this->error() to record errors. + * @param bool $valid + * @param string|null $message */ public function __construct($valid = true, $message = null) { $this->isValid = $valid; @@ -30,8 +32,8 @@ class ValidationResult extends Object { /** * Record an error against this validation result, - * @param $message The validation error message - * @param $code An optional error code string, that can be accessed with {@link $this->codeList()}. + * @param string $message The validation error message + * @param string $code An optional error code string, that can be accessed with {@link $this->codeList()}. * @return ValidationResult this */ public function error($message, $code = null) { @@ -99,7 +101,7 @@ class ValidationResult extends Object { * It will be valid if both this and the other result are valid. * This object will be modified to contain the new validation information. * - * @param ValidationResult the validation result object to combine + * @param ValidationResult $other the validation result object to combine * @return ValidationResult this */ public function combineAnd(ValidationResult $other) { From fdfd0c4fc370338016c87d8403cbb6bd21e72db2 Mon Sep 17 00:00:00 2001 From: Jono Menz Date: Mon, 31 Oct 2016 21:56:32 -0700 Subject: [PATCH 23/31] DOCS Remove duped content (#6214) D.R.Y. --- .../01_Templates/01_Syntax.md | 19 +++++- .../01_Templates/02_Common_Variables.md | 67 +------------------ 2 files changed, 17 insertions(+), 69 deletions(-) diff --git a/docs/en/02_Developer_Guides/01_Templates/01_Syntax.md b/docs/en/02_Developer_Guides/01_Templates/01_Syntax.md index 5806195be..11b01de0b 100644 --- a/docs/en/02_Developer_Guides/01_Templates/01_Syntax.md +++ b/docs/en/02_Developer_Guides/01_Templates/01_Syntax.md @@ -412,7 +412,7 @@ When in a particular scope, `$Up` takes the scope back to the previous level. <% end_loop %> Given the following structure, it will output the text. - + My Page | +-+ Child 1 @@ -427,6 +427,16 @@ Given the following structure, it will output the text. Page 'Grandchild 1' is a grandchild of 'My Page' Page 'Child 2' is a child of 'MyPage' +
    +Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect. +
    + + :::ss +

    Children of '$Title'

    + <% loop $Children.Sort('Title').First %> + <%-- We have two additional selectors in the loop expression so... --%> +

    Page '$Title' is a child of '$Up.Up.Up.Title'

    + <% end_loop %> #### Top @@ -444,8 +454,6 @@ page. The previous example could be rewritten to use the following syntax. <% end_loop %> <% end_loop %> - - ### With The `<% with %>` tag lets you change into a new scope. Consider the following example: @@ -466,7 +474,12 @@ Outside the `<% with %>.`, we are in the page scope. Inside it, we are in the sc refer directly to properties and methods of the [api:Member] object. `$FirstName` inside the scope is equivalent to `$CurrentMember.FirstName`. +### Me +`$Me` outputs the current object in scope. This will call the `forTemplate` of the object. + + :::ss + $Me ## Comments diff --git a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md index 9a4e9295d..d0351e2de 100644 --- a/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md +++ b/docs/en/02_Developer_Guides/01_Templates/02_Common_Variables.md @@ -302,72 +302,7 @@ For example, imagine you're on the "bob marley" page, which is three levels in: ## Navigating Scope -### Me - -`$Me` outputs the current object in scope. This will call the `forTemplate` of the object. - - :::ss - $Me - - -### Up - -When in a particular scope, `$Up` takes the scope back to the previous level. - - :::ss -

    Children of '$Title'

    - - <% loop $Children %> -

    Page '$Title' is a child of '$Up.Title'

    - - <% loop $Children %> -

    Page '$Title' is a grandchild of '$Up.Up.Title'

    - <% end_loop %> - <% end_loop %> - -Given the following structure, it will output the text. - - My Page - | - +-+ Child 1 - | | - | +- Grandchild 1 - | - +-+ Child 2 - - Children of 'My Page' - - Page 'Child 1' is a child of 'My Page' - Page 'Grandchild 1' is a grandchild of 'My Page' - Page 'Child 2' is a child of 'MyPage' - -
    -Additional selectors implicitely change the scope so you need to put additional `$Up` to get what you expect. -
    - - :::ss -

    Children of '$Title'

    - <% loop $Children.Sort('Title').First %> - <%-- We have two additional selectors in the loop expression so... --%> -

    Page '$Title' is a child of '$Up.Up.Up.Title'

    - <% end_loop %> - -### Top - -While `$Up` provides us a way to go up one level of scope, `$Top` is a shortcut to jump to the top most scope of the -page. The previous example could be rewritten to use the following syntax. - - :::ss -

    Children of '$Title'

    - - <% loop $Children %> -

    Page '$Title' is a child of '$Top.Title'

    - - <% loop $Children %> -

    Page '$Title' is a grandchild of '$Top.Title'

    - <% end_loop %> - <% end_loop %> - +See [scope](syntax#scope). ## Breadcrumbs From c61d61d00324e764022489968b5a114271793522 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Tue, 1 Nov 2016 20:58:03 +0000 Subject: [PATCH 24/31] FIX default_records are no longer inherited to child classes --- model/DataObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/DataObject.php b/model/DataObject.php index 1ac147fcd..dded6024f 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -3503,7 +3503,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @uses DataExtension->requireDefaultRecords() */ public function requireDefaultRecords() { - $defaultRecords = $this->stat('default_records'); + $defaultRecords = $this->config()->get('default_records', Config::UNINHERITED); if(!empty($defaultRecords)) { $hasData = DataObject::get_one($this->class); From 567b125fbccc766dd7bb3dd219644cdcd2660a94 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 2 Nov 2016 14:08:35 +0000 Subject: [PATCH 25/31] DOCS Fix up some PHPDoc on Versioned --- model/Versioned.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/model/Versioned.php b/model/Versioned.php index c17d11f12..af1648b65 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -10,7 +10,6 @@ * @subpackage model * * @property DataObject owner - * * @property int RecordID * @property int Version * @property bool WasPublished @@ -78,7 +77,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { "AuthorID" => "Int", "PublisherID" => "Int" ); - + /** + * @var array + * @config + */ private static $db = array( 'Version' => 'Int' ); @@ -583,6 +585,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * Generates a ($table)_version DB manipulation and injects it into the current $manipulation * * @param SQLQuery $manipulation The query to augment + * @param string $table + * @param string|int $recordID */ protected function augmentWriteVersioned(&$manipulation, $table, $recordID) { $baseDataClass = ClassInfo::baseDataClass($table); @@ -643,7 +647,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * * @param array $manipulation Source manipulation data * @param string $table Name of table - * @param int $recordID ID of record to version + * @param string|int $recordID ID of record to version */ protected function augmentWriteStaged(&$manipulation, $table, $recordID) { // If the record has already been inserted in the (table), get rid of it. @@ -659,7 +663,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { unset($manipulation[$table]); } - + /** + * @param array $manipulation + */ public function augmentWrite(&$manipulation) { // get Version number from base data table on write $version = null; @@ -1143,8 +1149,6 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * - If $_GET['stage'] is set, then it will use that stage, and store it in the session. * - If $_GET['archiveDate'] is set, it will use that date, and store it in the session. * - If neither of these are set, it checks the session, otherwise the stage is set to 'Live'. - * - * @param Session $session Optional session within which to store the resulting stage */ public static function choose_site_stage() { // Check any pre-existing session mode @@ -1521,6 +1525,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { $labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this page'); } + /** + * @param FieldList $fields + */ public function updateCMSFields(FieldList $fields) { // remove the version field from the CMS as this should be left // entirely up to the extension (not the cms user). @@ -1569,6 +1576,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { return $this->defaultStage; } + /** + * @return array + */ public static function get_template_global_variables() { return array( 'CurrentReadingMode' => 'get_reading_mode' @@ -1592,6 +1602,9 @@ class Versioned_Version extends ViewableData { /** @var DataObject */ protected $object; + /** + * @param array $record + */ public function __construct($record) { $this->record = $record; $record['ID'] = $record['RecordID']; From 135a64761fac74cc7ac75640551c5a14874ade95 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 3 Nov 2016 14:47:30 +1300 Subject: [PATCH 26/31] FIX: Ensure that builds use the 3.4 dependencies. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fix does it in a way that doesn’t need manual maintenance per-branch --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f0226c4c9..350136846 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ addons: env: global: - - CORE_RELEASE=3 - "ARTIFACTS_AWS_REGION=us-east-1" - "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts" - secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg=" @@ -33,6 +32,7 @@ matrix: env: DB=MYSQL BEHAT_TEST=1 CMS_TEST=1 before_script: + - export CORE_RELEASE=$TRAVIS_BRANCH - if ! [ $(phpenv version-name) = "5.3" ]; then printf "\n" | pecl install imagick; fi - if [ $(phpenv version-name) = "5.3" ]; then printf "\n" | pecl install imagick-3.3.0; fi - composer self-update || true From edfe514540aae0772f49225f3614ce045ad9e1d4 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Thu, 3 Nov 2016 14:47:30 +1300 Subject: [PATCH 27/31] FIX: Ensure that builds use the 3.4 dependencies. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fix does it in a way that doesn’t need manual maintenance per-branch --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 349f64da0..c4b007afa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ addons: env: global: - - CORE_RELEASE=3 - "ARTIFACTS_AWS_REGION=us-east-1" - "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts" - secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg=" @@ -29,6 +28,7 @@ matrix: env: DB=MYSQL BEHAT_TEST=1 before_script: + - export CORE_RELEASE=$TRAVIS_BRANCH - if ! [ $(phpenv version-name) = "5.3" ]; then printf "\n" | pecl install imagick; fi - if [ $(phpenv version-name) = "5.3" ]; then printf "\n" | pecl install imagick-3.3.0; fi - composer self-update || true From c914dde7d11337e6a5f802b0d446fb7ad40b7841 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Thu, 3 Nov 2016 13:17:41 +0000 Subject: [PATCH 28/31] Update travis build to use target branch as core release --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5974b62be..a1e0489e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ addons: env: global: - - CORE_RELEASE=3.1 - "ARTIFACTS_AWS_REGION=us-east-1" - "ARTIFACTS_S3_BUCKET=silverstripe-travis-artifacts" - secure: "DjwZKhY/c0wXppGmd8oEMiTV0ayfOXiCmi9Lg1aXoSXNnj+sjLmhYwhUWjehjR6IX0MRtzJG6v7V5Y+4nSGe+i+XIrBQnhPQ95Jrkm1gKofX2mznWTl9npQElNS1DXi58NLPbiB3qxHWGFBRAWmRQrsAouyZabkPnChnSa9ldOg=" @@ -29,6 +28,7 @@ matrix: env: DB=MYSQL BEHAT_TEST=1 before_script: + - export CORE_RELEASE=$TRAVIS_BRANCH - composer self-update || true - phpenv rehash - git clone git://github.com/silverstripe-labs/silverstripe-travis-support.git ~/travis-support From 27206c9240d97b589e4e682bf2063cbe49d83bdf Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 7 Nov 2016 13:42:47 +0000 Subject: [PATCH 29/31] Revert "Added $showSearchForm to ModelAdmin" --- admin/code/ModelAdmin.php | 19 +++------------- .../templates/Includes/ModelAdmin_Content.ss | 4 +--- admin/templates/Includes/ModelAdmin_Tools.ss | 22 +++++++++---------- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 652beb737..2a807059c 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -65,17 +65,10 @@ abstract class ModelAdmin extends LeftAndMain { /** * Change this variable if you don't want the Import from CSV form to appear. * This variable can be a boolean or an array. - * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo') + * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClasstwo') */ public $showImportForm = true; - /** - * Change this variable if you don't want the search form to appear. - * This variable can be a boolean or an array. - * If array, you can list className you want the form to appear on. i.e. array('myClassOne','myClassTwo') - */ - public $showSearchForm = true; - /** * List of all {@link DataObject}s which can be imported through * a subclass of {@link BulkLoader} (mostly CSV data). @@ -189,14 +182,9 @@ abstract class ModelAdmin extends LeftAndMain { } /** - * @return Form|bool + * @return Form */ public function SearchForm() { - if(!$this->showSearchForm || - (is_array($this->showSearchForm) && !in_array($this->modelClass, $this->showSearchForm)) - ) { - return false; - } $context = $this->getSearchContext(); $form = new Form($this, "SearchForm", $context->getSearchFields(), @@ -338,7 +326,7 @@ abstract class ModelAdmin extends LeftAndMain { /** * Generate a CSV import form for a single {@link DataObject} subclass. * - * @return Form|bool + * @return Form */ public function ImportForm() { $modelSNG = singleton($this->modelClass); @@ -414,7 +402,6 @@ abstract class ModelAdmin extends LeftAndMain { * @param array $data * @param Form $form * @param SS_HTTPRequest $request - * @return bool|null */ public function import($data, $form, $request) { if(!$this->showImportForm || (is_array($this->showImportForm) diff --git a/admin/templates/Includes/ModelAdmin_Content.ss b/admin/templates/Includes/ModelAdmin_Content.ss index 6179c0758..1a86d17e9 100644 --- a/admin/templates/Includes/ModelAdmin_Content.ss +++ b/admin/templates/Includes/ModelAdmin_Content.ss @@ -16,9 +16,7 @@
- <% if $SearchForm || $ImportForm %> - - <% end_if %> +
    <% loop $ManagedModelTabs %>
  • diff --git a/admin/templates/Includes/ModelAdmin_Tools.ss b/admin/templates/Includes/ModelAdmin_Tools.ss index 9909449c3..753c81f5a 100644 --- a/admin/templates/Includes/ModelAdmin_Tools.ss +++ b/admin/templates/Includes/ModelAdmin_Tools.ss @@ -1,13 +1,11 @@ -<% if $SearchForm || $ImportForm %> -
    - <% if $SearchForm %> -

    <%t ModelAdmin_Tools_ss.FILTER 'Filter' %>

    - $SearchForm - <% end_if %> +
    + <% if $SearchForm %> +

    <%t ModelAdmin_Tools_ss.FILTER 'Filter' %>

    + $SearchForm + <% end_if %> - <% if $ImportForm %> -

    <%t ModelAdmin_Tools_ss.IMPORT 'Import' %>

    - $ImportForm - <% end_if %> -
    -<% end_if %> + <% if $ImportForm %> +

    <%t ModelAdmin_Tools_ss.IMPORT 'Import' %>

    + $ImportForm + <% end_if %> +
    From 1ec56a120208eb8c82a9df0c54910ee6aa133139 Mon Sep 17 00:00:00 2001 From: Martijn Date: Thu, 20 Oct 2016 14:04:15 +0200 Subject: [PATCH 30/31] Prevent undefined index notices --- admin/code/ModelAdmin.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 2a807059c..b73289ac2 100644 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -213,12 +213,12 @@ abstract class ModelAdmin extends LeftAndMain { if(is_array($params)) { $params = ArrayLib::array_map_recursive('trim', $params); - } - // Parse all DateFields to handle user input non ISO 8601 dates - foreach($context->getFields() as $field) { - if($field instanceof DatetimeField) { - $params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()])); + // Parse all DateFields to handle user input non ISO 8601 dates + foreach($context->getFields() as $field) { + if($field instanceof DatetimeField && !empty($params[$field->getName()])) { + $params[$field->getName()] = date('Y-m-d', strtotime($params[$field->getName()])); + } } } From ffd993865299522c66b0dd91beeab35dde1da5fb Mon Sep 17 00:00:00 2001 From: Jonathon Menz Date: Mon, 7 Nov 2016 12:45:29 -0800 Subject: [PATCH 31/31] API ShortcodeParser getter and extension points --- parsers/ShortcodeParser.php | 147 +++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 60 deletions(-) diff --git a/parsers/ShortcodeParser.php b/parsers/ShortcodeParser.php index f3047d7a5..6ea8867cb 100644 --- a/parsers/ShortcodeParser.php +++ b/parsers/ShortcodeParser.php @@ -21,6 +21,13 @@ class ShortcodeParser extends Object { // -------------------------------------------------------------------------------------------------------------- + /** + * Registered shortcodes. Items follow this structure: + * [shortcode_name] => Array( + * [0] => class_containing_handler + * [1] => name_of_shortcode_handler_method + * ) + */ protected $shortcodes = array(); // -------------------------------------------------------------------------------------------------------------- @@ -96,6 +103,15 @@ class ShortcodeParser extends Object { if($this->registered($shortcode)) unset($this->shortcodes[$shortcode]); } + /** + * Get an array containing information about registered shortcodes + * + * @return array + */ + public function getRegisteredShortcodes() { + return $this->shortcodes; + } + /** * Remove all registered shortcodes. */ @@ -540,79 +556,90 @@ class ShortcodeParser extends Object { * @return string */ public function parse($content) { + + $this->extend('onBeforeParse', $content); + + $continue = true; + // If no shortcodes defined, don't try and parse any - if(!$this->shortcodes) return $content; + if(!$this->shortcodes) $continue = false; // If no content, don't try and parse it - if (!trim($content)) return $content; + else if (!trim($content)) $continue = false; // If no shortcode tag, don't try and parse it - if (strpos($content, '[') === false) return $content; + else if (strpos($content, '[') === false) $continue = false; - // First we operate in text mode, replacing any shortcodes with marker elements so that later we can - // use a proper DOM - list($content, $tags) = $this->replaceElementTagsWithMarkers($content); + if ($continue) { + // First we operate in text mode, replacing any shortcodes with marker elements so that later we can + // use a proper DOM + list($content, $tags) = $this->replaceElementTagsWithMarkers($content); - $htmlvalue = Injector::inst()->create('HTMLValue', $content); + $htmlvalue = Injector::inst()->create('HTMLValue', $content); - // Now parse the result into a DOM - if (!$htmlvalue->isValid()){ - if(self::$error_behavior == self::ERROR) { - user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR); - } - else { - return $content; - } - } - - // First, replace any shortcodes that are in attributes - $this->replaceAttributeTagsWithContent($htmlvalue); - - // Find all the element scoped shortcode markers - $shortcodes = $htmlvalue->query('//img[@class="'.self::$marker_class.'"]'); - - // Find the parents. Do this before DOM modification, since SPLIT might cause parents to move otherwise - $parents = $this->findParentsForMarkers($shortcodes); - - foreach($shortcodes as $shortcode) { - $tag = $tags[$shortcode->getAttribute('data-tagid')]; - $parent = $parents[$shortcode->getAttribute('data-parentid')]; - - $class = null; - if(!empty($tag['attrs']['location'])) $class = $tag['attrs']['location']; - else if(!empty($tag['attrs']['class'])) $class = $tag['attrs']['class']; - - $location = self::INLINE; - if($class == 'left' || $class == 'right') $location = self::BEFORE; - if($class == 'center' || $class == 'leftALone') $location = self::SPLIT; - - if(!$parent) { - if($location !== self::INLINE) { - user_error("Parent block for shortcode couldn't be found, but location wasn't INLINE", - E_USER_ERROR); + // Now parse the result into a DOM + if (!$htmlvalue->isValid()){ + if(self::$error_behavior == self::ERROR) { + user_error('Couldn\'t decode HTML when processing short codes', E_USER_ERRROR); + } + else { + $continue = false; } } - else { - $this->moveMarkerToCompliantHome($shortcode, $parent, $location); - } - - $this->replaceMarkerWithContent($shortcode, $tag); } - $content = $htmlvalue->getContent(); + if ($continue) { + // First, replace any shortcodes that are in attributes + $this->replaceAttributeTagsWithContent($htmlvalue); - // Clean up any marker classes left over, for example, those injected into