mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-29 12:49:06 +02:00
Merge pull request #5219 from open-sausages/pulls/4.0/optional-owned-by
API Improvements to ownership api
This commit is contained in:
commit
1e53f29f33
@ -12,6 +12,7 @@
|
|||||||
* @subpackage core
|
* @subpackage core
|
||||||
*/
|
*/
|
||||||
abstract class Extension {
|
abstract class Extension {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is used by extensions designed to be applied to controllers.
|
* This is used by extensions designed to be applied to controllers.
|
||||||
* It works the same way as {@link Controller::$allowed_actions}.
|
* It works the same way as {@link Controller::$allowed_actions}.
|
||||||
@ -32,10 +33,12 @@ abstract class Extension {
|
|||||||
protected $ownerBaseClass;
|
protected $ownerBaseClass;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference counter to ensure that the owner isn't cleared until clearOwner() has
|
* Ownership stack for recursive methods.
|
||||||
* been called as many times as setOwner()
|
* Last item is current owner.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
*/
|
*/
|
||||||
private $ownerRefs = 0;
|
private $ownerStack = [];
|
||||||
|
|
||||||
public $class;
|
public $class;
|
||||||
|
|
||||||
@ -55,6 +58,7 @@ abstract class Extension {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the owner of this extension.
|
* Set the owner of this extension.
|
||||||
|
*
|
||||||
* @param Object $owner The owner object,
|
* @param Object $owner The owner object,
|
||||||
* @param string $ownerBaseClass The base class that the extension is applied to; this may be
|
* @param string $ownerBaseClass The base class that the extension is applied to; this may be
|
||||||
* the class of owner, or it may be a parent. For example, if Versioned was applied to SiteTree,
|
* the class of owner, or it may be a parent. For example, if Versioned was applied to SiteTree,
|
||||||
@ -62,17 +66,32 @@ abstract class Extension {
|
|||||||
* would be 'SiteTree'.
|
* would be 'SiteTree'.
|
||||||
*/
|
*/
|
||||||
public function setOwner($owner, $ownerBaseClass = null) {
|
public function setOwner($owner, $ownerBaseClass = null) {
|
||||||
if($owner) $this->ownerRefs++;
|
if($owner) {
|
||||||
|
$this->ownerStack[] = $owner;
|
||||||
|
}
|
||||||
$this->owner = $owner;
|
$this->owner = $owner;
|
||||||
|
|
||||||
if($ownerBaseClass) $this->ownerBaseClass = $ownerBaseClass;
|
// Set ownerBaseClass
|
||||||
else if(!$this->ownerBaseClass && $owner) $this->ownerBaseClass = $owner->class;
|
if($ownerBaseClass) {
|
||||||
|
$this->ownerBaseClass = $ownerBaseClass;
|
||||||
|
} elseif(!$this->ownerBaseClass && $owner) {
|
||||||
|
$this->ownerBaseClass = get_class($owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the current owner, and restore extension to the state prior to the last setOwner()
|
||||||
|
*/
|
||||||
public function clearOwner() {
|
public function clearOwner() {
|
||||||
if($this->ownerRefs <= 0) user_error("clearOwner() called more than setOwner()", E_USER_WARNING);
|
if(empty($this->ownerStack)) {
|
||||||
$this->ownerRefs--;
|
throw new BadMethodCallException("clearOwner() called more than setOwner()");
|
||||||
if($this->ownerRefs == 0) $this->owner = null;
|
}
|
||||||
|
array_pop($this->ownerStack);
|
||||||
|
if($this->ownerStack) {
|
||||||
|
$this->owner = end($this->ownerStack);
|
||||||
|
} else {
|
||||||
|
$this->owner = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -97,7 +116,4 @@ abstract class Extension {
|
|||||||
return $parts[0];
|
return $parts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ is initialized. But it can also be set and reset temporarily to force a specific
|
|||||||
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
$obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records
|
||||||
Versioned::set_reading_mode($origMode); // reset current mode
|
Versioned::set_reading_mode($origMode); // reset current mode
|
||||||
|
|
||||||
### File ownership
|
### DataObject ownership
|
||||||
|
|
||||||
Typically when publishing versioned dataobjects, it is necessary to ensure that some linked components
|
Typically when publishing versioned dataobjects, it is necessary to ensure that some linked components
|
||||||
are published along with it. Unless this is done, site front-end content can appear incorrectly published.
|
are published along with it. Unless this is done, site front-end content can appear incorrectly published.
|
||||||
@ -160,8 +160,8 @@ The solution to this problem is the ownership API, which declares a two-way rela
|
|||||||
objects along database relations. This relationship is similar to many_many/belongs_many_many
|
objects along database relations. This relationship is similar to many_many/belongs_many_many
|
||||||
and has_one/has_many, however it relies on a pre-existing relationship to function.
|
and has_one/has_many, however it relies on a pre-existing relationship to function.
|
||||||
|
|
||||||
For instance, in order to specify this dependency, you must apply `owns` and `owned_by` config
|
For instance, in order to specify this dependency, you must apply `owns` on the owner to point to any
|
||||||
on a relationship.
|
owned relationships.
|
||||||
|
|
||||||
When pages of type `MyPage` are published, any owned images and banners will be automatically published,
|
When pages of type `MyPage` are published, any owned images and banners will be automatically published,
|
||||||
without requiring any custom code.
|
without requiring any custom code.
|
||||||
@ -185,33 +185,52 @@ without requiring any custom code.
|
|||||||
'Parent' => 'MyPage',
|
'Parent' => 'MyPage',
|
||||||
'Image' => 'Image',
|
'Image' => 'Image',
|
||||||
);
|
);
|
||||||
private static $owned_by = array(
|
|
||||||
'Parent'
|
|
||||||
);
|
|
||||||
private static $owns = array(
|
private static $owns = array(
|
||||||
'Image'
|
'Image'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class BannerImageExtension extends DataExtension {
|
|
||||||
private static $has_many = array(
|
Note that ownership cannot be used with polymorphic relations. E.g. has_one to non-type specific `DataObject`.
|
||||||
'Banners' => 'Banner'
|
|
||||||
|
#### DataObject ownership with custom relations
|
||||||
|
|
||||||
|
In some cases you might need to apply ownership where there is no underlying db relation, such as
|
||||||
|
those calculated at runtime based on business logic. In cases where you are not backing ownership
|
||||||
|
with standard relations (has_one, has_many, etc) it is necessary to declare ownership on both
|
||||||
|
sides of the relation.
|
||||||
|
|
||||||
|
This can be done by creating methods on both sides of your relation (e.g. parent and child class)
|
||||||
|
that can be used to traverse between each, and then by ensuring you configure both
|
||||||
|
`owns` config (on the parent) and `owned_by` (on the child).
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
|
||||||
|
:::php
|
||||||
|
class MyParent extends DataObject {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned'
|
||||||
|
);
|
||||||
|
private static $owns = array(
|
||||||
|
'ChildObjects'
|
||||||
|
);
|
||||||
|
public function ChildObjects() {
|
||||||
|
return MyChild::get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class MyChild extends DataObject {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned'
|
||||||
);
|
);
|
||||||
private static $owned_by = array(
|
private static $owned_by = array(
|
||||||
'Banners'
|
'Parent'
|
||||||
);
|
);
|
||||||
|
public function Parent() {
|
||||||
|
return MyParent::get()->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
With the config:
|
#### DataObject Ownership in HTML Content
|
||||||
|
|
||||||
:::yaml
|
|
||||||
Image:
|
|
||||||
extensions:
|
|
||||||
- BannerImageExtension
|
|
||||||
|
|
||||||
|
|
||||||
Note that it's important to define both `owns` and `owned_by` components of the relationship,
|
|
||||||
similar to how you would apply `has_one` and `has_many`, or `many_many` and `belongs_many_many`.
|
|
||||||
|
|
||||||
If you are using `[api:HTMLText]` or `[api:HTMLVarchar]` fields in your `DataObject::$db` definitions,
|
If you are using `[api:HTMLText]` or `[api:HTMLVarchar]` fields in your `DataObject::$db` definitions,
|
||||||
it's likely that your authors can insert images into those fields via the CMS interface.
|
it's likely that your authors can insert images into those fields via the CMS interface.
|
||||||
|
@ -1626,7 +1626,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
/**
|
/**
|
||||||
* Find the foreign class of a relation on this DataObject, regardless of the relation type.
|
* Find the foreign class of a relation on this DataObject, regardless of the relation type.
|
||||||
*
|
*
|
||||||
* @param $relationName Relation name.
|
* @param string $relationName Relation name.
|
||||||
* @return string Class name, or null if not found.
|
* @return string Class name, or null if not found.
|
||||||
*/
|
*/
|
||||||
public function getRelationClass($relationName) {
|
public function getRelationClass($relationName) {
|
||||||
@ -1654,6 +1654,137 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a relation name, determine the relation type
|
||||||
|
*
|
||||||
|
* @param string $component Name of component
|
||||||
|
* @return string has_one, has_many, many_many, belongs_many_many or belongs_to
|
||||||
|
*/
|
||||||
|
public function getRelationType($component) {
|
||||||
|
$types = array('has_one', 'has_many', 'many_many', 'belongs_many_many', 'belongs_to');
|
||||||
|
foreach($types as $type) {
|
||||||
|
$relations = Config::inst()->get($this->class, $type);
|
||||||
|
if($relations && isset($relations[$component])) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a relation declared on a remote class, generate a substitute component for the opposite
|
||||||
|
* side of the relation.
|
||||||
|
*
|
||||||
|
* Notes on behaviour:
|
||||||
|
* - This can still be used on components that are defined on both sides, but do not need to be.
|
||||||
|
* - All has_ones on remote class will be treated as local has_many, even if they are belongs_to
|
||||||
|
* - Cannot be used on polymorphic relationships
|
||||||
|
* - Cannot be used on unsaved objects.
|
||||||
|
*
|
||||||
|
* @param string $remoteClass
|
||||||
|
* @param string $remoteRelation
|
||||||
|
* @return DataList|DataObject The component, either as a list or single object
|
||||||
|
* @throws BadMethodCallException
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function inferReciprocalComponent($remoteClass, $remoteRelation) {
|
||||||
|
/** @var DataObject $remote */
|
||||||
|
$remote = $remoteClass::singleton();
|
||||||
|
$class = $remote->getRelationClass($remoteRelation);
|
||||||
|
|
||||||
|
// Validate arguments
|
||||||
|
if(!$this->isInDB()) {
|
||||||
|
throw new BadMethodCallException(__METHOD__ . " cannot be called on unsaved objects");
|
||||||
|
}
|
||||||
|
if(empty($class)) {
|
||||||
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
"%s invoked with invalid relation %s.%s",
|
||||||
|
__METHOD__,
|
||||||
|
$remoteClass,
|
||||||
|
$remoteRelation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if($class === 'DataObject') {
|
||||||
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
"%s cannot generate opposite component of relation %s.%s as it is polymorphic. " .
|
||||||
|
"This method does not support polymorphic relationships",
|
||||||
|
__METHOD__,
|
||||||
|
$remoteClass,
|
||||||
|
$remoteRelation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if(!is_a($this, $class, true)) {
|
||||||
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
"Relation %s on %s does not refer to objects of type %s",
|
||||||
|
$remoteRelation, $remoteClass, get_class($this)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the relation type to mock
|
||||||
|
$relationType = $remote->getRelationType($remoteRelation);
|
||||||
|
switch($relationType) {
|
||||||
|
case 'has_one': {
|
||||||
|
// Mock has_many
|
||||||
|
$joinField = "{$remoteRelation}ID";
|
||||||
|
$componentClass = ClassInfo::table_for_object_field($remoteClass, $joinField);
|
||||||
|
$result = HasManyList::create($componentClass, $joinField);
|
||||||
|
if ($this->model) {
|
||||||
|
$result->setDataModel($this->model);
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
->setDataQueryParam($this->getInheritableQueryParams())
|
||||||
|
->forForeignID($this->ID);
|
||||||
|
}
|
||||||
|
case 'belongs_to':
|
||||||
|
case 'has_many': {
|
||||||
|
// These relations must have a has_one on the other end, so find it
|
||||||
|
$joinField = $remote->getRemoteJoinField($remoteRelation, $relationType, $polymorphic);
|
||||||
|
if ($polymorphic) {
|
||||||
|
throw new InvalidArgumentException(sprintf(
|
||||||
|
"%s cannot generate opposite component of relation %s.%s, as the other end appears" .
|
||||||
|
"to be a has_one polymorphic. This method does not support polymorphic relationships",
|
||||||
|
__METHOD__,
|
||||||
|
$remoteClass,
|
||||||
|
$remoteRelation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
$joinID = $this->getField($joinField);
|
||||||
|
if (empty($joinID)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Get object by joined ID
|
||||||
|
return DataObject::get($remoteClass)
|
||||||
|
->filter('ID', $joinID)
|
||||||
|
->setDataQueryParam($this->getInheritableQueryParams())
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
case 'many_many':
|
||||||
|
case 'belongs_many_many': {
|
||||||
|
// Get components and extra fields from parent
|
||||||
|
list($componentClass, $parentClass, $componentField, $parentField, $table)
|
||||||
|
= $remote->manyManyComponent($remoteRelation);
|
||||||
|
$extraFields = $remote->manyManyExtraFieldsForComponent($remoteRelation) ?: array();
|
||||||
|
|
||||||
|
// Reverse parent and component fields and create an inverse ManyManyList
|
||||||
|
/** @var ManyManyList $result */
|
||||||
|
$result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields);
|
||||||
|
if($this->model) {
|
||||||
|
$result->setDataModel($this->model);
|
||||||
|
}
|
||||||
|
$this->extend('updateManyManyComponents', $result);
|
||||||
|
|
||||||
|
// If this is called on a singleton, then we return an 'orphaned relation' that can have the
|
||||||
|
// foreignID set elsewhere.
|
||||||
|
return $result
|
||||||
|
->setDataQueryParam($this->getInheritableQueryParams())
|
||||||
|
->forForeignID($this->ID);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to find the database key on another object that is used to store a
|
* Tries to find the database key on another object that is used to store a
|
||||||
* relationship to this class. If no join field can be found it defaults to 'ParentID'.
|
* relationship to this class. If no join field can be found it defaults to 'ParentID'.
|
||||||
|
@ -944,12 +944,112 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
* @param ArrayList $list Optional list to add items to
|
* @param ArrayList $list Optional list to add items to
|
||||||
* @return ArrayList list of objects
|
* @return ArrayList list of objects
|
||||||
*/
|
*/
|
||||||
public function findOwners($recursive = true, $list = null)
|
public function findOwners($recursive = true, $list = null) {
|
||||||
{
|
if (!$list) {
|
||||||
// Find objects in these relationships
|
$list = new ArrayList();
|
||||||
return $this->findRelatedObjects('owned_by', $recursive, $list);
|
}
|
||||||
|
|
||||||
|
// Build reverse lookup for ownership
|
||||||
|
// @todo - Cache this more intelligently
|
||||||
|
$rules = $this->lookupReverseOwners();
|
||||||
|
|
||||||
|
// Hand off to recursive method
|
||||||
|
return $this->findOwnersRecursive($recursive, $list, $rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find objects which own this object.
|
||||||
|
* Note that objects will only be searched in the same stage as the given record.
|
||||||
|
*
|
||||||
|
* @param bool $recursive True if recursive
|
||||||
|
* @param ArrayList $list List to add items to
|
||||||
|
* @param array $lookup List of reverse lookup rules for owned objects
|
||||||
|
* @return ArrayList list of objects
|
||||||
|
*/
|
||||||
|
public function findOwnersRecursive($recursive, $list, $lookup) {
|
||||||
|
// First pass: find objects that are explicitly owned_by (e.g. custom relationships)
|
||||||
|
$owners = $this->findRelatedObjects('owned_by', false);
|
||||||
|
|
||||||
|
// Second pass: Find owners via reverse lookup list
|
||||||
|
foreach($lookup as $ownedClass => $classLookups) {
|
||||||
|
// Skip owners of other objects
|
||||||
|
if(!is_a($this->owner, $ownedClass)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach($classLookups as $classLookup) {
|
||||||
|
// Merge new owners into this object's owners
|
||||||
|
$ownerClass = $classLookup['class'];
|
||||||
|
$ownerRelation = $classLookup['relation'];
|
||||||
|
$result = $this->owner->inferReciprocalComponent($ownerClass, $ownerRelation);
|
||||||
|
$this->mergeRelatedObjects($owners, $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge all objects into the main list
|
||||||
|
$newItems = $this->mergeRelatedObjects($list, $owners);
|
||||||
|
|
||||||
|
// If recursing, iterate over all newly added items
|
||||||
|
if($recursive) {
|
||||||
|
foreach($newItems as $item) {
|
||||||
|
/** @var Versioned|DataObject $item */
|
||||||
|
$item->findOwnersRecursive(true, $list, $lookup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a list of classes, each of which with a list of methods to invoke
|
||||||
|
* to lookup owners.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function lookupReverseOwners() {
|
||||||
|
// Find all classes with 'owns' config
|
||||||
|
$lookup = array();
|
||||||
|
foreach(ClassInfo::subclassesFor(DataObject::class) as $class) {
|
||||||
|
// Ensure this class is versioned
|
||||||
|
if(!Object::has_extension($class, Versioned::class)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check owned objects for this class
|
||||||
|
$owns = Config::inst()->get($class, 'owns', Config::UNINHERITED);
|
||||||
|
if(empty($owns)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var DataObject $instance */
|
||||||
|
$instance = $class::singleton();
|
||||||
|
foreach($owns as $owned) {
|
||||||
|
// Find owned class
|
||||||
|
$ownedClass = $instance->getRelationClass($owned);
|
||||||
|
// Skip custom methods that don't have db relationsm
|
||||||
|
if(!$ownedClass) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if($ownedClass === 'DataObject') {
|
||||||
|
throw new LogicException(sprintf(
|
||||||
|
"Relation %s on class %s cannot be owned as it is polymorphic",
|
||||||
|
$owned, $class
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lookup for owned class
|
||||||
|
if(!isset($lookup[$ownedClass])) {
|
||||||
|
$lookup[$ownedClass] = array();
|
||||||
|
}
|
||||||
|
$lookup[$ownedClass][] = [
|
||||||
|
'class' => $class,
|
||||||
|
'relation' => $owned
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find objects in the given relationships, merging them into the given list
|
* Find objects in the given relationships, merging them into the given list
|
||||||
*
|
*
|
||||||
@ -985,33 +1085,55 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
|
|
||||||
// Inspect value of this relationship
|
// Inspect value of this relationship
|
||||||
$items = $owner->{$relationship}();
|
$items = $owner->{$relationship}();
|
||||||
if(!$items) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if($items instanceof DataObject) {
|
|
||||||
$items = array($items);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Versioned|DataObject $item */
|
// Merge any new item
|
||||||
foreach($items as $item) {
|
$newItems = $this->mergeRelatedObjects($list, $items);
|
||||||
// Identify item
|
|
||||||
$itemKey = $item->class . '/' . $item->ID;
|
|
||||||
|
|
||||||
// Skip unsaved, unversioned, or already checked objects
|
// Recurse if necessary
|
||||||
if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
|
if($recursive) {
|
||||||
continue;
|
foreach($newItems as $item) {
|
||||||
}
|
/** @var Versioned|DataObject $item */
|
||||||
|
|
||||||
// Save record
|
|
||||||
$list[$itemKey] = $item;
|
|
||||||
if($recursive) {
|
|
||||||
$item->findRelatedObjects($source, true, $list);
|
$item->findRelatedObjects($source, true, $list);
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to merge owned/owning items into a list.
|
||||||
|
* Items already present in the list will be skipped.
|
||||||
|
*
|
||||||
|
* @param ArrayList $list Items to merge into
|
||||||
|
* @param mixed $items List of new items to merge
|
||||||
|
* @return ArrayList List of all newly added items that did not already exist in $list
|
||||||
|
*/
|
||||||
|
protected function mergeRelatedObjects($list, $items) {
|
||||||
|
$added = new ArrayList();
|
||||||
|
if(!$items) {
|
||||||
|
return $added;
|
||||||
|
}
|
||||||
|
if($items instanceof DataObject) {
|
||||||
|
$items = array($items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Versioned|DataObject $item */
|
||||||
|
foreach($items as $item) {
|
||||||
|
// Identify item
|
||||||
|
$itemKey = $item->class . '/' . $item->ID;
|
||||||
|
|
||||||
|
// Skip unsaved, unversioned, or already checked objects
|
||||||
|
if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save record
|
||||||
|
$list[$itemKey] = $item;
|
||||||
|
$added[$itemKey] = $item;
|
||||||
|
}
|
||||||
|
return $added;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function should return true if the current user can publish this record.
|
* This function should return true if the current user can publish this record.
|
||||||
* It can be overloaded to customise the security model for an application.
|
* It can be overloaded to customise the security model for an application.
|
||||||
@ -2070,6 +2192,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
|
|||||||
public function onAfterRollback($version) {
|
public function onAfterRollback($version) {
|
||||||
// Find record at this version
|
// Find record at this version
|
||||||
$baseClass = ClassInfo::baseDataClass($this->owner);
|
$baseClass = ClassInfo::baseDataClass($this->owner);
|
||||||
|
/** @var Versioned|DataObject $recordVersion */
|
||||||
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
|
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
|
||||||
|
|
||||||
// Note that unlike other publishing actions, rollback is NOT recursive;
|
// Note that unlike other publishing actions, rollback is NOT recursive;
|
||||||
|
@ -220,6 +220,39 @@ class DataExtensionTest extends SapphireTest {
|
|||||||
$this->assertNotEmpty($fields->dataFieldByName('ChildField'));
|
$this->assertNotEmpty($fields->dataFieldByName('ChildField'));
|
||||||
$this->assertNotEmpty($fields->dataFieldByName('GrandchildField'));
|
$this->assertNotEmpty($fields->dataFieldByName('GrandchildField'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test setOwner behaviour
|
||||||
|
*/
|
||||||
|
public function testSetOwner() {
|
||||||
|
$extension = new DataExtensionTest_Ext1();
|
||||||
|
$obj1 = $this->objFromFixture('DataExtensionTest_RelatedObject', 'obj1');
|
||||||
|
$obj2 = $this->objFromFixture('DataExtensionTest_RelatedObject', 'obj1');
|
||||||
|
|
||||||
|
$extension->setOwner(null);
|
||||||
|
$this->assertNull($extension->getOwner());
|
||||||
|
|
||||||
|
// Set original owner
|
||||||
|
$extension->setOwner($obj1);
|
||||||
|
$this->assertEquals($obj1, $extension->getOwner());
|
||||||
|
|
||||||
|
// Set nested owner
|
||||||
|
$extension->setOwner($obj2);
|
||||||
|
$this->assertEquals($obj2, $extension->getOwner());
|
||||||
|
|
||||||
|
// Clear nested owner
|
||||||
|
$extension->clearOwner();
|
||||||
|
$this->assertEquals($obj1, $extension->getOwner());
|
||||||
|
|
||||||
|
// Clear original owner
|
||||||
|
$extension->clearOwner();
|
||||||
|
$this->assertNull($extension->getOwner());
|
||||||
|
|
||||||
|
// Another clearOwner should error
|
||||||
|
$this->setExpectedException("BadMethodCallException", "clearOwner() called more than setOwner()");
|
||||||
|
$extension->clearOwner();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataExtensionTest_Member extends DataObject implements TestOnly {
|
class DataExtensionTest_Member extends DataObject implements TestOnly {
|
||||||
|
@ -318,6 +318,12 @@ class DataObjectTest extends SapphireTest {
|
|||||||
// There will be a method called $obj->relname() that returns the object itself
|
// There will be a method called $obj->relname() that returns the object itself
|
||||||
$this->assertEquals($team1ID, $captain1->FavouriteTeam()->ID);
|
$this->assertEquals($team1ID, $captain1->FavouriteTeam()->ID);
|
||||||
|
|
||||||
|
// Test that getNonReciprocalComponent can find has_one from the has_many end
|
||||||
|
$this->assertEquals(
|
||||||
|
$team1ID,
|
||||||
|
$captain1->inferReciprocalComponent('DataObjectTest_Team', 'PlayerFans')->ID
|
||||||
|
);
|
||||||
|
|
||||||
// Check entity with polymorphic has-one
|
// Check entity with polymorphic has-one
|
||||||
$fan1 = $this->objFromFixture("DataObjectTest_Fan", "fan1");
|
$fan1 = $this->objFromFixture("DataObjectTest_Fan", "fan1");
|
||||||
$this->assertTrue((bool)$fan1->hasValue('Favourite'));
|
$this->assertTrue((bool)$fan1->hasValue('Favourite'));
|
||||||
@ -408,10 +414,19 @@ class DataObjectTest extends SapphireTest {
|
|||||||
// Test getComponents() gets the ComponentSet of the other side of the relation
|
// Test getComponents() gets the ComponentSet of the other side of the relation
|
||||||
$this->assertTrue($team1->Comments()->Count() == 2);
|
$this->assertTrue($team1->Comments()->Count() == 2);
|
||||||
|
|
||||||
|
$team1Comments = [
|
||||||
|
['Comment' => 'This is a team comment by Joe'],
|
||||||
|
['Comment' => 'This is a team comment by Bob'],
|
||||||
|
];
|
||||||
|
|
||||||
// Test the IDs on the DataObjects are set correctly
|
// Test the IDs on the DataObjects are set correctly
|
||||||
foreach($team1->Comments() as $comment) {
|
$this->assertDOSEquals($team1Comments, $team1->Comments());
|
||||||
$this->assertEquals($team1->ID, $comment->TeamID);
|
|
||||||
}
|
// Test that has_many can be infered from the has_one via getNonReciprocalComponent
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
$team1Comments,
|
||||||
|
$team1->inferReciprocalComponent('DataObjectTest_TeamComment', 'Team')
|
||||||
|
);
|
||||||
|
|
||||||
// Test that we can add and remove items that already exist in the database
|
// Test that we can add and remove items that already exist in the database
|
||||||
$newComment = new DataObjectTest_TeamComment();
|
$newComment = new DataObjectTest_TeamComment();
|
||||||
@ -1220,6 +1235,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
|
|
||||||
public function testMultipleManyManyWithSameClass() {
|
public function testMultipleManyManyWithSameClass() {
|
||||||
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
$team = $this->objFromFixture('DataObjectTest_Team', 'team1');
|
||||||
|
$company2 = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany2');
|
||||||
$sponsors = $team->Sponsors();
|
$sponsors = $team->Sponsors();
|
||||||
$equipmentSuppliers = $team->EquipmentSuppliers();
|
$equipmentSuppliers = $team->EquipmentSuppliers();
|
||||||
|
|
||||||
@ -1241,6 +1257,25 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$this->assertInstanceOf('ManyManyList', $teamWithoutSponsor->Sponsors());
|
$this->assertInstanceOf('ManyManyList', $teamWithoutSponsor->Sponsors());
|
||||||
$this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
|
$this->assertEquals(0, $teamWithoutSponsor->Sponsors()->count());
|
||||||
|
|
||||||
|
// Test that belongs_many_many can be infered from with getNonReciprocalComponent
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Name' => 'Company corp'],
|
||||||
|
['Name' => 'Team co.'],
|
||||||
|
],
|
||||||
|
$team->inferReciprocalComponent('DataObjectTest_EquipmentCompany', 'SponsoredTeams')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test that many_many can be infered from getNonReciprocalComponent
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Team 1'],
|
||||||
|
['Title' => 'Team 2'],
|
||||||
|
['Title' => 'Subteam 1'],
|
||||||
|
],
|
||||||
|
$company2->inferReciprocalComponent('DataObjectTest_Team', 'Sponsors')
|
||||||
|
);
|
||||||
|
|
||||||
// Check many_many_extraFields still works
|
// Check many_many_extraFields still works
|
||||||
$equipmentCompany = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany1');
|
$equipmentCompany = $this->objFromFixture('DataObjectTest_EquipmentCompany', 'equipmentcompany1');
|
||||||
$equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
|
$equipmentCompany->SponsoredTeams()->add($teamWithoutSponsor, array('SponsorFee' => 1000));
|
||||||
@ -1269,6 +1304,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
|
$subTeam->Sponsors()->add($subEquipmentCompany, array('SponsorFee' => 1200));
|
||||||
$this->assertEquals(1200, $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
|
$this->assertEquals(1200, $subTeam->Sponsors()->byID($subEquipmentCompany->ID)->SponsorFee,
|
||||||
'Data from inherited many_many_extraFields was not stored/extracted correctly');
|
'Data from inherited many_many_extraFields was not stored/extracted correctly');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testManyManyExtraFields() {
|
public function testManyManyExtraFields() {
|
||||||
@ -1537,6 +1573,7 @@ class DataObjectTest extends SapphireTest {
|
|||||||
$company = new DataObjectTest_Company();
|
$company = new DataObjectTest_Company();
|
||||||
$ceo = new DataObjectTest_CEO();
|
$ceo = new DataObjectTest_CEO();
|
||||||
|
|
||||||
|
$company->Name = 'New Company';
|
||||||
$company->write();
|
$company->write();
|
||||||
$ceo->write();
|
$ceo->write();
|
||||||
|
|
||||||
@ -1546,6 +1583,19 @@ class DataObjectTest extends SapphireTest {
|
|||||||
|
|
||||||
$this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
|
$this->assertEquals($company->ID, $ceo->Company()->ID, 'belongs_to returns the right results.');
|
||||||
|
|
||||||
|
// Test belongs_to can be infered via getNonReciprocalComponent
|
||||||
|
// Note: Will be returned as has_many since the belongs_to is ignored.
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[['Name' => 'New Company']],
|
||||||
|
$ceo->inferReciprocalComponent('DataObjectTest_Company', 'CEO')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test has_one to a belongs_to can be infered via getNonReciprocalComponent
|
||||||
|
$this->assertEquals(
|
||||||
|
$ceo->ID,
|
||||||
|
$company->inferReciprocalComponent('DataObjectTest_CEO', 'Company')->ID
|
||||||
|
);
|
||||||
|
|
||||||
// Test automatic creation of class where no assigment exists
|
// Test automatic creation of class where no assigment exists
|
||||||
$ceo = new DataObjectTest_CEO();
|
$ceo = new DataObjectTest_CEO();
|
||||||
$ceo->write();
|
$ceo->write();
|
||||||
@ -1749,7 +1799,8 @@ class DataObjectTest_Team extends DataObject implements TestOnly {
|
|||||||
private static $has_many = array(
|
private static $has_many = array(
|
||||||
'SubTeams' => 'DataObjectTest_SubTeam',
|
'SubTeams' => 'DataObjectTest_SubTeam',
|
||||||
'Comments' => 'DataObjectTest_TeamComment',
|
'Comments' => 'DataObjectTest_TeamComment',
|
||||||
'Fans' => 'DataObjectTest_Fan.Favourite' // Polymorphic - Team fans
|
'Fans' => 'DataObjectTest_Fan.Favourite', // Polymorphic - Team fans
|
||||||
|
'PlayerFans' => 'DataObjectTest_Player.FavouriteTeam'
|
||||||
);
|
);
|
||||||
|
|
||||||
private static $many_many = array(
|
private static $many_many = array(
|
||||||
|
@ -11,12 +11,15 @@ class VersionedOwnershipTest extends SapphireTest {
|
|||||||
'VersionedOwnershipTest_Related',
|
'VersionedOwnershipTest_Related',
|
||||||
'VersionedOwnershipTest_Attachment',
|
'VersionedOwnershipTest_Attachment',
|
||||||
'VersionedOwnershipTest_RelatedMany',
|
'VersionedOwnershipTest_RelatedMany',
|
||||||
|
'VersionedOwnershipTest_Page',
|
||||||
|
'VersionedOwnershipTest_Banner',
|
||||||
|
'VersionedOwnershipTest_Image',
|
||||||
|
'VersionedOwnershipTest_CustomRelation',
|
||||||
);
|
);
|
||||||
|
|
||||||
protected static $fixture_file = 'VersionedOwnershipTest.yml';
|
protected static $fixture_file = 'VersionedOwnershipTest.yml';
|
||||||
|
|
||||||
public function setUp()
|
public function setUp() {
|
||||||
{
|
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
Versioned::set_stage(Versioned::DRAFT);
|
Versioned::set_stage(Versioned::DRAFT);
|
||||||
@ -27,7 +30,7 @@ class VersionedOwnershipTest extends SapphireTest {
|
|||||||
if(stripos($name, '_published') !== false) {
|
if(stripos($name, '_published') !== false) {
|
||||||
/** @var Versioned|DataObject $object */
|
/** @var Versioned|DataObject $object */
|
||||||
$object = DataObject::get($class)->byID($id);
|
$object = DataObject::get($class)->byID($id);
|
||||||
$object->publish('Stage', 'Live');
|
$object->publish(Versioned::DRAFT, Versioned::LIVE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,6 +507,80 @@ class VersionedOwnershipTest extends SapphireTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that you can find owners without owned_by being defined explicitly
|
||||||
|
*/
|
||||||
|
public function testInferedOwners() {
|
||||||
|
// Make sure findOwned() works
|
||||||
|
/** @var VersionedOwnershipTest_Page $page1 */
|
||||||
|
$page1 = $this->objFromFixture('VersionedOwnershipTest_Page', 'page1_published');
|
||||||
|
/** @var VersionedOwnershipTest_Page $page2 */
|
||||||
|
$page2 = $this->objFromFixture('VersionedOwnershipTest_Page', 'page2_published');
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 1'],
|
||||||
|
['Title' => 'Image 1'],
|
||||||
|
['Title' => 'Custom 1'],
|
||||||
|
],
|
||||||
|
$page1->findOwned()
|
||||||
|
);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 2'],
|
||||||
|
['Title' => 'Banner 3'],
|
||||||
|
['Title' => 'Image 1'],
|
||||||
|
['Title' => 'Image 2'],
|
||||||
|
['Title' => 'Custom 2'],
|
||||||
|
],
|
||||||
|
$page2->findOwned()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check that findOwners works
|
||||||
|
/** @var VersionedOwnershipTest_Image $image1 */
|
||||||
|
$image1 = $this->objFromFixture('VersionedOwnershipTest_Image', 'image1_published');
|
||||||
|
/** @var VersionedOwnershipTest_Image $image2 */
|
||||||
|
$image2 = $this->objFromFixture('VersionedOwnershipTest_Image', 'image2_published');
|
||||||
|
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 1'],
|
||||||
|
['Title' => 'Banner 2'],
|
||||||
|
['Title' => 'Page 1'],
|
||||||
|
['Title' => 'Page 2'],
|
||||||
|
],
|
||||||
|
$image1->findOwners()
|
||||||
|
);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 1'],
|
||||||
|
['Title' => 'Banner 2'],
|
||||||
|
],
|
||||||
|
$image1->findOwners(false)
|
||||||
|
);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 3'],
|
||||||
|
['Title' => 'Page 2'],
|
||||||
|
],
|
||||||
|
$image2->findOwners()
|
||||||
|
);
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[
|
||||||
|
['Title' => 'Banner 3'],
|
||||||
|
],
|
||||||
|
$image2->findOwners(false)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test custom relation can findOwners()
|
||||||
|
/** @var VersionedOwnershipTest_CustomRelation $custom1 */
|
||||||
|
$custom1 = $this->objFromFixture('VersionedOwnershipTest_CustomRelation', 'custom1_published');
|
||||||
|
$this->assertDOSEquals(
|
||||||
|
[['Title' => 'Page 1']],
|
||||||
|
$custom1->findOwners()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -622,3 +699,107 @@ class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly {
|
|||||||
'AttachedTo'
|
'AttachedTo'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page which owns a lits of banners
|
||||||
|
*
|
||||||
|
* @mixin Versioned
|
||||||
|
*/
|
||||||
|
class VersionedOwnershipTest_Page extends DataObject implements TestOnly {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar(255)',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $many_many = array(
|
||||||
|
'Banners' => 'VersionedOwnershipTest_Banner',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $owns = array(
|
||||||
|
'Banners',
|
||||||
|
'Custom'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All custom objects with the same number. E.g. 'Page 1' owns 'Custom 1'
|
||||||
|
*
|
||||||
|
* @return DataList
|
||||||
|
*/
|
||||||
|
public function Custom() {
|
||||||
|
$title = str_replace('Page', 'Custom', $this->Title);
|
||||||
|
return VersionedOwnershipTest_CustomRelation::get()
|
||||||
|
->filter('Title', $title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Banner which doesn't declare its belongs_many_many, but owns an Image
|
||||||
|
*
|
||||||
|
* @mixin Versioned
|
||||||
|
*/
|
||||||
|
class VersionedOwnershipTest_Banner extends DataObject implements TestOnly {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar(255)',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $has_one = array(
|
||||||
|
'Image' => 'VersionedOwnershipTest_Image',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $owns = array(
|
||||||
|
'Image',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object which is owned via a custom PHP method rather than DB relation
|
||||||
|
*
|
||||||
|
* @mixin Versioned
|
||||||
|
*/
|
||||||
|
class VersionedOwnershipTest_CustomRelation extends DataObject implements TestOnly {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar(255)',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $owned_by = array(
|
||||||
|
'Pages'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All pages with the same number. E.g. 'Page 1' owns 'Custom 1'
|
||||||
|
*
|
||||||
|
* @return DataList
|
||||||
|
*/
|
||||||
|
public function Pages() {
|
||||||
|
$title = str_replace('Custom', 'Page', $this->Title);
|
||||||
|
return VersionedOwnershipTest_Page::get()->filter('Title', $title);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple versioned dataobject
|
||||||
|
*
|
||||||
|
* @mixin Versioned
|
||||||
|
*/
|
||||||
|
class VersionedOwnershipTest_Image extends DataObject implements TestOnly {
|
||||||
|
private static $extensions = array(
|
||||||
|
'Versioned',
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar(255)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -43,3 +43,34 @@ VersionedOwnershipTest_RelatedMany:
|
|||||||
VersionedOwnershipTest_Object:
|
VersionedOwnershipTest_Object:
|
||||||
object1:
|
object1:
|
||||||
Title: 'Object 1'
|
Title: 'Object 1'
|
||||||
|
|
||||||
|
VersionedOwnershipTest_Image:
|
||||||
|
image1_published:
|
||||||
|
Title: 'Image 1'
|
||||||
|
image2_published:
|
||||||
|
Title: 'Image 2'
|
||||||
|
|
||||||
|
VersionedOwnershipTest_Banner:
|
||||||
|
banner1_published:
|
||||||
|
Title: 'Banner 1'
|
||||||
|
Image: =>VersionedOwnershipTest_Image.image1_published
|
||||||
|
banner2_published:
|
||||||
|
Title: 'Banner 2'
|
||||||
|
Image: =>VersionedOwnershipTest_Image.image1_published
|
||||||
|
banner3_published:
|
||||||
|
Title: 'Banner 3'
|
||||||
|
Image: =>VersionedOwnershipTest_Image.image2_published
|
||||||
|
|
||||||
|
VersionedOwnershipTest_Page:
|
||||||
|
page1_published:
|
||||||
|
Title: 'Page 1'
|
||||||
|
Banners: =>VersionedOwnershipTest_Banner.banner1_published
|
||||||
|
page2_published:
|
||||||
|
Title: 'Page 2'
|
||||||
|
Banners: =>VersionedOwnershipTest_Banner.banner2_published,=>VersionedOwnershipTest_Banner.banner3_published
|
||||||
|
|
||||||
|
VersionedOwnershipTest_CustomRelation:
|
||||||
|
custom1_published:
|
||||||
|
Title: 'Custom 1'
|
||||||
|
custom2_published:
|
||||||
|
Title: 'Custom 2'
|
||||||
|
Loading…
Reference in New Issue
Block a user