diff --git a/docs/en/02_Developer_Guides/00_Model/10_Versioning.md b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md index 587d820fd..a8e32e2c8 100644 --- a/docs/en/02_Developer_Guides/00_Model/10_Versioning.md +++ b/docs/en/02_Developer_Guides/00_Model/10_Versioning.md @@ -129,6 +129,68 @@ is initialized. But it can also be set and reset temporarily to force a specific $obj = MyRecord::getComplexObjectRetrieval(); // returns 'Stage' records Versioned::set_reading_mode($origMode); // reset current mode +### File ownership + +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. + +For instance, a page which has a list of rotating banners will require that those banners are published +whenever that page is. + +The solution to this problem is the ownership API, which declares a two-way relationship between +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. + +For instance, in order to specify this dependency, you must apply `owns` and `owned_by` config +on a relationship. + + + :::php + class MyPage extends Page { + private static $has_many = array( + 'Banners' => 'Banner' + ); + private static $owns = array( + 'Banners' + ); + } + + class Banner extends Page { + private static $extensions = array( + 'Versioned' + ); + private static $has_one = array( + 'Parent' => 'MyPage', + 'Image' => 'Image', + ); + private static $owned_by = array( + 'Parent' + ); + private static $owns = array( + 'Image' + ); + } + + class BannerImageExtension extends DataExtension { + private static $has_many = array( + 'Banners' => 'Banner' + ); + private static $owned_by = array( + 'Banners' + ); + } + +With the config: + + :::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`. + ### Custom SQL We generally discourage writing `Versioned` queries from scratch, due to the complexities involved through joining diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index f633d06bb..bc6c0c9fd 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -646,3 +646,9 @@ has been removed from core. You can configure the built-in `charmap` plugin inst For more information on available options and plugins please refer to the [tinymce documentation](https://www.tinymce.com/docs/configure/) + +### Implementation of ownership API + +In order to support the recursive publishing of dataobjects, a new API has been developed to allow +developers to declare dependencies between objects. See the +[versioned documentation](/developer_guides/model/versioning) for more information. diff --git a/model/DataList.php b/model/DataList.php index 55765ecba..db7652234 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -158,7 +158,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * * @param string|array $keyOrArray Either the single key to set, or an array of key value pairs to set * @param mixed $val If $keyOrArray is not an array, this is the value to set - * @return DataList + * @return static */ public function setDataQueryParam($keyOrArray, $val = null) { $clone = clone $this; @@ -179,7 +179,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-) * * @param array $parameters Out variable for parameters required for this query - * @param string The resulting SQL query (may be paramaterised) + * @return string The resulting SQL query (may be paramaterised) */ public function sql(&$parameters = array()) { return $this->dataQuery->query()->sql($parameters); @@ -737,7 +737,7 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab * @return DataObject */ protected function createDataObject($row) { - $defaultClass = $this->dataClass; + $class = $this->dataClass; // Failover from RecordClassName to ClassName if(empty($row['RecordClassName'])) { @@ -746,17 +746,26 @@ class DataList extends ViewableData implements SS_List, SS_Filterable, SS_Sortab // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass if(class_exists($row['RecordClassName'])) { - $item = Injector::inst()->create($row['RecordClassName'], $row, false, $this->model); - } else { - $item = Injector::inst()->create($defaultClass, $row, false, $this->model); + $class = $row['RecordClassName']; } + $item = Injector::inst()->create($class, $row, false, $this->model); //set query params on the DataObject to tell the lazy loading mechanism the context the object creation context - $item->setSourceQueryParams($this->dataQuery()->getQueryParams()); + $item->setSourceQueryParams($this->getQueryParams()); return $item; } + /** + * Get query parameters for this list. + * These values will be assigned as query parameters to newly created objects from this list. + * + * @return array + */ + public function getQueryParams() { + return $this->dataQuery()->getQueryParams(); + } + /** * Returns an Iterator for this DataList. * This function allows you to use DataLists in foreach loops diff --git a/model/DataObject.php b/model/DataObject.php index 53c3acf14..88cf67574 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -224,42 +224,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ protected $unsavedRelations; - /** - * Returns when validation on DataObjects is enabled. - * - * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead - * @return bool - */ - public static function get_validation_enabled() { - Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead'); - return Config::inst()->get('DataObject', 'validation_enabled'); - } - - /** - * Set whether DataObjects should be validated before they are written. - * - * Caution: Validation can contain safeguards against invalid/malicious data, - * and check permission levels (e.g. on {@link Group}). Therefore it is recommended - * to only disable validation for very specific use cases. - * - * @param $enable bool - * @see DataObject::validate() - * @deprecated 3.2 Use the "DataObject.validation_enabled" config setting instead - */ - public static function set_validation_enabled($enable) { - Deprecation::notice('3.2', 'Use the "DataObject.validation_enabled" config setting instead'); - Config::inst()->update('DataObject', 'validation_enabled', (bool)$enable); - } - - /** - * Clear all cached classname specs. It's necessary to clear all cached subclassed names - * for any classes if a new class manifest is generated. - */ - public static function clear_classname_spec_cache() { - Deprecation::notice('4.0', 'Call DBClassName::clear_classname_cache() instead'); - DBClassName::clear_classname_cache(); - } - /** * Return the complete map of fields to specification on this object, including fixed_fields. * "ID" will be included on every table. @@ -279,11 +243,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Refresh cache self::cache_database_fields($class); - + // Return cached values return self::$_cache_database_fields[$class]; } - + /** * Cache all database and composite fields for the given class. * Will do nothing if already cached @@ -309,7 +273,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } else { $dbFields['ID'] = $fixedFields['ID']; } - + // Check each DB value as either a field or composite field $db = Config::inst()->get($class, 'db', Config::UNINHERITED) ?: array(); foreach($db as $fieldName => $fieldSpec) { @@ -366,7 +330,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(empty($class)) { $class = get_called_class(); } - + // Get all fields $fields = self::database_fields($class); @@ -530,8 +494,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Create a duplicate of this node. * Note: now also duplicates relations. * - * @param $doWrite Perform a write() operation before returning the object. If this is true, it will create the - * duplicate in the database. + * @param bool $doWrite Perform a write() operation before returning the object. + * If this is true, it will create the duplicate in the database. * @return DataObject A duplicate of this node. The exact type will be the type of this node. */ public function duplicate($doWrite = true) { @@ -554,8 +518,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * The destinationObject must be written to the database already and have an ID. Writing is performed * automatically when adding the new relations. * - * @param $sourceObject the source object to duplicate from - * @param $destinationObject the destination object to populate with the duplicated relations + * @param DataObject $sourceObject the source object to duplicate from + * @param DataObject $destinationObject the destination object to populate with the duplicated relations * @return DataObject with the new many_many relations copied in */ protected function duplicateManyManyRelations($sourceObject, $destinationObject) { @@ -1529,8 +1493,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * non-polymorphic relations, or for polymorphic relations with a class set. * * @param string $componentName Name of the component - * * @return DataObject The component object. It's exact type will be that of the component. + * @throws Exception */ public function getComponent($componentName) { if(isset($this->components[$componentName])) { @@ -1548,28 +1512,38 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } if($joinID) { - $component = DataObject::get_by_id($class, $joinID); + // Ensure that the selected object originates from the same stage, subsite, etc + $component = DataObject::get($class) + ->filter('ID', $joinID) + ->setDataQueryParam($this->getInheritableQueryParams()) + ->first(); } if(empty($component)) { $component = $this->model->$class->newObject(); } } elseif($class = $this->belongsToComponent($componentName)) { - $joinField = $this->getRemoteJoinField($componentName, 'belongs_to', $polymorphic); - $joinID = $this->ID; + $joinID = $this->ID; if($joinID) { - - $filter = $polymorphic - ? array( + // Prepare filter for appropriate join type + if($polymorphic) { + $filter = array( "{$joinField}ID" => $joinID, "{$joinField}Class" => $this->class - ) - : array( + ); + } else { + $filter = array( $joinField => $joinID ); - $component = DataObject::get($class)->filter($filter)->first(); + } + + // Ensure that the selected object originates from the same stage, subsite, etc + $component = DataObject::get($class) + ->filter($filter) + ->setDataQueryParam($this->getInheritableQueryParams()) + ->first(); } if(empty($component)) { @@ -1582,7 +1556,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } } else { - throw new Exception("DataObject->getComponent(): Could not find component '$componentName'."); + throw new InvalidArgumentException( + "DataObject->getComponent(): Could not find component '$componentName'." + ); } $this->components[$componentName] = $component; @@ -1593,31 +1569,18 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Returns a one-to-many relation as a HasManyList * * @param string $componentName Name of the component - * @param string|null $filter Deprecated. A filter to be inserted into the WHERE clause - * @param string|null|array $sort Deprecated. A sort expression to be inserted into the ORDER BY clause. If omitted, - * the static field $default_sort on the component class will be used. - * @param string $join Deprecated, use leftJoin($table, $joinClause) instead - * @param string|null|array $limit Deprecated. A limit expression to be inserted into the LIMIT clause - * * @return HasManyList The components of the one-to-many relationship. */ - public function getComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) { + public function getComponents($componentName) { $result = null; - if(!$componentClass = $this->hasManyComponent($componentName)) { - user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName'" - . " on class '$this->class'", E_USER_ERROR); - } - - if($join) { - throw new \InvalidArgumentException( - 'The $join argument has been removed. Use leftJoin($table, $joinClause) instead.' - ); - } - - if($filter !== null || $sort !== null || $limit !== null) { - Deprecation::notice('4.0', 'The $filter, $sort and $limit parameters for DataObject::getComponents() - have been deprecated. Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL); + $componentClass = $this->hasManyComponent($componentName); + if(!$componentClass) { + throw new InvalidArgumentException(sprintf( + "DataObject::getComponents(): Unknown 1-to-many component '%s' on class '%s'", + $componentName, + $this->class + )); } // If we haven't been written yet, we can't save these relations, so use a list that handles this case @@ -1631,19 +1594,20 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Determine type and nature of foreign relation $joinField = $this->getRemoteJoinField($componentName, 'has_many', $polymorphic); + /** @var HasManyList $result */ if($polymorphic) { $result = PolymorphicHasManyList::create($componentClass, $joinField, $this->class); } else { $result = HasManyList::create($componentClass, $joinField); } - if($this->model) $result->setDataModel($this->model); + if($this->model) { + $result->setDataModel($this->model); + } return $result - ->forForeignID($this->ID) - ->where($filter) - ->limit($limit) - ->sort($sort); + ->setDataQueryParam($this->getInheritableQueryParams()) + ->forForeignID($this->ID); } /** @@ -1689,6 +1653,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @param string $type the join type - either 'has_many' or 'belongs_to' * @param boolean $polymorphic Flag set to true if the remote join field is polymorphic. * @return string + * @throws Exception */ public function getRemoteJoinField($component, $type = 'has_many', &$polymorphic = false) { // Extract relation from current object @@ -1763,19 +1728,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * Returns a many-to-many component, as a ManyManyList. * @param string $componentName Name of the many-many component * @return ManyManyList The set of components - * - * @todo Implement query-params */ - public function getManyManyComponents($componentName, $filter = null, $sort = null, $join = null, $limit = null) { - list($parentClass, $componentClass, $parentField, $componentField, $table) - = $this->manyManyComponent($componentName); - - if($filter !== null || $sort !== null || $join !== null || $limit !== null) { - Deprecation::notice('4.0', 'The $filter, $sort, $join and $limit parameters for - DataObject::getManyManyComponents() have been deprecated. - Please manipluate the returned list directly.', Deprecation::SCOPE_GLOBAL); + public function getManyManyComponents($componentName) { + $manyManyComponent = $this->manyManyComponent($componentName); + if(!$manyManyComponent) { + throw new InvalidArgumentException(sprintf( + "DataObject::getComponents(): Unknown many-to-many component '%s' on class '%s'", + $componentName, + $this->class + )); } + list($parentClass, $componentClass, $parentField, $componentField, $table) = $manyManyComponent; + // If we haven't been written yet, we can't save these relations, so use a list that handles this case if(!$this->ID) { if(!isset($this->unsavedRelations[$componentName])) { @@ -1786,54 +1751,29 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } $extraFields = $this->manyManyExtraFieldsForComponent($componentName) ?: array(); + /** @var ManyManyList $result */ $result = ManyManyList::create($componentClass, $table, $componentField, $parentField, $extraFields); - - if($this->model) $result->setDataModel($this->model); + 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 - ->forForeignID($this->ID) - ->where($filter) - ->sort($sort) - ->limit($limit); - } - - /** - * @deprecated 4.0 Method has been replaced by hasOne() and hasOneComponent() - * @param string $component - * @return array|null - */ - public function has_one($component = null) { - if($component) { - Deprecation::notice('4.0', 'Please use hasOneComponent() instead'); - return $this->hasOneComponent($component); - } - - Deprecation::notice('4.0', 'Please use hasOne() instead'); - return $this->hasOne(); + ->setDataQueryParam($this->getInheritableQueryParams()) + ->forForeignID($this->ID); } /** * Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and * their classes. If the selected has_one is a polymorphic field then 'DataObject' will be returned for the type. * - * @param string $component Deprecated - Name of component * @return string|array The class of the one-to-one component, or an array of all one-to-one components and * their classes. */ - public function hasOne($component = null) { - if($component) { - Deprecation::notice( - '4.0', - 'Please use DataObject::hasOneComponent() instead of passing a component name to hasOne()', - Deprecation::SCOPE_GLOBAL - ); - return $this->hasOneComponent($component); - } - + public function hasOne() { return (array)Config::inst()->get($this->class, 'has_one', Config::INHERITED); } @@ -1853,22 +1793,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } - /** - * @deprecated 4.0 Method has been replaced by belongsTo() and belongsToComponent() - * @param string $component - * @param bool $classOnly - * @return array|null - */ - public function belongs_to($component = null, $classOnly = true) { - if($component) { - Deprecation::notice('4.0', 'Please use belongsToComponent() instead'); - return $this->belongsToComponent($component, $classOnly); - } - - Deprecation::notice('4.0', 'Please use belongsTo() instead'); - return $this->belongsTo(null, $classOnly); - } - /** * Returns the class of a remote belongs_to relationship. If no component is specified a map of all components and * their class name will be returned. @@ -1964,22 +1888,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } } - /** - * @deprecated 4.0 Method has been replaced by hasMany() and hasManyComponent() - * @param string $component - * @param bool $classOnly - * @return array|null - */ - public function has_many($component = null, $classOnly = true) { - if($component) { - Deprecation::notice('4.0', 'Please use hasManyComponent() instead'); - return $this->hasManyComponent($component, $classOnly); - } - - Deprecation::notice('4.0', 'Please use hasMany() instead'); - return $this->hasMany(null, $classOnly); - } - /** * Gets the class of a one-to-many relationship. If no $component is specified then an array of all the one-to-many * relationships and their classes will be returned. @@ -2026,42 +1934,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return ($classOnly) ? preg_replace('/(.+)?\..+/', '$1', $hasMany) : $hasMany; } - /** - * @deprecated 4.0 Method has been replaced by manyManyExtraFields() and - * manyManyExtraFieldsForComponent() - * @param string $component - * @return array - */ - public function many_many_extraFields($component = null) { - if($component) { - Deprecation::notice('4.0', 'Please use manyManyExtraFieldsForComponent() instead'); - return $this->manyManyExtraFieldsForComponent($component); - } - - Deprecation::notice('4.0', 'Please use manyManyExtraFields() instead'); - return $this->manyManyExtraFields(); - } - /** * Return the many-to-many extra fields specification. * * If you don't specify a component name, it returns all * extra fields for all components available. * - * @param string $component Deprecated - Name of component * @return array|null */ - public function manyManyExtraFields($component = null) { - if($component) { - Deprecation::notice( - '4.0', - 'Please use DataObject::manyManyExtraFieldsForComponent() instead of passing a component name - to manyManyExtraFields()', - Deprecation::SCOPE_GLOBAL - ); - return $this->manyManyExtraFieldsForComponent($component); - } - + public function manyManyExtraFields() { return Config::inst()->get($this->class, 'many_many_extraFields', Config::INHERITED); } @@ -2111,43 +1992,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return isset($items) ? $items : null; } - /** - * @deprecated 4.0 Method has been renamed to manyMany() - * @param string $component - * @return array|null - */ - public function many_many($component = null) { - if($component) { - Deprecation::notice('4.0', 'Please use manyManyComponent() instead'); - return $this->manyManyComponent($component); - } - - Deprecation::notice('4.0', 'Please use manyMany() instead'); - return $this->manyMany(); - } - /** * Return information about a many-to-many component. * The return value is an array of (parentclass, childclass). If $component is null, then all many-many * components are returned. * * @see DataObject::manyManyComponent() - * @param string $component Deprecated - Name of component * @return array|null An array of (parentclass, childclass), or an array of all many-many components */ - public function manyMany($component = null) { - if($component) { - Deprecation::notice( - '4.0', - 'Please use DataObject::manyManyComponent() instead of passing a component name to manyMany()', - Deprecation::SCOPE_GLOBAL - ); - return $this->manyManyComponent($component); - } - + public function manyMany() { $manyManys = (array)Config::inst()->get($this->class, 'many_many', Config::INHERITED); $belongsManyManys = (array)Config::inst()->get($this->class, 'belongs_many_many', Config::INHERITED); - $items = array_merge($manyManys, $belongsManyManys); return $items; } @@ -3285,6 +3140,17 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return $this->sourceQueryParams; } + /** + * Get list of parameters that should be inherited to relations on this object + * + * @return array + */ + public function getInheritableQueryParams() { + $params = $this->getSourceQueryParams(); + $this->extend('updateInheritableQueryParams', $params); + return $params; + } + /** * @see $sourceQueryParams * @param array @@ -3457,14 +3323,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $this->extend('requireDefaultRecords', $dummy); } - /** - * @deprecated since version 4.0 - */ - public function inheritedDatabaseFields() { - Deprecation::notice('4.0', 'Use db() instead'); - return $this->db(); - } - /** * Get the default searchable fields for this object, as defined in the * $searchable_fields list. If searchable fields are not defined on the @@ -3730,7 +3588,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Use a casting object for a field. This is a map from * field name to class name of the casting object. - * + * * @var array */ private static $casting = array( diff --git a/model/ManyManyList.php b/model/ManyManyList.php index 6f9675e7a..cc3e76e9d 100644 --- a/model/ManyManyList.php +++ b/model/ManyManyList.php @@ -146,14 +146,6 @@ class ManyManyList extends RelationList { return $dataObject; } - /** - * Return a filter expression for when getting the contents of the - * relationship for some foreign ID - * - * @param int $id - * - * @return string - */ protected function foreignIDFilter($id = null) { if ($id === null) { $id = $this->getForeignID(); diff --git a/model/RelationList.php b/model/RelationList.php index 86ed5709c..f4d182c4f 100644 --- a/model/RelationList.php +++ b/model/RelationList.php @@ -16,11 +16,26 @@ abstract class RelationList extends DataList implements Relation { return $this->dataQuery->getQueryParam('Foreign.ID'); } + public function getQueryParams() { + $params = parent::getQueryParams(); + + // Remove `Foreign.` query parameters for created objects, + // as this would interfere with relations on those objects. + foreach(array_keys($params) as $key) { + if(stripos($key, 'Foreign.') !== 0) { + unset($params[$key]); + } + } + + return $params; + } + /** * Returns a copy of this list with the ManyMany relationship linked to * the given foreign ID. * * @param int|array $id An ID or an array of IDs. + * @return static */ public function forForeignID($id) { // Turn a 1-element array into a simple value diff --git a/model/versioning/Versioned.php b/model/versioning/Versioned.php index 76e6a5fee..18adbbd1f 100644 --- a/model/versioning/Versioned.php +++ b/model/versioning/Versioned.php @@ -8,6 +8,7 @@ * the pages used in the CMS. * * @property int $Version + * @property DataObject|Versioned $owner * * @package framework * @subpackage model @@ -160,6 +161,34 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { */ private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT'); + /** + * List of relationships on this object that are "owned" by this object. + * Owership in the context of versioned objects is a relationship where + * the publishing of owning objects requires the publishing of owned objects. + * + * E.g. A page owns a set of banners, as in order for the page to be published, all + * banners on this page must also be published for it to be visible. + * + * Typically any object and its owned objects should be visible in the same edit view. + * E.g. a page and {@see GridField} of banners. + * + * Page hierarchy is typically not considered an ownership relationship. + * + * Ownership is recursive; If A owns B and B owns C then A owns C. + * + * @config + * @var array List of has_many or many_many relationships owned by this object. + */ + private static $owns = array(); + + /** + * Opposing relationship to owns config; Represents the objects which + * own the current object. + * + * @var array + */ + private static $owned_by = array(); + /** * Reset static configuration variables to their default values. */ @@ -201,14 +230,19 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { if($parts[0] == 'Archive') { $dataQuery->setQueryParam('Versioned.mode', 'archive'); $dataQuery->setQueryParam('Versioned.date', $parts[1]); - - } else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage - && array_search($parts[1],$this->stages) !== false) { - + } else if($parts[0] == 'Stage' && in_array($parts[1], $this->stages)) { $dataQuery->setQueryParam('Versioned.mode', 'stage'); $dataQuery->setQueryParam('Versioned.stage', $parts[1]); } + } + + public function updateInheritableQueryParams(&$params) { + // Versioned.mode === all_versions doesn't inherit very well, so default to stage + if(isset($params['Versioned.mode']) && $params['Versioned.mode'] === 'all_versions') { + $params['Versioned.mode'] = 'stage'; + $params['Versioned.stage'] = $this->defaultStage; + } } /** @@ -309,8 +343,12 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { case 'latest_versions': foreach($query->getFrom() as $alias => $join) { if($alias != $baseTable) { - $query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"" - . " AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); + // Make sure join includes version as well + $query->setJoinFilter( + $alias, + "\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"" + . " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"" + ); } $query->renameTable($alias, $alias . '_versions'); } @@ -320,33 +358,30 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name); } - // Alias the record ID as the row ID - $query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID"); + // Alias the record ID as the row ID, and ensure ID filters are aliased correctly + $query->selectField("\"{$baseTable}_versions\".\"RecordID\"", "ID"); + $query->replaceText("\"{$baseTable}_versions\".\"ID\"", "\"{$baseTable}_versions\".\"RecordID\""); - // Ensure that any sort order referring to this ID is correctly aliased - $orders = $query->getOrderBy(); - foreach($orders as $order => $dir) { - if($order === "\"$baseTable\".\"ID\"") { - unset($orders[$order]); - $orders["\"{$baseTable}_versions\".\"RecordID\""] = $dir; - } - } - $query->setOrderBy($orders); + // However, if doing count, undo rewrite of "ID" column + $query->replaceText( + "count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")", + "count(DISTINCT \"{$baseTable}_versions\".\"ID\")" + ); // latest_version has one more step // Return latest version instances, regardless of whether they are on a particular stage // This provides "show all, including deleted" functonality if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') { $query->addWhere( - "\"{$alias}_versions\".\"Version\" IN + "\"{$baseTable}_versions\".\"Version\" IN (SELECT LatestVersion FROM (SELECT - \"{$alias}_versions\".\"RecordID\", - MAX(\"{$alias}_versions\".\"Version\") AS LatestVersion - FROM \"{$alias}_versions\" - GROUP BY \"{$alias}_versions\".\"RecordID\" - ) AS \"{$alias}_versions_latest\" - WHERE \"{$alias}_versions_latest\".\"RecordID\" = \"{$alias}_versions\".\"RecordID\" + \"{$baseTable}_versions\".\"RecordID\", + MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion + FROM \"{$baseTable}_versions\" + GROUP BY \"{$baseTable}_versions\".\"RecordID\" + ) AS \"{$baseTable}_versions_latest\" + WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" )"); } else { // If all versions are requested, ensure that records are sorted by this field @@ -415,14 +450,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { $versionableExtensions = $this->owner->config()->versionableExtensions; if(count($versionableExtensions)){ foreach ($versionableExtensions as $versionableExtension => $suffixes) { - if ($this->owner->hasExtension($versionableExtension)) { - $allSuffixes = array_merge($allSuffixes, (array)$suffixes); - foreach ((array)$suffixes as $suffix) { - $allSuffixes[$suffix] = $versionableExtension; - } + if ($this->owner->hasExtension($versionableExtension)) { + $allSuffixes = array_merge($allSuffixes, (array)$suffixes); + foreach ((array)$suffixes as $suffix) { + $allSuffixes[$suffix] = $versionableExtension; } } } + } // Add the default table with an empty suffix to the list (table name = class name) array_push($allSuffixes,''); @@ -779,6 +814,95 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { $this->migrateVersion(null); } + /** + * Find all objects owned by the current 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 Optional list to add items to + * @return ArrayList list of objects + */ + public function findOwned($recursive = true, $list = null) + { + // Find objects in these relationships + return $this->findRelatedObjects('owns', $recursive, $list); + } + + /** + * 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 Optional list to add items to + * @return ArrayList list of objects + */ + public function findOwners($recursive = true, $list = null) + { + // Find objects in these relationships + return $this->findRelatedObjects('owned_by', $recursive, $list); + } + + /** + * Find objects in the given relationships, merging them into the given list + * + * @param array $source Config property to extract relationships from + * @param bool $recursive True if recursive + * @param ArrayList $list Optional list to add items to + * @return ArrayList The list + */ + public function findRelatedObjects($source, $recursive = true, $list = null) + { + if (!$list) { + $list = new ArrayList(); + } + + // Skip search for unsaved records + if(!$this->owner->isInDB()) { + return $list; + } + + $relationships = $this->owner->config()->{$source}; + foreach($relationships as $relationship) { + // Warn if invalid config + if(!$this->owner->hasMethod($relationship)) { + trigger_error(sprintf( + "Invalid %s config value \"%s\" on object on class \"%s\"", + $source, + $relationship, + $this->owner->class + ), E_USER_WARNING); + continue; + } + + // Inspect value of this relationship + $items = $this->owner->{$relationship}(); + if(!$items) { + continue; + } + 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; + if($recursive) { + $item->findRelatedObjects($source, true, $list); + }; + } + } + return $list; + } + /** * 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. @@ -1020,11 +1144,11 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { if ($owner->hasExtension($versionableExtension)) { $ext = $owner->getExtensionInstance($versionableExtension); $ext->setOwner($owner); - $table = $ext->extendWithSuffix($table); - $ext->clearOwner(); - } + $table = $ext->extendWithSuffix($table); + $ext->clearOwner(); } } + } return $table; } @@ -1129,7 +1253,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { */ public function publish($fromStage, $toStage, $createNewVersion = false) { $owner = $this->owner; - $owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion); + $owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion); $baseClass = ClassInfo::baseDataClass($owner->class); @@ -1153,14 +1277,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { } else { $from->migrateVersion($from->Version); - // Mark this version as having been published at some stage + // Mark this version as having been published at some stage $publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0; $extTable = $this->extendWithSuffix($baseClass); - DB::prepared_query("UPDATE \"{$extTable}_versions\" - SET \"WasPublished\" = ?, \"PublisherID\" = ? - WHERE \"RecordID\" = ? AND \"Version\" = ?", - array(1, $publisherID, $from->ID, $from->Version) - ); + DB::prepared_query("UPDATE \"{$extTable}_versions\" + SET \"WasPublished\" = ?, \"PublisherID\" = ? + WHERE \"RecordID\" = ? AND \"Version\" = ?", + array(1, $publisherID, $from->ID, $from->Version) + ); } // Change to new stage, write, and revert state @@ -1184,7 +1308,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { Versioned::set_reading_mode($oldMode); - $owner->extend('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion); + $owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion); } /** @@ -1245,6 +1369,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * @param string $limit * @param string $join Deprecated, use leftJoin($table, $joinClause) instead * @param string $having + * @return ArrayList */ public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") { // Make sure the table names are not postfixed (e.g. _Live) @@ -1310,6 +1435,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { /** * Return the base table - the class that directly extends DataObject. * + * @param string $stage * @return string */ public function baseTable($stage = null) { @@ -1476,7 +1602,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * @param string $stage The name of the stage. * @param string $filter A filter to be inserted into the WHERE clause. * @param boolean $cache Use caching. - * @param string $orderby A sort expression to be inserted into the ORDER BY clause. + * @param string $sort A sort expression to be inserted into the ORDER BY clause. * * @return DataObject */ @@ -1578,9 +1704,9 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * * @return DataList A modified DataList designated to the specified stage */ - public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', - $containerClass = 'DataList') { - + public static function get_by_stage( + $class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList' + ) { $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass); return $result->setDataQueryParam(array( 'Versioned.mode' => 'stage', @@ -1589,27 +1715,28 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { } /** - * @param string $stage + * Delete this record from the given stage * - * @return int + * @param string $stage */ public function deleteFromStage($stage) { $oldMode = Versioned::get_reading_mode(); Versioned::reading_stage($stage); $clone = clone $this->owner; - $result = $clone->delete(); + $clone->delete(); Versioned::set_reading_mode($oldMode); // Fix the version number cache (in case you go delete from stage and then check ExistsOnLive) $baseClass = ClassInfo::baseDataClass($this->owner->class); self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null; - - return $result; } /** + * Write the given record to the draft stage + * * @param string $stage * @param boolean $forceInsert + * @return int The ID of the record */ public function writeToStage($stage, $forceInsert = false) { $oldMode = Versioned::get_reading_mode(); @@ -1639,12 +1766,14 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { /** * Return the latest version of the given record. * + * @param string $class + * @param int $id * @return DataObject */ public static function get_latest_version($class, $id) { $baseClass = ClassInfo::baseDataClass($class); $list = DataList::create($baseClass) - ->where("\"$baseClass\".\"RecordID\" = $id") + ->where(array("\"$baseClass\".\"RecordID\"" => $id)) ->setDataQueryParam("Versioned.mode", "latest_versions"); return $list->First(); @@ -1716,6 +1845,7 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * @param string $class * @param string $filter * @param string $sort + * @return DataList */ public static function get_including_deleted($class, $filter = "", $sort = "") { $list = DataList::create($class) @@ -1742,8 +1872,10 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { public static function get_version($class, $id, $version) { $baseClass = ClassInfo::baseDataClass($class); $list = DataList::create($baseClass) - ->where("\"$baseClass\".\"RecordID\" = $id") - ->where("\"$baseClass\".\"Version\" = " . (int)$version) + ->where(array( + "\"{$baseClass}\".\"RecordID\"" => $id, + "\"{$baseClass}\".\"Version\"" => $version + )) ->setDataQueryParam("Versioned.mode", 'all_versions'); return $list->First(); @@ -1758,9 +1890,8 @@ class Versioned extends DataExtension implements TemplateGlobalProvider { * @return DataList */ public static function get_all_versions($class, $id) { - $baseClass = ClassInfo::baseDataClass($class); $list = DataList::create($class) - ->where("\"$baseClass\".\"RecordID\" = $id") + ->filter('ID', $id) ->setDataQueryParam('Versioned.mode', 'all_versions'); return $list; @@ -1847,6 +1978,11 @@ class Versioned_Version extends ViewableData { */ protected $object; + /** + * Create a new version from a database row + * + * @param array $record + */ public function __construct($record) { $this->record = $record; $record['ID'] = $record['RecordID']; @@ -1859,6 +1995,8 @@ class Versioned_Version extends ViewableData { } /** + * Either 'published' if published, or 'internal' if not. + * * @return string */ public function PublishedClass() { @@ -1866,6 +2004,8 @@ class Versioned_Version extends ViewableData { } /** + * Author of this DataObject + * * @return Member */ public function Author() { @@ -1873,6 +2013,8 @@ class Versioned_Version extends ViewableData { } /** + * Member object of the person who last published this record + * * @return Member */ public function Publisher() { @@ -1884,6 +2026,8 @@ class Versioned_Version extends ViewableData { } /** + * True if this record is published via publish() method + * * @return boolean */ public function Published() { @@ -1891,36 +2035,45 @@ class Versioned_Version extends ViewableData { } /** - * Copied from DataObject to allow access via dot notation. + * Traverses to a field referenced by relationships between data objects, returning the value + * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName) + * + * @param $fieldName string + * @return string | null - will return null on a missing value */ public function relField($fieldName) { $component = $this; + // We're dealing with relations here so we traverse the dot syntax if(strpos($fieldName, '.') !== false) { - $parts = explode('.', $fieldName); - $fieldName = array_pop($parts); - - // Traverse dot syntax - foreach($parts as $relation) { - if($component instanceof SS_List) { - if(method_exists($component,$relation)) { + $relations = explode('.', $fieldName); + $fieldName = array_pop($relations); + foreach($relations as $relation) { + // Inspect $component for element $relation + if($component->hasMethod($relation)) { + // Check nested method $component = $component->$relation(); - } else { + } elseif($component instanceof SS_List) { + // Select adjacent relation from DataList $component = $component->relation($relation); - } + } elseif($component instanceof DataObject + && ($dbObject = $component->dbObject($relation)) + ) { + // Select db object + $component = $dbObject; } else { - $component = $component->$relation(); + user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR); } } } - // Unlike has-one's, these "relations" can return false - if($component) { + // Bail if the component is null + if(!$component) { + return null; + } if ($component->hasMethod($fieldName)) { return $component->$fieldName(); } - return $component->$fieldName; } } -} diff --git a/tests/model/DataDifferencerTest.php b/tests/model/DataDifferencerTest.php index dfceedaed..b595f2cc8 100644 --- a/tests/model/DataDifferencerTest.php +++ b/tests/model/DataDifferencerTest.php @@ -19,6 +19,8 @@ class DataDifferencerTest extends SapphireTest { public function setUp() { parent::setUp(); + Versioned::reading_stage('Stage'); + // Set backend root to /DataDifferencerTest AssetStoreTest_SpyStore::activate('DataDifferencerTest'); @@ -39,11 +41,13 @@ class DataDifferencerTest extends SapphireTest { public function testArrayValues() { $obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1'); + $beforeVersion = $obj1->Version; // create a new version $obj1->Choices = 'a'; $obj1->write(); - $obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1); - $obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version); + $afterVersion = $obj1->Version; + $obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $beforeVersion); + $obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $afterVersion); $differ = new DataDifferencer($obj1v1, $obj1v2); $obj1Diff = $differ->diffedData(); // TODO Using getter would split up field again, bug only caused by simulating @@ -52,6 +56,7 @@ class DataDifferencerTest extends SapphireTest { } public function testHasOnes() { + /** @var DataDifferencerTest_Object $obj1 */ $obj1 = $this->objFromFixture('DataDifferencerTest_Object', 'obj1'); $image1 = $this->objFromFixture('Image', 'image1'); $image2 = $this->objFromFixture('Image', 'image2'); @@ -59,14 +64,19 @@ class DataDifferencerTest extends SapphireTest { $relobj2 = $this->objFromFixture('DataDifferencerTest_HasOneRelationObject', 'relobj2'); // create a new version + $beforeVersion = $obj1->Version; $obj1->ImageID = $image2->ID; $obj1->HasOneRelationID = $relobj2->ID; $obj1->write(); - $obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version-1); - $obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $obj1->Version); + $afterVersion = $obj1->Version; + $this->assertNotEquals($beforeVersion, $afterVersion); + /** @var DataDifferencerTest_Object $obj1v1 */ + $obj1v1 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $beforeVersion); + /** @var DataDifferencerTest_Object $obj1v2 */ + $obj1v2 = Versioned::get_version('DataDifferencerTest_Object', $obj1->ID, $afterVersion); $differ = new DataDifferencer($obj1v1, $obj1v2); $obj1Diff = $differ->diffedData(); - + $this->assertContains($image1->Name, $obj1Diff->getField('Image')); $this->assertContains($image2->Name, $obj1Diff->getField('Image')); $this->assertContains( @@ -76,6 +86,11 @@ class DataDifferencerTest extends SapphireTest { } } +/** + * @property string $Choices + * @method Image Image() + * @method DataDifferencerTest_HasOneRelationObject HasOneRelation() + */ class DataDifferencerTest_Object extends DataObject implements TestOnly { private static $extensions = array('Versioned("Stage", "Live")'); @@ -113,4 +128,4 @@ class DataDifferencerTest_HasOneRelationObject extends DataObject implements Tes private static $has_many = array( 'Objects' => 'DataDifferencerTest_Object' ); -} \ No newline at end of file +} diff --git a/tests/model/VersionedOwnershipTest.php b/tests/model/VersionedOwnershipTest.php new file mode 100644 index 000000000..c399d1cc7 --- /dev/null +++ b/tests/model/VersionedOwnershipTest.php @@ -0,0 +1,291 @@ +getFixtureFactory()->getFixtures() as $class => $fixtures) { + foreach($fixtures as $name => $id) { + if(stripos($name, '_published') !== false) { + /** @var Versioned|DataObject $object */ + $object = DataObject::get($class)->byID($id); + $object->publish('Stage', 'Live'); + } + } + } + } + + /** + * Test basic findOwned() in stage mode + */ + public function testFindOwned() { + /** @var VersionedOwnershipTest_Subclass $subclass1 */ + $subclass1 = $this->objFromFixture('VersionedOwnershipTest_Subclass', 'subclass1_published'); + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ['Title' => 'Attachment 1'], + ['Title' => 'Attachment 2'], + ['Title' => 'Attachment 5'], + ], + $subclass1->findOwned() + ); + + // Non-recursive search + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ], + $subclass1->findOwned(false) + ); + + /** @var VersionedOwnershipTest_Subclass $subclass2 */ + $subclass2 = $this->objFromFixture('VersionedOwnershipTest_Subclass', 'subclass2_published'); + $this->assertDOSEquals( + [ + ['Title' => 'Related 2'], + ['Title' => 'Attachment 3'], + ['Title' => 'Attachment 4'], + ['Title' => 'Attachment 5'], + ], + $subclass2->findOwned() + ); + + // Non-recursive search + $this->assertDOSEquals( + [ + ['Title' => 'Related 2'] + ], + $subclass2->findOwned(false) + ); + + /** @var VersionedOwnershipTest_Related $related1 */ + $related1 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related1'); + $this->assertDOSEquals( + [ + ['Title' => 'Attachment 1'], + ['Title' => 'Attachment 2'], + ['Title' => 'Attachment 5'], + ], + $related1->findOwned() + ); + + /** @var VersionedOwnershipTest_Related $related2 */ + $related2 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related2_published'); + $this->assertDOSEquals( + [ + ['Title' => 'Attachment 3'], + ['Title' => 'Attachment 4'], + ['Title' => 'Attachment 5'], + ], + $related2->findOwned() + ); + } + + /** + * Test findOwners + */ + public function testFindOwners() { + /** @var VersionedOwnershipTest_Attachment $attachment1 */ + $attachment1 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment1'); + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ['Title' => 'Subclass 1'], + ], + $attachment1->findOwners() + ); + + // Non-recursive search + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ], + $attachment1->findOwners(false) + ); + + /** @var VersionedOwnershipTest_Attachment $attachment5 */ + $attachment5 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment5_published'); + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ['Title' => 'Related 2'], + ['Title' => 'Subclass 1'], + ['Title' => 'Subclass 2'], + ], + $attachment5->findOwners() + ); + + // Non-recursive + $this->assertDOSEquals( + [ + ['Title' => 'Related 1'], + ['Title' => 'Related 2'], + ], + $attachment5->findOwners(false) + ); + + /** @var VersionedOwnershipTest_Related $related1 */ + $related1 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related1'); + $this->assertDOSEquals( + [ + ['Title' => 'Subclass 1'], + ], + $related1->findOwners() + ); + } + + /** + * Test findOwners on Live stage + */ + public function testFindOwnersLive() { + // Modify a few records on stage + $related2 = $this->objFromFixture('VersionedOwnershipTest_Related', 'related2_published'); + $related2->Title .= ' Modified'; + $related2->write(); + $attachment3 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment3_published'); + $attachment3->Title .= ' Modified'; + $attachment3->write(); + $attachment4 = $this->objFromFixture('VersionedOwnershipTest_Attachment', 'attachment4_published'); + $attachment4->delete(); + $subclass2ID = $this->idFromFixture('VersionedOwnershipTest_Subclass', 'subclass2_published'); + + // Check that stage record is ok + /** @var VersionedOwnershipTest_Subclass $subclass2Stage */ + $subclass2Stage = \Versioned::get_by_stage('VersionedOwnershipTest_Subclass', 'Stage')->byID($subclass2ID); + $this->assertDOSEquals( + [ + ['Title' => 'Related 2 Modified'], + ['Title' => 'Attachment 3 Modified'], + ['Title' => 'Attachment 5'], + ], + $subclass2Stage->findOwned() + ); + + // Non-recursive + $this->assertDOSEquals( + [ + ['Title' => 'Related 2 Modified'], + ], + $subclass2Stage->findOwned(false) + ); + + // Live records are unchanged + /** @var VersionedOwnershipTest_Subclass $subclass2Live */ + $subclass2Live = \Versioned::get_by_stage('VersionedOwnershipTest_Subclass', 'Live')->byID($subclass2ID); + $this->assertDOSEquals( + [ + ['Title' => 'Related 2'], + ['Title' => 'Attachment 3'], + ['Title' => 'Attachment 4'], + ['Title' => 'Attachment 5'], + ], + $subclass2Live->findOwned() + ); + + // Test non-recursive + $this->assertDOSEquals( + [ + ['Title' => 'Related 2'], + ], + $subclass2Live->findOwned(false) + ); + } +} + +/** + * @mixin Versioned + */ +class VersionedOwnershipTest_Object extends DataObject implements TestOnly { + private static $extensions = array( + 'Versioned', + ); + + private static $db = array( + 'Title' => 'Varchar(255)', + 'Content' => 'Text', + ); +} + +class VersionedOwnershipTest_Subclass extends VersionedOwnershipTest_Object implements TestOnly { + private static $db = array( + 'Description' => 'Text', + ); + + private static $has_one = array( + 'Related' => 'VersionedOwnershipTest_Related', + ); + + private static $owns = array( + 'Related', + ); +} + +/** + * @mixin Versioned + */ +class VersionedOwnershipTest_Related extends DataObject implements TestOnly { + private static $extensions = array( + 'Versioned', + ); + + private static $db = array( + 'Title' => 'Varchar(255)', + ); + + private static $has_many = array( + 'Parents' => 'VersionedOwnershipTest_Subclass.Related', + ); + + private static $owned_by = array( + 'Parents', + ); + + private static $many_many = array( + // Note : Currently unversioned, take care + 'Attachments' => 'VersionedOwnershipTest_Attachment', + ); + + private static $owns = array( + 'Attachments', + ); +} + +/** + * @mixin Versioned + */ +class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly { + + private static $extensions = array( + 'Versioned', + ); + + private static $db = array( + 'Title' => 'Varchar(255)', + ); + + private static $belongs_many_many = array( + 'AttachedTo' => 'VersionedOwnershipTest_Related.Attachments' + ); + + private static $owned_by = array( + 'AttachedTo' + ); +} diff --git a/tests/model/VersionedOwnershipTest.yml b/tests/model/VersionedOwnershipTest.yml new file mode 100644 index 000000000..270de2cc3 --- /dev/null +++ b/tests/model/VersionedOwnershipTest.yml @@ -0,0 +1,31 @@ +VersionedOwnershipTest_Attachment: + attachment1: + Title: 'Attachment 1' + attachment2: + Title: 'Attachment 2' + attachment3_published: + Title: 'Attachment 3' + attachment4_published: + Title: 'Attachment 4' + attachment5_published: + Title: 'Attachment 5' + +VersionedOwnershipTest_Related: + related1: + Title: 'Related 1' + Attachments: =>VersionedOwnershipTest_Attachment.attachment1,=>VersionedOwnershipTest_Attachment.attachment2,=>VersionedOwnershipTest_Attachment.attachment5_published + related2_published: + Title: 'Related 2' + Attachments: =>VersionedOwnershipTest_Attachment.attachment3_published,=>VersionedOwnershipTest_Attachment.attachment4_published,=>VersionedOwnershipTest_Attachment.attachment5_published + +VersionedOwnershipTest_Subclass: + subclass1_published: + Title: 'Subclass 1' + Related: =>VersionedOwnershipTest_Related.related1 + subclass2_published: + Title: 'Subclass 2' + Related: =>VersionedOwnershipTest_Related.related2_published + +VersionedOwnershipTest_Object: + object1: + Title: 'Object 1' diff --git a/tests/security/SecurityTest.php b/tests/security/SecurityTest.php index 03eac3ed5..15be5b652 100644 --- a/tests/security/SecurityTest.php +++ b/tests/security/SecurityTest.php @@ -522,7 +522,7 @@ class SecurityTest extends FunctionalTest { )); $this->assertEquals($attempt->Status, 'Failure'); $this->assertEquals($attempt->Email, 'sam@silverstripe.com'); - $this->assertEquals($attempt->Member(), $member); + $this->assertEquals($attempt->MemberID, $member->ID); /* UNSUCCESSFUL ATTEMPTS WITH NONEXISTING USER ARE LOGGED */ $this->doTestLoginForm('wronguser@silverstripe.com', 'wrongpassword'); @@ -551,7 +551,7 @@ class SecurityTest extends FunctionalTest { $this->assertTrue(is_object($attempt)); $this->assertEquals($attempt->Status, 'Success'); $this->assertEquals($attempt->Email, 'sam@silverstripe.com'); - $this->assertEquals($attempt->Member(), $member); + $this->assertEquals($attempt->MemberID, $member->ID); } public function testDatabaseIsReadyWithInsufficientMemberColumns() {