Merge pull request #5095 from open-sausages/pulls/4.0/ownership-api

API Ownership API Implementation
This commit is contained in:
Ingo Schommer 2016-03-03 17:39:07 +13:00
commit 0f08176c6c
11 changed files with 743 additions and 311 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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(

View File

@ -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();

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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'
);
}
}

View File

@ -0,0 +1,291 @@
<?php
/**
* Tests ownership API of versioned DataObjects
*/
class VersionedOwnershipTest extends SapphireTest {
protected $extraDataObjects = array(
'VersionedOwnershipTest_Object',
'VersionedOwnershipTest_Subclass',
'VersionedOwnershipTest_Related',
'VersionedOwnershipTest_Attachment',
);
protected static $fixture_file = 'VersionedOwnershipTest.yml';
public function setUp()
{
parent::setUp();
Versioned::reading_stage('Stage');
// Automatically publish any object named *_published
foreach($this->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'
);
}

View File

@ -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'

View File

@ -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() {