From 41f6267c234291eb48394280f9767d0754c0b9f6 Mon Sep 17 00:00:00 2001 From: Martin Portevin Date: Tue, 12 Jun 2018 16:17:31 +0200 Subject: [PATCH 1/4] Fix "+" icon in add new inline button --- .../Symbiote/GridFieldExtensions/GridFieldAddNewInlineButton.ss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Symbiote/GridFieldExtensions/GridFieldAddNewInlineButton.ss b/templates/Symbiote/GridFieldExtensions/GridFieldAddNewInlineButton.ss index c63a65a..40f70de 100644 --- a/templates/Symbiote/GridFieldExtensions/GridFieldAddNewInlineButton.ss +++ b/templates/Symbiote/GridFieldExtensions/GridFieldAddNewInlineButton.ss @@ -1,3 +1,3 @@ - From 688c29ba049cd56dcfccc6d15204b2b92d145bd0 Mon Sep 17 00:00:00 2001 From: Robbie Averill Date: Mon, 25 Jun 2018 14:16:05 +1200 Subject: [PATCH 2/4] Add various recipe versions to Travis build matrix --- .travis.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e18a3b..74cc970 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,25 +2,25 @@ language: php env: global: - - COMPOSER_ROOT_VERSION="4.0.x-dev" + - COMPOSER_ROOT_VERSION="3.1.x-dev" matrix: include: - php: 5.6 - env: DB=MYSQL PHPCS_TEST=1 PHPUNIT_TEST=1 + env: DB=MYSQL RECIPE_VERSION=1.0.x-dev PHPCS_TEST=1 PHPUNIT_TEST=1 - php: 7.0 - env: DB=PGSQL PHPUNIT_TEST=1 + env: DB=PGSQL RECIPE_VERSION=1.1.x-dev PHPUNIT_TEST=1 - php: 7.1 - env: DB=MYSQL PHPUNIT_COVERAGE_TEST=1 + env: DB=MYSQL RECIPE_VERSION=4.2.x-dev PHPUNIT_COVERAGE_TEST=1 - php: 7.2 - env: DB=MYSQL PHPUNIT_TEST=1 + env: DB=MYSQL RECIPE_VERSION=4.x-dev PHPUNIT_TEST=1 before_script: - phpenv rehash - phpenv config-rm xdebug.ini - composer validate - - composer require silverstripe/recipe-core 1.0.x-dev --no-update + - composer require silverstripe/recipe-core "$RECIPE_VERSION" --no-update - if [[ $DB == PGSQL ]]; then composer require silverstripe/postgresql:2.0.x-dev --no-update; fi - composer install --prefer-dist --no-interaction --no-progress --no-suggest --optimize-autoloader --verbose --profile From 95f0acb0f473b44a9019016180c200f7a40f1c11 Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Fri, 4 May 2018 17:31:00 +1200 Subject: [PATCH 3/4] NEW Add support for ManyManyThrough relations Previously relationships defiend as many_many came in a special type of RelationList - however now this can be one of two types of RelationList depending on the type of definition, with both being valid many_many relationships. This had the unfortunate side effect of seeing the OrderableRows component in (the least) cease functioning correctly. No longer. This also has the fortunate bonus of allowing a many_many relationship to be versioned; where previously while each item in the relationship could be versioned, the relationship itself could not. --- src/GridFieldOrderableRows.php | 180 ++++++++++++++++++++++++++------- 1 file changed, 142 insertions(+), 38 deletions(-) diff --git a/src/GridFieldOrderableRows.php b/src/GridFieldOrderableRows.php index 56538f0..13d6e60 100755 --- a/src/GridFieldOrderableRows.php +++ b/src/GridFieldOrderableRows.php @@ -2,6 +2,7 @@ namespace Symbiote\GridFieldExtensions; +use Exception; use SilverStripe\Control\Controller; use SilverStripe\Control\RequestHandler; use SilverStripe\Core\ClassInfo; @@ -17,9 +18,11 @@ use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectInterface; +use SilverStripe\ORM\DataObjectSchema; use SilverStripe\ORM\DB; use SilverStripe\ORM\ManyManyList; -use SilverStripe\ORM\Map; +use SilverStripe\ORM\ManyManyThroughList; +use SilverStripe\ORM\ManyManyThroughQueryManipulator; use SilverStripe\ORM\SS_List; use SilverStripe\ORM\FieldType\DBDatetime; use SilverStripe\Versioned\Versioned; @@ -141,6 +144,16 @@ class GridFieldOrderableRows extends RequestHandler implements return $this->extraSortFields; } + /** + * Checks to see if the relationship list is for a type of many_many + * + * @param SS_List $list + */ + protected function isManyMany(SS_List $list) + { + return $list instanceof ManyManyList || $list instanceof ManyManyThroughList; + } + /** * Sets extra sort fields to apply before the sort field. * @@ -183,12 +196,19 @@ class GridFieldOrderableRows extends RequestHandler implements { $field = $this->getSortField(); + // Check extra fields on many many relation types if ($list instanceof ManyManyList) { $extra = $list->getExtraFields(); if ($extra && array_key_exists($field, $extra)) { return; } + } elseif ($list instanceof ManyManyThroughList) { + $manipulator = $this->getManyManyInspector($list); + $fieldTable = DataObject::getSchema()->tableForField($manipulator->getJoinClass(), $field); + if ($fieldTable) { + return; + } } $classes = ClassInfo::dataClassesFor($list->dataClass()); @@ -199,7 +219,7 @@ class GridFieldOrderableRows extends RequestHandler implements } } - throw new \Exception("Couldn't find the sort field '" . $field . "'"); + throw new Exception("Couldn't find the sort field '" . $field . "'"); } /** @@ -219,6 +239,8 @@ class GridFieldOrderableRows extends RequestHandler implements if ($extra && array_key_exists($field, $extra)) { return $table; } + } elseif ($list instanceof ManyManyThroughList) { + return $this->getManyManyInspector($list)->getJoinAlias(); } $classes = ClassInfo::dataClassesFor($list->dataClass()); @@ -228,8 +250,7 @@ class GridFieldOrderableRows extends RequestHandler implements return DataObject::getSchema()->tableName($class); } } - - throw new \Exception("Couldn't find the sort field '$field'"); + throw new Exception("Couldn't find the sort field '$field'"); } public function getURLHandlers($grid) @@ -345,9 +366,10 @@ class GridFieldOrderableRows extends RequestHandler implements } $list = $grid->getList(); $modelClass = $grid->getModelClass(); - if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) { + $isManyMany = $this->isManyMany($list); + if ($isManyMany && !singleton($modelClass)->canView()) { $this->httpError(403); - } elseif (!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) { + } elseif (!$isManyMany && !singleton($modelClass)->canEdit()) { $this->httpError(403); } @@ -369,10 +391,10 @@ class GridFieldOrderableRows extends RequestHandler implements } /** - * Get mapping of sort value to ID from posted data + * Get mapping of sort value to item ID from posted data (gridfield list state), ordered by sort value. * * @param array $data Raw posted data - * @return array + * @return array [sortIndex => recordID] */ protected function getSortedIDs($data) { @@ -478,7 +500,7 @@ class GridFieldOrderableRows extends RequestHandler implements if (!is_array($sortedIDs)) { return false; } - $field = $this->getSortField(); + $sortField = $this->getSortField(); $sortterm = ''; if ($this->extraSortFields) { @@ -491,7 +513,7 @@ class GridFieldOrderableRows extends RequestHandler implements } } $list = $grid->getList(); - $sortterm .= '"'.$this->getSortTable($list).'"."'.$field.'"'; + $sortterm .= '"'.$this->getSortTable($list).'"."'.$sortField.'"'; $items = $list->filter('ID', $sortedIDs)->sort($sortterm); // Ensure that each provided ID corresponded to an actual object. @@ -512,11 +534,21 @@ class GridFieldOrderableRows extends RequestHandler implements if (isset($record->_SortColumn0)) { $current[$record->ID] = $record->_SortColumn0; } else { - $current[$record->ID] = $record->$field; + $current[$record->ID] = $record->$sortField; } } + } elseif ($items instanceof ManyManyThroughList) { + $manipulator = $this->getManyManyInspector($list); + $joinClass = $manipulator->getJoinClass(); + $fromRelationName = $manipulator->getForeignKey(); + $toRelationName = $manipulator->getLocalKey(); + $sortlist = DataList::create($joinClass)->filter([ + $toRelationName => $items->column('ID'), + $fromRelationName => $items->first()->getJoin()->$fromRelationName, + ]); + $current = $sortlist->map($toRelationName, $sortField)->toArray(); } else { - $current = $items->map('ID', $field)->toArray(); + $current = $items->map('ID', $sortField)->toArray(); } // Perform the actual re-ordering. @@ -524,20 +556,52 @@ class GridFieldOrderableRows extends RequestHandler implements return true; } + /** + * @param SS_List $list + * @param array $values **UNUSED** [listItemID => currentSortValue]; + * @param array $sortedIDs [newSortValue => listItemID] + */ protected function reorderItems($list, array $values, array $sortedIDs) { + // setup $sortField = $this->getSortField(); - /** @var SS_List $map */ - $map = $list->map('ID', $sortField); - //fix for versions of SS that return inconsistent types for `map` function - if ($map instanceof Map) { - $map = $map->toArray(); - } - - // If not a ManyManyList and using versioning, detect it. - $this->validateSortField($list); $class = $list->dataClass(); - $isVersioned = $class::has_extension(Versioned::class); + // The problem is that $sortedIDs is a list of the _related_ item IDs, which causes trouble + // with ManyManyThrough, where we need the ID of the _join_ item in order to set the value. + $itemToSortReference = ($list instanceof ManyManyThroughList) ? 'getJoin' : 'Me'; + $currentSortList = $list->map('ID', $itemToSortReference)->toArray(); + + // sanity check. + $this->validateSortField($list); + + $isVersioned = false; + // check if sort column is present on the model provided by dataClass() and if it's versioned + // cases: + // Model has sort column and is versioned - handle as versioned + // Model has sort column and is NOT versioned - handle as NOT versioned + // Model doesn't have sort column because sort column is on ManyManyList - handle as NOT versioned + // Model doesn't have sort column because sort column is on ManyManyThroughList... + // - Related item is not versioned: + // - Through object is versioned: THROW an error. + // - Through object is NOT versioned: handle as NOT versioned + // - Related item is versioned... + // - Through object is versioned: handle as versioned + // - Through object is NOT versioned: THROW an error. + $isManyMany = $this->isManyMany($list); + if ($isManyMany && $list instanceof ManyManyThroughList) { + $listClassVersioned = $class::create()->hasExtension(Versioned::class); + // We'll be updating the join class, not the relation class. + $class = $this->getManyManyInspector($list)->getJoinClass(); + $isVersioned = $class::create()->hasExtension(Versioned::class); + + if ($listClassVersioned xor $isVersioned) { + throw new Exception( + 'ManyManyThrough cannot mismatch Versioning between join class and related class' + ); + } + } elseif (!$isManyMany) { + $isVersioned = $class::create()->hasExtension(Versioned::class); + } // Loop through each item, and update the sort values which do not // match to order the objects. @@ -545,22 +609,22 @@ class GridFieldOrderableRows extends RequestHandler implements $sortTable = $this->getSortTable($list); $now = DBDatetime::now()->Rfc2822(); $additionalSQL = ''; - $baseTable = DataObject::getSchema()->baseDataTable($list->dataClass()); + $baseTable = DataObject::getSchema()->baseDataTable($class); $isBaseTable = ($baseTable == $sortTable); if (!$list instanceof ManyManyList && $isBaseTable) { $additionalSQL = ", \"LastEdited\" = '$now'"; } - foreach ($sortedIDs as $sortValue => $id) { - if ($map[$id] != $sortValue) { + foreach ($sortedIDs as $newSortValue => $targetRecordID) { + if ($currentSortList[$targetRecordID]->$sortField != $newSortValue) { DB::query(sprintf( 'UPDATE "%s" SET "%s" = %d%s WHERE %s', $sortTable, $sortField, - $sortValue, + $newSortValue, $additionalSQL, - $this->getSortTableClauseForIds($list, $id) + $this->getSortTableClauseForIds($list, $targetRecordID) )); if (!$isBaseTable && !$list instanceof ManyManyList) { @@ -568,20 +632,22 @@ class GridFieldOrderableRows extends RequestHandler implements 'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s', $baseTable, $now, - $this->getSortTableClauseForIds($list, $id) + $this->getSortTableClauseForIds($list, $targetRecordID) )); } } } } else { // For versioned objects, modify them with the ORM so that the - // *_versions table is updated. This ensures re-ordering works + // *_Versions table is updated. This ensures re-ordering works // similar to the SiteTree where you change the position, and then // you go into the record and publish it. - foreach ($sortedIDs as $sortValue => $id) { - if ($map[$id] != $sortValue) { - $record = $class::get()->byID($id); - $record->$sortField = $sortValue; + foreach ($sortedIDs as $newSortValue => $targetRecordID) { + // either the list data class (has_many, (belongs_)many_many) + // or the intermediary join class (many_many through) + $record = $currentSortList[$targetRecordID]; + if ($record->$sortField != $newSortValue) { + $record->$sortField = $newSortValue; $record->write(); } } @@ -618,7 +684,7 @@ class GridFieldOrderableRows extends RequestHandler implements $this->getSortTableClauseForIds($list, $id) )); - if (!$isBaseTable && !$list instanceof ManyManyList) { + if (!$isBaseTable && !$this->isManyMany($list)) { DB::query(sprintf( 'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s', $baseTable, @@ -629,6 +695,18 @@ class GridFieldOrderableRows extends RequestHandler implements } } + /** + * Forms a WHERE clause for the table the sort column is defined on. + * e.g. ID = 5 + * e.g. ID IN(5, 8, 10) + * e.g. SortOrder = 5 AND RelatedThing.ID = 3 + * e.g. SortOrder IN(5, 8, 10) AND RelatedThing.ID = 3 + * + * @param DataList $list + * @param int|string|array $ids a single number, or array of numbers + * + * @return string + */ protected function getSortTableClauseForIds(DataList $list, $ids) { if (is_array($ids)) { @@ -637,10 +715,13 @@ class GridFieldOrderableRows extends RequestHandler implements $value = '= ' . (int) $ids; } - if ($list instanceof ManyManyList) { - $extra = $list->getExtraFields(); - $key = $list->getLocalKey(); - $foreignKey = $list->getForeignKey(); + if ($this->isManyMany($list)) { + $intropector = $this->getManyManyInspector($list); + $extra = $list instanceof ManyManyList ? + $intropector->getExtraFields() : + DataObjectSchema::create()->fieldSpecs($intropector->getJoinClass(), DataObjectSchema::DB_ONLY); + $key = $intropector->getLocalKey(); + $foreignKey = $intropector->getForeignKey(); $foreignID = (int) $list->getForeignID(); if ($extra && array_key_exists($this->getSortField(), $extra)) { @@ -656,4 +737,27 @@ class GridFieldOrderableRows extends RequestHandler implements return "\"ID\" $value"; } + + /** + * A ManyManyList defines functions such as getLocalKey, however on ManyManyThroughList + * these functions are moved to ManyManyThroughQueryManipulator, but otherwise retain + * the same signature. + * + * @param ManyManyList|ManyManyThroughList + * + * @return ManyManyList|ManyManyThroughQueryManipulator + */ + protected function getManyManyInspector($list) + { + $inspector = $list; + if ($list instanceof ManyManyThroughList) { + foreach ($list->dataQuery()->getDataQueryManipulators() as $manipulator) { + if ($manipulator instanceof ManyManyThroughQueryManipulator) { + $inspector = $manipulator; + break; + } + } + } + return $inspector; + } } From bba85470549623f01e86da8c666730179e13932f Mon Sep 17 00:00:00 2001 From: Dylan Wagstaff Date: Mon, 25 Jun 2018 12:22:27 +1200 Subject: [PATCH 4/4] Add unit tests for new ManyManyThrough support The previous commit (9fa9ef89) added support for the new SilverStripe 4 feature of Many Many relationships through an intermediary object. After much head scratching and community testing, the solution was proven to work, however had no automated tests to confirm as such. This commit rectifies that by testing both versioned and unversioned DataObjects in a many_many through style relationship. Some minor tidy and comments were also added as per feedback on the functionality code changes. --- src/GridFieldOrderableRows.php | 23 ++-- tests/GridFieldOrderableRowsTest.php | 121 +++++++++++++++++-- tests/OrderableRowsThroughTest.yml | 30 +++++ tests/OrderableRowsThroughVersionedTest.php | 126 ++++++++++++++++++++ tests/Stub/StubParent.php | 19 +-- tests/Stub/ThroughBelongs.php | 15 +++ tests/Stub/ThroughDefiner.php | 23 ++++ tests/Stub/ThroughIntermediary.php | 21 ++++ 8 files changed, 346 insertions(+), 32 deletions(-) create mode 100644 tests/OrderableRowsThroughTest.yml create mode 100644 tests/OrderableRowsThroughVersionedTest.php create mode 100644 tests/Stub/ThroughBelongs.php create mode 100644 tests/Stub/ThroughDefiner.php create mode 100644 tests/Stub/ThroughIntermediary.php diff --git a/src/GridFieldOrderableRows.php b/src/GridFieldOrderableRows.php index 13d6e60..c0c9e87 100755 --- a/src/GridFieldOrderableRows.php +++ b/src/GridFieldOrderableRows.php @@ -148,6 +148,8 @@ class GridFieldOrderableRows extends RequestHandler implements * Checks to see if the relationship list is for a type of many_many * * @param SS_List $list + * + * @return bool */ protected function isManyMany(SS_List $list) { @@ -497,7 +499,7 @@ class GridFieldOrderableRows extends RequestHandler implements */ protected function executeReorder(GridField $grid, $sortedIDs) { - if (!is_array($sortedIDs)) { + if (!is_array($sortedIDs) || empty($sortedIDs)) { return false; } $sortField = $this->getSortField(); @@ -544,6 +546,7 @@ class GridFieldOrderableRows extends RequestHandler implements $toRelationName = $manipulator->getLocalKey(); $sortlist = DataList::create($joinClass)->filter([ $toRelationName => $items->column('ID'), + // first() is safe as there are earlier checks to ensure our list to sort is valid $fromRelationName => $items->first()->getJoin()->$fromRelationName, ]); $current = $sortlist->map($toRelationName, $sortField)->toArray(); @@ -587,19 +590,19 @@ class GridFieldOrderableRows extends RequestHandler implements // - Related item is versioned... // - Through object is versioned: handle as versioned // - Through object is NOT versioned: THROW an error. - $isManyMany = $this->isManyMany($list); - if ($isManyMany && $list instanceof ManyManyThroughList) { + if ($list instanceof ManyManyThroughList) { $listClassVersioned = $class::create()->hasExtension(Versioned::class); // We'll be updating the join class, not the relation class. $class = $this->getManyManyInspector($list)->getJoinClass(); $isVersioned = $class::create()->hasExtension(Versioned::class); + // If one side of the relationship is versioned and the other is not, throw an error. if ($listClassVersioned xor $isVersioned) { throw new Exception( 'ManyManyThrough cannot mismatch Versioning between join class and related class' ); } - } elseif (!$isManyMany) { + } elseif (!$this->isManyMany($list)) { $isVersioned = $class::create()->hasExtension(Versioned::class); } @@ -716,12 +719,12 @@ class GridFieldOrderableRows extends RequestHandler implements } if ($this->isManyMany($list)) { - $intropector = $this->getManyManyInspector($list); + $introspector = $this->getManyManyInspector($list); $extra = $list instanceof ManyManyList ? - $intropector->getExtraFields() : - DataObjectSchema::create()->fieldSpecs($intropector->getJoinClass(), DataObjectSchema::DB_ONLY); - $key = $intropector->getLocalKey(); - $foreignKey = $intropector->getForeignKey(); + $introspector->getExtraFields() : + DataObjectSchema::create()->fieldSpecs($introspector->getJoinClass(), DataObjectSchema::DB_ONLY); + $key = $introspector->getLocalKey(); + $foreignKey = $introspector->getForeignKey(); $foreignID = (int) $list->getForeignID(); if ($extra && array_key_exists($this->getSortField(), $extra)) { @@ -743,7 +746,7 @@ class GridFieldOrderableRows extends RequestHandler implements * these functions are moved to ManyManyThroughQueryManipulator, but otherwise retain * the same signature. * - * @param ManyManyList|ManyManyThroughList + * @param ManyManyList|ManyManyThroughList $list * * @return ManyManyList|ManyManyThroughQueryManipulator */ diff --git a/tests/GridFieldOrderableRowsTest.php b/tests/GridFieldOrderableRowsTest.php index d49674d..2f1a54e 100644 --- a/tests/GridFieldOrderableRowsTest.php +++ b/tests/GridFieldOrderableRowsTest.php @@ -12,16 +12,22 @@ use Symbiote\GridFieldExtensions\Tests\Stub\StubOrdered; use Symbiote\GridFieldExtensions\Tests\Stub\StubParent; use Symbiote\GridFieldExtensions\Tests\Stub\StubSubclass; use Symbiote\GridFieldExtensions\Tests\Stub\StubUnorderable; +use Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner; +use Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediary; +use Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs; /** * Tests for the {@link GridFieldOrderableRows} component. */ class GridFieldOrderableRowsTest extends SapphireTest { - - protected $usesDatabase = true; - - protected static $fixture_file = 'GridFieldOrderableRowsTest.yml'; + /** + * @var string + */ + protected static $fixture_file = [ + 'GridFieldOrderableRowsTest.yml', + 'OrderableRowsThroughTest.yml' + ]; protected static $extra_dataobjects = [ StubParent::class, @@ -29,27 +35,44 @@ class GridFieldOrderableRowsTest extends SapphireTest StubSubclass::class, StubUnorderable::class, StubOrderableChild::class, + StubOrderedVersioned::class, + StubSubclassOrderedVersioned::class, + ThroughDefiner::class, + ThroughIntermediary::class, + ThroughBelongs::class, ]; - public function testReorderItems() + public function reorderItemsProvider() { - $orderable = new GridFieldOrderableRows('ManyManySort'); + return [ + [StubParent::class . '.parent', 'MyManyMany', 'ManyManySort'], + [ThroughDefiner::class . '.DefinerOne', 'Belongings', 'Sort'], + ]; + } + + /** + * @dataProvider reorderItemsProvider + */ + public function testReorderItems($fixtureID, $relationName, $sortName) + { + $orderable = new GridFieldOrderableRows($sortName); $reflection = new ReflectionMethod($orderable, 'executeReorder'); $reflection->setAccessible(true); - $parent = $this->objFromFixture(StubParent::class, 'parent'); - $config = new GridFieldConfig_RelationEditor(); $config->addComponent($orderable); + list($parentClass, $parentInstanceID) = explode('.', $fixtureID); + $parent = $this->objFromFixture($parentClass, $parentInstanceID); + $grid = new GridField( - 'MyManyMany', - 'My Many Many', - $parent->MyManyMany()->sort('ManyManySort'), + $relationName, + 'Testing Many Many', + $parent->$relationName()->sort($sortName), $config ); - $originalOrder = $parent->MyManyMany()->sort('ManyManySort')->column('ID'); + $originalOrder = $parent->$relationName()->sort($sortName)->column('ID'); $desiredOrder = []; // Make order non-contiguous, and 1-based @@ -61,7 +84,7 @@ class GridFieldOrderableRowsTest extends SapphireTest $reflection->invoke($orderable, $grid, $desiredOrder); - $newOrder = $parent->MyManyMany()->sort('ManyManySort')->map('ManyManySort', 'ID')->toArray(); + $newOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray(); $this->assertEquals($desiredOrder, $newOrder); } @@ -125,5 +148,77 @@ class GridFieldOrderableRowsTest extends SapphireTest 'StubParent_MyManyMany', $orderable->setSortField('ManyManySort')->getSortTable($parent->MyManyMany()) ); + + $this->assertEquals( + 'StubOrderedVersioned', + $orderable->setSortField('Sort')->getSortTable($parent->MyHasManySubclassOrderedVersioned()) + ); + } + + public function testReorderItemsSubclassVersioned() + { + $orderable = new GridFieldOrderableRows('Sort'); + $reflection = new ReflectionMethod($orderable, 'executeReorder'); + $reflection->setAccessible(true); + + $parent = $this->objFromFixture(StubParent::class, 'parent-subclass-ordered-versioned'); + + // make sure all items are published + foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) { + $item->publishRecursive(); + } + + // there should be no difference between stages at this point + $differenceFound = false; + foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) { + /** @var StubSubclassOrderedVersioned|Versioned $item */ + if ($item->stagesDiffer()) { + $this->fail('Unexpected difference found on stages'); + } + } + + // reorder items + $config = new GridFieldConfig_RelationEditor(); + $config->addComponent($orderable); + + $grid = new GridField( + 'TestField', + 'TestField', + $parent->MyHasManySubclassOrderedVersioned()->sort('Sort', 'ASC'), + $config + ); + + $originalOrder = $parent->MyHasManySubclassOrderedVersioned() + ->sort('Sort', 'ASC') + ->column('ID'); + + $desiredOrder = []; + + // Make order non-contiguous, and 1-based + foreach (array_reverse($originalOrder) as $index => $id) { + $desiredOrder[$index * 2 + 1] = $id; + } + + $this->assertNotEquals($originalOrder, $desiredOrder); + + $reflection->invoke($orderable, $grid, $desiredOrder); + + $newOrder = $parent->MyHasManySubclassOrderedVersioned() + ->sort('Sort', 'ASC') + ->map('Sort', 'ID') + ->toArray(); + + $this->assertEquals($desiredOrder, $newOrder); + + // reorder should have been handled as versioned - there should be a difference between stages now + $differenceFound = false; + foreach ($parent->MyHasManySubclassOrderedVersioned() as $item) { + if ($item->stagesDiffer()) { + $differenceFound = true; + break; + } + } + + $this->assertTrue($differenceFound); } } diff --git a/tests/OrderableRowsThroughTest.yml b/tests/OrderableRowsThroughTest.yml new file mode 100644 index 0000000..a0e11ea --- /dev/null +++ b/tests/OrderableRowsThroughTest.yml @@ -0,0 +1,30 @@ +Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner: + DefinerOne: + DefinerTwo: + +Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs: + BelongsOne: + BelongsTwo: + BelongsThree: + +Symbiote\GridFieldExtensions\Tests\Stub\ThroughIntermediary: + One: + Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne + Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsOne + Sort: 3 + Two: + Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne + Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsTwo + Sort: 2 + Three: + Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerOne + Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsThree + Sort: 1 + Four: + Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerTwo + Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsTwo + Sort: 1 + Five: + Defining: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughDefiner.DefinerTwo + Belonging: =>Symbiote\GridFieldExtensions\Tests\Stub\ThroughBelongs.BelongsThree + Sort: 2 diff --git a/tests/OrderableRowsThroughVersionedTest.php b/tests/OrderableRowsThroughVersionedTest.php new file mode 100644 index 0000000..694da01 --- /dev/null +++ b/tests/OrderableRowsThroughVersionedTest.php @@ -0,0 +1,126 @@ + [Versioned::class], + ThroughIntermediary::class => [Versioned::class], + ThroughBelongs::class => [Versioned::class], + ]; + + protected $originalReadingMode; + + protected function setUp() + { + parent::setUp(); + $this->orignalReadingMode = Versioned::get_reading_mode(); + } + + protected function tearDown() + { + Versioned::set_reading_mode($this->originalReadingMode); + unset($this->originalReadingMode); + parent::tearDown(); + } + + /** + * Basically the same as GridFieldOrderableRowsTest::testReorderItems + * but with some Versioned calls & checks mixed in. + */ + public function testReorderingSavesAndPublishes() + { + $parent = $this->objFromFixture(ThroughDefiner::class, 'DefinerOne'); + $relationName = 'Belongings'; + $sortName = 'Sort'; + + $orderable = new GridFieldOrderableRows($sortName); + $reflection = new ReflectionMethod($orderable, 'executeReorder'); + $reflection->setAccessible(true); + + $config = new GridFieldConfig_RelationEditor(); + $config->addComponent($orderable); + + // This test data is versioned - ensure we're all published + $parent->publishRecursive(); + // there should be no difference between stages at this point + foreach ($parent->$relationName() as $item) { + $this->assertFalse( + $item->getJoin()->stagesDiffer(), + 'No records should be different from their published versions' + ); + } + + $grid = new GridField( + 'Belongings', + 'Testing Many Many', + $parent->$relationName()->sort($sortName), + $config + ); + + $originalOrder = $parent->$relationName()->sort($sortName)->column('ID'); + // Ring (un)shift by one, e.g. 3,2,1 becomes 1,3,2. + // then string key our new order starting at 1 + $desiredOrder = array_values($originalOrder); + array_unshift($desiredOrder, array_pop($desiredOrder)); + $desiredOrder = array_combine( + range('1', count($desiredOrder)), + $desiredOrder + ); + $this->assertNotEquals($originalOrder, $desiredOrder); + + // Perform the reorder + $reflection->invoke($orderable, $grid, $desiredOrder); + + // Verify draft stage has reordered + Versioned::set_stage(Versioned::DRAFT); + $newOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray(); + $this->assertEquals($desiredOrder, $newOrder); + + // reorder should have been handled as versioned - there should be a difference between stages now + // by using a ring style shift every item should have a new sort (thus a new version). + $differenceFound = false; + foreach ($parent->$relationName() as $item) { + if ($item->getJoin()->stagesDiffer()) { + $differenceFound = true; + } + } + $this->assertTrue($differenceFound, 'All records should have changes in draft'); + + // Verify live stage has NOT reordered + Versioned::set_stage(Versioned::LIVE); + $sameOrder = $parent->$relationName()->sort($sortName)->column('ID'); + $this->assertEquals($originalOrder, $sameOrder); + + $parent->publishRecursive(); + + foreach ($parent->$relationName() as $item) { + $this->assertFalse( + $item->getJoin()->stagesDiffer(), + 'No records should be different from their published versions anymore' + ); + } + + $newLiveOrder = $parent->$relationName()->sort($sortName)->map($sortName, 'ID')->toArray(); + $this->assertEquals($desiredOrder, $newLiveOrder); + } +} diff --git a/tests/Stub/StubParent.php b/tests/Stub/StubParent.php index 7d60a11..8904318 100644 --- a/tests/Stub/StubParent.php +++ b/tests/Stub/StubParent.php @@ -7,18 +7,19 @@ use SilverStripe\ORM\DataObject; class StubParent extends DataObject implements TestOnly { - private static $has_many = array( + private static $has_many = [ 'MyHasMany' => StubOrdered::class, - 'MyHasManySubclass' => StubSubclass::class - ); + 'MyHasManySubclass' => StubSubclass::class, + 'MyHasManySubclassOrderedVersioned' => StubSubclassOrderedVersioned::class, + ]; - private static $many_many = array( - 'MyManyMany' => StubOrdered::class - ); + private static $many_many = [ + 'MyManyMany' => StubOrdered::class, + ]; - private static $many_many_extraFields = array( - 'MyManyMany' => array('ManyManySort' => 'Int') - ); + private static $many_many_extraFields = [ + 'MyManyMany' => ['ManyManySort' => 'Int'], + ]; private static $table_name = 'StubParent'; } diff --git a/tests/Stub/ThroughBelongs.php b/tests/Stub/ThroughBelongs.php new file mode 100644 index 0000000..3c6cb07 --- /dev/null +++ b/tests/Stub/ThroughBelongs.php @@ -0,0 +1,15 @@ + ThroughDefiner::class, + ]; +} diff --git a/tests/Stub/ThroughDefiner.php b/tests/Stub/ThroughDefiner.php new file mode 100644 index 0000000..62ab202 --- /dev/null +++ b/tests/Stub/ThroughDefiner.php @@ -0,0 +1,23 @@ + [ + 'through' => ThroughIntermediary::class, + 'from' => 'Defining', + 'to' => 'Belonging', + ] + ]; + + private static $owns = [ + 'Belongings' + ]; +} diff --git a/tests/Stub/ThroughIntermediary.php b/tests/Stub/ThroughIntermediary.php new file mode 100644 index 0000000..12a48e1 --- /dev/null +++ b/tests/Stub/ThroughIntermediary.php @@ -0,0 +1,21 @@ + DBInt::class, + ]; + + private static $has_one = [ + 'Defining' => ThroughDefiner::class, + 'Belonging' => ThroughBelongs::class, + ]; +}