mirror of
https://github.com/symbiote/silverstripe-gridfieldextensions.git
synced 2024-10-22 17:05:39 +02:00
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.
This commit is contained in:
parent
ba0d23ab5e
commit
9fa9ef8903
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Symbiote\GridFieldExtensions;
|
namespace Symbiote\GridFieldExtensions;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use SilverStripe\Control\Controller;
|
use SilverStripe\Control\Controller;
|
||||||
use SilverStripe\Control\RequestHandler;
|
use SilverStripe\Control\RequestHandler;
|
||||||
use SilverStripe\Core\ClassInfo;
|
use SilverStripe\Core\ClassInfo;
|
||||||
@ -17,9 +18,11 @@ use SilverStripe\ORM\ArrayList;
|
|||||||
use SilverStripe\ORM\DataList;
|
use SilverStripe\ORM\DataList;
|
||||||
use SilverStripe\ORM\DataObject;
|
use SilverStripe\ORM\DataObject;
|
||||||
use SilverStripe\ORM\DataObjectInterface;
|
use SilverStripe\ORM\DataObjectInterface;
|
||||||
|
use SilverStripe\ORM\DataObjectSchema;
|
||||||
use SilverStripe\ORM\DB;
|
use SilverStripe\ORM\DB;
|
||||||
use SilverStripe\ORM\ManyManyList;
|
use SilverStripe\ORM\ManyManyList;
|
||||||
use SilverStripe\ORM\Map;
|
use SilverStripe\ORM\ManyManyThroughList;
|
||||||
|
use SilverStripe\ORM\ManyManyThroughQueryManipulator;
|
||||||
use SilverStripe\ORM\SS_List;
|
use SilverStripe\ORM\SS_List;
|
||||||
use SilverStripe\ORM\FieldType\DBDatetime;
|
use SilverStripe\ORM\FieldType\DBDatetime;
|
||||||
use SilverStripe\Versioned\Versioned;
|
use SilverStripe\Versioned\Versioned;
|
||||||
@ -141,6 +144,16 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
return $this->extraSortFields;
|
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.
|
* Sets extra sort fields to apply before the sort field.
|
||||||
*
|
*
|
||||||
@ -183,12 +196,19 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
{
|
{
|
||||||
$field = $this->getSortField();
|
$field = $this->getSortField();
|
||||||
|
|
||||||
|
// Check extra fields on many many relation types
|
||||||
if ($list instanceof ManyManyList) {
|
if ($list instanceof ManyManyList) {
|
||||||
$extra = $list->getExtraFields();
|
$extra = $list->getExtraFields();
|
||||||
|
|
||||||
if ($extra && array_key_exists($field, $extra)) {
|
if ($extra && array_key_exists($field, $extra)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} elseif ($list instanceof ManyManyThroughList) {
|
||||||
|
$manipulator = $this->getManyManyInspector($list);
|
||||||
|
$fieldTable = DataObject::getSchema()->tableForField($manipulator->getJoinClass(), $field);
|
||||||
|
if ($fieldTable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
$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 . "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -217,6 +237,8 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
if ($extra && array_key_exists($field, $extra)) {
|
if ($extra && array_key_exists($field, $extra)) {
|
||||||
return $table;
|
return $table;
|
||||||
}
|
}
|
||||||
|
} elseif ($list instanceof ManyManyThroughList) {
|
||||||
|
return $this->getManyManyInspector($list)->getJoinAlias();
|
||||||
}
|
}
|
||||||
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
$classes = ClassInfo::dataClassesFor($list->dataClass());
|
||||||
foreach ($classes as $class) {
|
foreach ($classes as $class) {
|
||||||
@ -224,7 +246,7 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
return DataObject::getSchema()->tableName($class);
|
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)
|
public function getURLHandlers($grid)
|
||||||
@ -340,9 +362,10 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
}
|
}
|
||||||
$list = $grid->getList();
|
$list = $grid->getList();
|
||||||
$modelClass = $grid->getModelClass();
|
$modelClass = $grid->getModelClass();
|
||||||
if ($list instanceof ManyManyList && !singleton($modelClass)->canView()) {
|
$isManyMany = $this->isManyMany($list);
|
||||||
|
if ($isManyMany && !singleton($modelClass)->canView()) {
|
||||||
$this->httpError(403);
|
$this->httpError(403);
|
||||||
} elseif (!($list instanceof ManyManyList) && !singleton($modelClass)->canEdit()) {
|
} elseif (!$isManyMany && !singleton($modelClass)->canEdit()) {
|
||||||
$this->httpError(403);
|
$this->httpError(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,10 +387,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
|
* @param array $data Raw posted data
|
||||||
* @return array
|
* @return array [sortIndex => recordID]
|
||||||
*/
|
*/
|
||||||
protected function getSortedIDs($data)
|
protected function getSortedIDs($data)
|
||||||
{
|
{
|
||||||
@ -473,7 +496,7 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
if (!is_array($sortedIDs)) {
|
if (!is_array($sortedIDs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$field = $this->getSortField();
|
$sortField = $this->getSortField();
|
||||||
|
|
||||||
$sortterm = '';
|
$sortterm = '';
|
||||||
if ($this->extraSortFields) {
|
if ($this->extraSortFields) {
|
||||||
@ -486,7 +509,7 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$list = $grid->getList();
|
$list = $grid->getList();
|
||||||
$sortterm .= '"'.$this->getSortTable($list).'"."'.$field.'"';
|
$sortterm .= '"'.$this->getSortTable($list).'"."'.$sortField.'"';
|
||||||
$items = $list->filter('ID', $sortedIDs)->sort($sortterm);
|
$items = $list->filter('ID', $sortedIDs)->sort($sortterm);
|
||||||
|
|
||||||
// Ensure that each provided ID corresponded to an actual object.
|
// Ensure that each provided ID corresponded to an actual object.
|
||||||
@ -507,11 +530,21 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
if (isset($record->_SortColumn0)) {
|
if (isset($record->_SortColumn0)) {
|
||||||
$current[$record->ID] = $record->_SortColumn0;
|
$current[$record->ID] = $record->_SortColumn0;
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
$current = $items->map('ID', $field)->toArray();
|
$current = $items->map('ID', $sortField)->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the actual re-ordering.
|
// Perform the actual re-ordering.
|
||||||
@ -519,35 +552,51 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
return true;
|
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)
|
protected function reorderItems($list, array $values, array $sortedIDs)
|
||||||
{
|
{
|
||||||
|
// setup
|
||||||
$sortField = $this->getSortField();
|
$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);
|
|
||||||
$isVersioned = false;
|
|
||||||
$class = $list->dataClass();
|
$class = $list->dataClass();
|
||||||
|
// 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
|
// check if sort column is present on the model provided by dataClass() and if it's versioned
|
||||||
// cases:
|
// cases:
|
||||||
// Model has sort column and is versioned - handle as versioned
|
// Model has sort column and is versioned - handle as versioned
|
||||||
// Model has sort column and is NOT versioned - handle as NOT 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 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);
|
||||||
|
|
||||||
// try to match table name, note that we have to cover the case where the table which has the sort column
|
if ($listClassVersioned xor $isVersioned) {
|
||||||
// belongs to ancestor of the object which is populating the list
|
throw new Exception(
|
||||||
$classes = ClassInfo::ancestry($class, true);
|
'ManyManyThrough cannot mismatch Versioning between join class and related class'
|
||||||
foreach ($classes as $currentClass) {
|
);
|
||||||
if (DataObject::getSchema()->tableName($currentClass) == $this->getSortTable($list)) {
|
|
||||||
$isVersioned = $class::has_extension(Versioned::class);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
} elseif (!$isManyMany) {
|
||||||
|
$isVersioned = $class::create()->hasExtension(Versioned::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through each item, and update the sort values which do not
|
// Loop through each item, and update the sort values which do not
|
||||||
@ -556,22 +605,22 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
$sortTable = $this->getSortTable($list);
|
$sortTable = $this->getSortTable($list);
|
||||||
$now = DBDatetime::now()->Rfc2822();
|
$now = DBDatetime::now()->Rfc2822();
|
||||||
$additionalSQL = '';
|
$additionalSQL = '';
|
||||||
$baseTable = DataObject::getSchema()->baseDataTable($list->dataClass());
|
$baseTable = DataObject::getSchema()->baseDataTable($class);
|
||||||
|
|
||||||
$isBaseTable = ($baseTable == $sortTable);
|
$isBaseTable = ($baseTable == $sortTable);
|
||||||
if (!$list instanceof ManyManyList && $isBaseTable) {
|
if (!$list instanceof ManyManyList && $isBaseTable) {
|
||||||
$additionalSQL = ", \"LastEdited\" = '$now'";
|
$additionalSQL = ", \"LastEdited\" = '$now'";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($sortedIDs as $sortValue => $id) {
|
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
||||||
if ($map[$id] != $sortValue) {
|
if ($currentSortList[$targetRecordID]->$sortField != $newSortValue) {
|
||||||
DB::query(sprintf(
|
DB::query(sprintf(
|
||||||
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
'UPDATE "%s" SET "%s" = %d%s WHERE %s',
|
||||||
$sortTable,
|
$sortTable,
|
||||||
$sortField,
|
$sortField,
|
||||||
$sortValue,
|
$newSortValue,
|
||||||
$additionalSQL,
|
$additionalSQL,
|
||||||
$this->getSortTableClauseForIds($list, $id)
|
$this->getSortTableClauseForIds($list, $targetRecordID)
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!$isBaseTable && !$list instanceof ManyManyList) {
|
if (!$isBaseTable && !$list instanceof ManyManyList) {
|
||||||
@ -579,20 +628,22 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
||||||
$baseTable,
|
$baseTable,
|
||||||
$now,
|
$now,
|
||||||
$this->getSortTableClauseForIds($list, $id)
|
$this->getSortTableClauseForIds($list, $targetRecordID)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For versioned objects, modify them with the ORM so that the
|
// 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
|
// similar to the SiteTree where you change the position, and then
|
||||||
// you go into the record and publish it.
|
// you go into the record and publish it.
|
||||||
foreach ($sortedIDs as $sortValue => $id) {
|
foreach ($sortedIDs as $newSortValue => $targetRecordID) {
|
||||||
if ($map[$id] != $sortValue) {
|
// either the list data class (has_many, (belongs_)many_many)
|
||||||
$record = $class::get()->byID($id);
|
// or the intermediary join class (many_many through)
|
||||||
$record->$sortField = $sortValue;
|
$record = $currentSortList[$targetRecordID];
|
||||||
|
if ($record->$sortField != $newSortValue) {
|
||||||
|
$record->$sortField = $newSortValue;
|
||||||
$record->write();
|
$record->write();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -629,7 +680,7 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
$this->getSortTableClauseForIds($list, $id)
|
$this->getSortTableClauseForIds($list, $id)
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!$isBaseTable && !$list instanceof ManyManyList) {
|
if (!$isBaseTable && !$this->isManyMany($list)) {
|
||||||
DB::query(sprintf(
|
DB::query(sprintf(
|
||||||
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
'UPDATE "%s" SET "LastEdited" = \'%s\' WHERE %s',
|
||||||
$baseTable,
|
$baseTable,
|
||||||
@ -640,6 +691,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)
|
protected function getSortTableClauseForIds(DataList $list, $ids)
|
||||||
{
|
{
|
||||||
if (is_array($ids)) {
|
if (is_array($ids)) {
|
||||||
@ -648,10 +711,13 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
$value = '= ' . (int) $ids;
|
$value = '= ' . (int) $ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($list instanceof ManyManyList) {
|
if ($this->isManyMany($list)) {
|
||||||
$extra = $list->getExtraFields();
|
$intropector = $this->getManyManyInspector($list);
|
||||||
$key = $list->getLocalKey();
|
$extra = $list instanceof ManyManyList ?
|
||||||
$foreignKey = $list->getForeignKey();
|
$intropector->getExtraFields() :
|
||||||
|
DataObjectSchema::create()->fieldSpecs($intropector->getJoinClass(), DataObjectSchema::DB_ONLY);
|
||||||
|
$key = $intropector->getLocalKey();
|
||||||
|
$foreignKey = $intropector->getForeignKey();
|
||||||
$foreignID = (int) $list->getForeignID();
|
$foreignID = (int) $list->getForeignID();
|
||||||
|
|
||||||
if ($extra && array_key_exists($this->getSortField(), $extra)) {
|
if ($extra && array_key_exists($this->getSortField(), $extra)) {
|
||||||
@ -667,4 +733,27 @@ class GridFieldOrderableRows extends RequestHandler implements
|
|||||||
|
|
||||||
return "\"ID\" $value";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user