API Split out SilverStripe\ORM\Versioned into new module

This commit is contained in:
Damian Mooyman 2017-03-21 16:22:23 +13:00
parent a9e1ce48df
commit ac3a9c9e6e
86 changed files with 155 additions and 8683 deletions

View File

@ -41,7 +41,7 @@ before_script:
- composer install --prefer-dist
- "if [ \"$DB\" = \"PGSQL\" ]; then composer require silverstripe/postgresql:2.0.x-dev --prefer-dist; fi"
- "if [ \"$DB\" = \"SQLITE\" ]; then composer require silverstripe/sqlite3:2.0.x-dev --prefer-dist; fi"
- composer require silverstripe/config:1.0.x-dev silverstripe/admin:1.0.x-dev silverstripe/assets:1.0.x-dev --prefer-dist
- composer require silverstripe/config:1.0.x-dev silverstripe/admin:1.0.x-dev silverstripe/assets:1.0.x-dev silverstripe/versioned:1.0.x-dev --prefer-dist
- "if [ \"$CMS_TEST\" = \"1\" ]; then composer require silverstripe/cms:4.0.x-dev silverstripe/siteconfig:4.0.x-dev silverstripe/reports:4.0.x-dev --prefer-dist; fi"
- "if [ \"$CMS_TEST\" = \"1\" ]; then php ./cms/tests/bootstrap/mysite.php; fi"
- "if [ \"$BEHAT_TEST\" = \"1\" ]; then sh -e /etc/init.d/xvfb start; sleep 3; fi"

View File

@ -115,14 +115,6 @@ mappings:
SilverStripe\Model\FieldType\DBVarchar: SilverStripe\ORM\FieldType\DBVarchar
SilverStripe\Model\FieldType\DBYear: SilverStripe\ORM\FieldType\DBYear
SilverStripe\Model\FieldType\SS_Datetime: SilverStripe\ORM\FieldType\DBDatetime
ChangeSet: SilverStripe\ORM\Versioning\ChangeSet
ChangeSetItem: SilverStripe\ORM\Versioning\ChangeSetItem
DataDifferencer: SilverStripe\ORM\Versioning\DataDifferencer
VersionableExtension: SilverStripe\ORM\Versioning\VersionableExtension
Versioned: SilverStripe\ORM\Versioning\Versioned
Versioned_Version: SilverStripe\ORM\Versioning\Versioned_Version
VersionedGridFieldDetailForm: SilverStripe\ORM\Versioning\VersionedGridFieldDetailForm
VersionedGridFieldItemRequest: SilverStripe\ORM\Versioning\VersionedGridFieldItemRequest
Hierarchy: SilverStripe\ORM\Hierarchy\Hierarchy
Authenticator: SilverStripe\Security\Authenticator
BasicAuth: SilverStripe\Security\BasicAuth
@ -288,7 +280,7 @@ mappings:
RequestHandler: SilverStripe\Control\RequestHandler
RequestProcessor: SilverStripe\Control\RequestProcessor
Session: SilverStripe\Control\Session
VersionedRequestFilter: SilverStripe\Control\VersionedRequestFilter
VersionedRequestFilter: SilverStripe\Versioned\VersionedRequestFilter
Email: SilverStripe\Control\Email\Email
Mailer: SilverStripe\Control\Email\Mailer
RSSFeed: SilverStripe\Control\RSS\RSSFeed
@ -680,22 +672,22 @@ mappings:
ArrayLibTest: SilverStripe\ORM\Tests\ArrayLibTest
ArrayListTest: SilverStripe\ORM\Tests\ArrayListTest
ArrayListTest_Object: SilverStripe\ORM\Tests\ArrayListTest\TestObject
ChangeSetItemTest_Versioned: SilverStripe\ORM\Tests\ChangeSetItemTest\VersionedObject
ChangeSetItemTest: SilverStripe\ORM\Tests\ChangeSetItemTest
ChangeSetTest_Permissions: SilverStripe\ORM\Tests\ChangeSetTest\Permissions
ChangeSetTest_Base: SilverStripe\ORM\Tests\ChangeSetTest\BaseObject
ChangeSetTest_Mid: SilverStripe\ORM\Tests\ChangeSetTest\MidObject
ChangeSetTest_End: SilverStripe\ORM\Tests\ChangeSetTest\EndObject
ChangeSetTest_EndChild: SilverStripe\ORM\Tests\ChangeSetTest\EndObjectChild
ChangeSetTest: SilverStripe\ORM\Tests\ChangeSetTest
ChangeSetItemTest_Versioned: SilverStripe\Versioned\Tests\ChangeSetItemTest\VersionedObject
ChangeSetItemTest: SilverStripe\Versioned\Tests\ChangeSetItemTest
ChangeSetTest_Permissions: SilverStripe\Versioned\Tests\ChangeSetTest\Permissions
ChangeSetTest_Base: SilverStripe\Versioned\Tests\ChangeSetTest\BaseObject
ChangeSetTest_Mid: SilverStripe\Versioned\Tests\ChangeSetTest\MidObject
ChangeSetTest_End: SilverStripe\Versioned\Tests\ChangeSetTest\EndObject
ChangeSetTest_EndChild: SilverStripe\Versioned\Tests\ChangeSetTest\EndObjectChild
ChangeSetTest: SilverStripe\Versioned\Tests\ChangeSetTest
ComponentSetTest: SilverStripe\ORM\Tests\ComponentSetTest
ComponentSetTest_Player: SilverStripe\ORM\Tests\ComponentSetTest\Player
ComponentSetTest_Team: SilverStripe\ORM\Tests\ComponentSetTest\Team
DatabaseTest: SilverStripe\ORM\Tests\DatabaseTest
DatabaseTest_MyObject: SilverStripe\ORM\Tests\DatabaseTest\MyObject
DataDifferencerTest: SilverStripe\ORM\Tests\DataDifferencerTest
DataDifferencerTest_Object: SilverStripe\ORM\Tests\DataDifferencerTest\TestObject
DataDifferencerTest_HasOneRelationObject: SilverStripe\ORM\Tests\DataDifferencerTest\HasOneRelationObject
DataDifferencerTest: SilverStripe\Versioned\Tests\DataDifferencerTest
DataDifferencerTest_Object: SilverStripe\Versioned\Tests\DataDifferencerTest\TestObject
DataDifferencerTest_HasOneRelationObject: SilverStripe\Versioned\Tests\DataDifferencerTest\HasOneRelationObject
DataExtensionTest: SilverStripe\ORM\Tests\DataExtensionTest
DataExtensionTest_Member: SilverStripe\ORM\Tests\DataExtensionTest\TestMember
DataExtensionTest_Player: SilverStripe\ORM\Tests\DataExtensionTest\Player
@ -718,8 +710,8 @@ mappings:
DataObjectDuplicateTestClass2: SilverStripe\ORM\Tests\DataObjectDuplicationTest\Class2
DataObjectDuplicateTestClass3: SilverStripe\ORM\Tests\DataObjectDuplicationTest\Class3
DataObjectLazyLoadingTest: SilverStripe\ORM\Tests\DataObjectLazyLoadingTest
VersionedLazy_DataObject: SilverStripe\ORM\Tests\DataObjectLazyLoadingTest\VersionedObject
VersionedLazySub_DataObject: SilverStripe\ORM\Tests\DataObjectLazyLoadingTest\VersionedSubObject
VersionedLazy_DataObject: SilverStripe\Versioned\Tests\VersionedDataObjectLazyLoadingTest\VersionedObject
VersionedLazySub_DataObject: SilverStripe\Versioned\Tests\VersionedDataObjectLazyLoadingTest\VersionedSubObject
DataObjectSchemaGenerationTest: SilverStripe\ORM\Tests\DataObjectSchemaGenerationTest
DataObjectSchemaGenerationTest_DO: SilverStripe\ORM\Tests\DataObjectSchemaGenerationTest\TestObject
DataObjectSchemaGenerationTest_IndexDO: SilverStripe\ORM\Tests\DataObjectSchemaGenerationTest\TestIndexObject
@ -809,9 +801,9 @@ mappings:
ManyManyThroughListTest_Object: SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject
ManyManyThroughListTest_JoinObject: SilverStripe\ORM\Tests\ManyManyThroughListTest\JoinObject
ManyManyThroughListTest_Item: SilverStripe\ORM\Tests\ManyManyThroughListTest\Item
ManyManyThroughListTest_VersionedObject: SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedObject
ManyManyThroughListTest_VersionedJoinObject: SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedJoinObject
ManyManyThroughListTest_VersionedItem: SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedItem
ManyManyThroughListTest_VersionedObject: SilverStripe\Versioned\Tests\VersionedManyManyThroughListTest\VersionedObject
ManyManyThroughListTest_VersionedJoinObject: SilverStripe\Versioned\Tests\VersionedManyManyThroughListTest\VersionedJoinObject
ManyManyThroughListTest_VersionedItem: SilverStripe\Versioned\Tests\VersionedManyManyThroughListTest\VersionedItem
MapTest: SilverStripe\ORM\Tests\MapTest
MySQLDatabaseTest: SilverStripe\ORM\Tests\MySQLDatabaseTest
MySQLDatabaseTest_Data: SilverStripe\ORM\Tests\MySQLDatabaseTest\Data
@ -834,31 +826,31 @@ mappings:
UnsavedRelationListTest_DataObject: SilverStripe\ORM\Tests\UnsavedRelationListTest\TestObject
URLSegmentFilterTest: SilverStripe\ORM\Tests\URLSegmentFilterTest
ValidationExceptionTest: SilverStripe\ORM\Tests\ValidationExceptionTest
VersionableExtensionsTest: SilverStripe\ORM\Tests\VersionableExtensionsTest
VersionableExtensionsTest_DataObject: SilverStripe\ORM\Tests\VersionableExtensionsTest\TestObject
VersionableExtensionsTest_Extension: SilverStripe\ORM\Tests\VersionableExtensionsTest\TestExtension
VersionedOwnershipTest: SilverStripe\ORM\Tests\VersionedOwnershipTest
VersionedOwnershipTest_Object: SilverStripe\ORM\Tests\VersionedOwnershipTest\TestObject
VersionedOwnershipTest_Subclass: SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass
VersionedOwnershipTest_Related: SilverStripe\ORM\Tests\VersionedOwnershipTest\Related
VersionedOwnershipTest_RelatedMany: SilverStripe\ORM\Tests\VersionedOwnershipTest\RelatedMany
VersionedOwnershipTest_Attachment: SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment
VersionedOwnershipTest_Page: SilverStripe\ORM\Tests\VersionedOwnershipTest\TestPage
VersionedOwnershipTest_Banner: SilverStripe\ORM\Tests\VersionedOwnershipTest\Banner
VersionedOwnershipTest_CustomRelation: SilverStripe\ORM\Tests\VersionedOwnershipTest\CustomRelation
VersionedOwnershipTest_Image: SilverStripe\ORM\Tests\VersionedOwnershipTest\Image
VersionedTest: SilverStripe\ORM\Tests\VersionedTest
VersionedTest_DataObject: SilverStripe\ORM\Tests\VersionedTest\TestObject
VersionedTest_WithIndexes: SilverStripe\ORM\Tests\VersionedTest\WithIndexes
VersionedTest_RelatedWithoutVersion: SilverStripe\ORM\Tests\VersionedTest\RelatedWithoutversion
VersionedTest_Subclass: SilverStripe\ORM\Tests\VersionedTest\Subclass
VersionedTest_AnotherSubclass: SilverStripe\ORM\Tests\VersionedTest\AnotherSubclass
VersionedTest_UnversionedWithField: SilverStripe\ORM\Tests\VersionedTest\UnversionedWithField
VersionedTest_SingleStage: SilverStripe\ORM\Tests\VersionedTest\SingleStage
VersionedTest_PublicStage: SilverStripe\ORM\Tests\VersionedTest\PublicStage
VersionedTest_PublicViaExtension: SilverStripe\ORM\Tests\VersionedTest\PublicViaExtension
VersionedTest_PublicExtension: SilverStripe\ORM\Tests\VersionedTest\PublicExtension
VersionedTest_CustomTable: SilverStripe\ORM\Tests\VersionedTest\CustomTable
VersionableExtensionsTest: SilverStripe\Versioned\Tests\VersionableExtensionsTest
VersionableExtensionsTest_DataObject: SilverStripe\Versioned\Tests\VersionableExtensionsTest\TestObject
VersionableExtensionsTest_Extension: SilverStripe\Versioned\Tests\VersionableExtensionsTest\TestExtension
VersionedOwnershipTest: SilverStripe\Versioned\Tests\VersionedOwnershipTest
VersionedOwnershipTest_Object: SilverStripe\Versioned\Tests\VersionedOwnershipTest\TestObject
VersionedOwnershipTest_Subclass: SilverStripe\Versioned\Tests\VersionedOwnershipTest\Subclass
VersionedOwnershipTest_Related: SilverStripe\Versioned\Tests\VersionedOwnershipTest\Related
VersionedOwnershipTest_RelatedMany: SilverStripe\Versioned\Tests\VersionedOwnershipTest\RelatedMany
VersionedOwnershipTest_Attachment: SilverStripe\Versioned\Tests\VersionedOwnershipTest\Attachment
VersionedOwnershipTest_Page: SilverStripe\Versioned\Tests\VersionedOwnershipTest\TestPage
VersionedOwnershipTest_Banner: SilverStripe\Versioned\Tests\VersionedOwnershipTest\Banner
VersionedOwnershipTest_CustomRelation: SilverStripe\Versioned\Tests\VersionedOwnershipTest\CustomRelation
VersionedOwnershipTest_Image: SilverStripe\Versioned\Tests\VersionedOwnershipTest\Image
VersionedTest: SilverStripe\Versioned\Tests\VersionedTest
VersionedTest_DataObject: SilverStripe\Versioned\Tests\VersionedTest\TestObject
VersionedTest_WithIndexes: SilverStripe\Versioned\Tests\VersionedTest\WithIndexes
VersionedTest_RelatedWithoutVersion: SilverStripe\Versioned\Tests\VersionedTest\RelatedWithoutversion
VersionedTest_Subclass: SilverStripe\Versioned\Tests\VersionedTest\Subclass
VersionedTest_AnotherSubclass: SilverStripe\Versioned\Tests\VersionedTest\AnotherSubclass
VersionedTest_UnversionedWithField: SilverStripe\Versioned\Tests\VersionedTest\UnversionedWithField
VersionedTest_SingleStage: SilverStripe\Versioned\Tests\VersionedTest\SingleStage
VersionedTest_PublicStage: SilverStripe\Versioned\Tests\VersionedTest\PublicStage
VersionedTest_PublicViaExtension: SilverStripe\Versioned\Tests\VersionedTest\PublicViaExtension
VersionedTest_PublicExtension: SilverStripe\Versioned\Tests\VersionedTest\PublicExtension
VersionedTest_CustomTable: SilverStripe\Versioned\Tests\VersionedTest\CustomTable
BasicAuthTest: SilverStripe\Security\Tests\BasicAuthTest
BasicAuthTest_ControllerSecuredWithPermission: SilverStripe\Security\Tests\BasicAuthTest\ControllerSecuredWithPermission
BasicAuthTest_ControllerSecuredWithoutPermission: SilverStripe\Security\Tests\BasicAuthTest\ControllerSecuredWithoutPermission

View File

@ -4,10 +4,8 @@ Name: requestprocessors
SilverStripe\Core\Injector\Injector:
FlushRequestFilter:
class: SilverStripe\Control\FlushRequestFilter
VersionedRequestFilter:
class: SilverStripe\Control\VersionedRequestFilter
SilverStripe\Control\RequestProcessor:
properties:
filters:
- '%$FlushRequestFilter'
- '%$VersionedRequestFilter'

View File

@ -1,6 +0,0 @@
---
Name: versioning
---
SilverStripe\Forms\GridField\GridFieldDetailForm:
extensions:
- SilverStripe\ORM\Versioning\VersionedGridFieldDetailForm

View File

@ -33,6 +33,7 @@
},
"require-dev": {
"phpunit/PHPUnit": "^5.7",
"silverstripe/versioned": "^1.0@dev",
"silverstripe/behat-extension": "^2.1.0",
"silverstripe/serve": "dev-master",
"silverstripe/testsession": "^2.0.0-alpha3",

View File

@ -20,7 +20,9 @@
<testsuite name="Default">
<directory>tests/php</directory>
<directory>silverstripe-admin/tests/php</directory>
<directory>silverstripe-assets/tests/php</directory>
<directory>versioned/tests/php</directory>
</testsuite>
<listeners>

View File

@ -6,12 +6,11 @@ use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\ArrayLib;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use SilverStripe\View\Requirements;
use SilverStripe\View\Requirements_Backend;
use SilverStripe\View\TemplateGlobalProvider;
@ -280,7 +279,9 @@ class Director implements TemplateGlobalProvider
// These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics.
$oldReadingMode = Versioned::get_reading_mode();
if (class_exists(Versioned::class)) {
$oldReadingMode = Versioned::get_reading_mode();
}
$getVars = array();
if (!$httpMethod) {
@ -330,7 +331,9 @@ class Director implements TemplateGlobalProvider
// These are needed so that calling Director::test() does not muck with whoever is calling it.
// Really, it's some inappropriate coupling and should be resolved by making less use of statics
Versioned::set_reading_mode($oldReadingMode);
if (class_exists(Versioned::class)) {
Versioned::set_reading_mode($oldReadingMode);
}
Injector::unnest(); // Restore old CookieJar, etc
Config::unnest();

View File

@ -1,57 +0,0 @@
<?php
namespace SilverStripe\Control;
use SilverStripe\Core\Convert;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Security\Security;
/**
* Initialises the versioned stage when a request is made.
*/
class VersionedRequestFilter implements RequestFilter
{
public function preRequest(HTTPRequest $request, Session $session, DataModel $model)
{
// Bootstrap session so that Session::get() accesses the right instance
$dummyController = new Controller();
$dummyController->setSession($session);
$dummyController->setRequest($request);
$dummyController->pushCurrent();
// Block non-authenticated users from setting the stage mode
if (!Versioned::can_choose_site_stage($request)) {
$permissionMessage = sprintf(
_t(
"ContentController.DRAFT_SITE_ACCESS_RESTRICTION",
'You must log in with your CMS password in order to view the draft or archived content. '.
'<a href="%s">Click here to go back to the published site.</a>'
),
Convert::raw2xml(Controller::join_links(Director::baseURL(), $request->getURL(), "?stage=Live"))
);
// Force output since RequestFilter::preRequest doesn't support response overriding
$response = Security::permissionFailure($dummyController, $permissionMessage);
$session->inst_save();
$dummyController->popCurrent();
// Prevent output in testing
if (class_exists('SilverStripe\\Dev\\SapphireTest', false) && SapphireTest::is_running_test()) {
throw new HTTPResponse_Exception($response);
}
$response->output();
die;
}
Versioned::choose_site_stage();
$dummyController->popCurrent();
return true;
}
public function postRequest(HTTPRequest $request, HTTPResponse $response, DataModel $model)
{
return true;
}
}

View File

@ -8,7 +8,7 @@ use SilverStripe\Control\Director;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DatabaseAdmin;
use SilverStripe\Security\Permission;
use SilverStripe\Security\Security;
@ -67,7 +67,9 @@ class DevelopmentAdmin extends Controller
// Backwards compat: Default to "draft" stage, which is important
// for tasks like dev/build which call DataObject->requireDefaultRecords(),
// but also for other administrative tasks which have assumptions about the default stage.
Versioned::set_stage(Versioned::DRAFT);
if (class_exists(Versioned::class)) {
Versioned::set_stage(Versioned::DRAFT);
}
}
public function index()

View File

@ -23,7 +23,7 @@ use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Resettable;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataModel;
use SilverStripe\ORM\FieldType\DBDatetime;
@ -235,7 +235,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase
Injector::nest();
$this->originalEnv = Director::get_environment_type();
$this->originalReadingMode = Versioned::get_reading_mode();
if (class_exists(Versioned::class)) {
$this->originalReadingMode = Versioned::get_reading_mode();
}
// We cannot run the tests on this abstract class.
if (get_class($this) == __CLASS__) {
@ -258,7 +260,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase
$this->originalRequirements = Requirements::backend();
Member::set_password_validator(null);
Cookie::config()->update('report_errors', false);
if (class_exists('SilverStripe\\CMS\\Controllers\\RootURLController')) {
if (class_exists(RootURLController::class)) {
RootURLController::reset();
}
@ -597,7 +599,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase
}
Director::set_environment_type($this->originalEnv);
Versioned::set_reading_mode($this->originalReadingMode);
if (class_exists(Versioned::class)) {
Versioned::set_reading_mode($this->originalReadingMode);
}
//unnest injector / config now that tests are over
Injector::unnest();

View File

@ -574,6 +574,14 @@ class DataObjectSchema
list($childClass, $relationName) = explode('.', $specification, 2);
}
// Check child class exists
if (!class_exists($childClass)) {
throw new LogicException(
"belongs_many_many relation {$parentClass}.{$component} points to "
. "{$childClass} which does not exist"
);
}
// We need to find the inverse component name, if not explicitly given
if (!$relationName) {
$relationName = $this->getManyManyInverseRelationship($childClass, $parentClass);

View File

@ -9,7 +9,7 @@ use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Security\Security;
use SilverStripe\Security\Permission;

View File

@ -12,7 +12,7 @@ use SilverStripe\ORM\ValidationResult;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use Exception;
/**
@ -685,7 +685,7 @@ class Hierarchy extends DataExtension implements Resettable
$stageChildren = $this->owner->stageChildren(true);
// Add live site content that doesn't exist on the stage site, if required.
if ($this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
if ($this->owner->hasExtension(Versioned::class)) {
// Next, go through the live children. Only some of these will be listed
$liveChildren = $this->owner->liveChildren(true, true);
if ($liveChildren) {
@ -715,7 +715,7 @@ class Hierarchy extends DataExtension implements Resettable
*/
public function AllHistoricalChildren()
{
if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
if (!$this->owner->hasExtension(Versioned::class)) {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
}
@ -736,7 +736,7 @@ class Hierarchy extends DataExtension implements Resettable
*/
public function numHistoricalChildren()
{
if (!$this->owner->hasExtension('SilverStripe\ORM\Versioning\Versioned')) {
if (!$this->owner->hasExtension(Versioned::class)) {
throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied');
}

View File

@ -1,579 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\TabSet;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\ReadonlyField;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\UnexpectedDataException;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use BadMethodCallException;
use Exception;
use LogicException;
/**
* The ChangeSet model tracks several VersionedAndStaged objects for later publication as a single
* atomic action
*
* @method HasManyList Changes()
* @method Member Owner()
* @property string $Name
* @property string $State
* @property bool $IsInferred
*/
class ChangeSet extends DataObject
{
private static $singular_name = 'Campaign';
private static $plural_name = 'Campaigns';
/** An active changeset */
const STATE_OPEN = 'open';
/** A changeset which is reverted and closed */
const STATE_REVERTED = 'reverted';
/** A changeset which is published and closed */
const STATE_PUBLISHED = 'published';
private static $table_name = 'ChangeSet';
private static $db = array(
'Name' => 'Varchar',
'State' => "Enum('open,published,reverted','open')",
'IsInferred' => 'Boolean(0)' // True if created automatically
);
private static $has_many = array(
'Changes' => 'SilverStripe\ORM\Versioning\ChangeSetItem',
);
private static $defaults = array(
'State' => 'open'
);
private static $has_one = array(
'Owner' => 'SilverStripe\\Security\\Member',
);
private static $casting = array(
'Description' => 'Text',
);
/**
* List of classes to set apart in description
*
* @config
* @var array
*/
private static $important_classes = array(
'SilverStripe\\CMS\\Model\\SiteTree',
'SilverStripe\\Assets\\File',
);
private static $summary_fields = [
'Name' => 'Title',
'ChangesCount' => 'Changes',
'Description' => 'Description',
];
/**
* Default permission to require for publishers.
* Publishers must either be able to use the campaign admin, or have all admin access.
*
* Also used as default permission for ChangeSetItem default permission.
*
* @config
* @var array
*/
private static $required_permission = array('CMS_ACCESS_CampaignAdmin', 'CMS_ACCESS_LeftAndMain');
/**
* Publish this changeset, then closes it.
*
* @throws Exception
* @return bool True if successful
*/
public function publish()
{
// Logical checks prior to publish
if ($this->State !== static::STATE_OPEN) {
throw new BadMethodCallException(
"ChangeSet can't be published if it has been already published or reverted."
);
}
if (!$this->isSynced()) {
throw new ValidationException(
"ChangeSet does not include all necessary changes and cannot be published."
);
}
if (!$this->canPublish()) {
throw new LogicException("The current member does not have permission to publish this ChangeSet.");
}
DB::get_conn()->withTransaction(function () {
foreach ($this->Changes() as $change) {
/** @var ChangeSetItem $change */
$change->publish();
}
// Once this changeset is published, unlink any objects linking to
// records in this changeset as unlinked (set RelationID to 0).
// This is done as a safer alternative to deleting records on live that
// are deleted on stage.
foreach ($this->Changes() as $change) {
/** @var ChangeSetItem $change */
$change->unlinkDisownedObjects();
}
$this->State = static::STATE_PUBLISHED;
$this->write();
});
return true;
}
/**
* Add a new change to this changeset. Will automatically include all owned
* changes as those are dependencies of this item.
*
* @param DataObject $object
*/
public function addObject(DataObject $object)
{
if (!$this->isInDB()) {
throw new BadMethodCallException("ChangeSet must be saved before adding items");
}
if (!$object->isInDB()) {
throw new BadMethodCallException("Items must be saved before adding to a changeset");
}
$references = [
'ObjectID' => $object->ID,
'ObjectClass' => $object->baseClass(),
];
// Get existing item in case already added
$item = $this->Changes()->filter($references)->first();
if (!$item) {
$item = new ChangeSetItem($references);
$this->Changes()->add($item);
}
$item->ReferencedBy()->removeAll();
$item->Added = ChangeSetItem::EXPLICITLY;
$item->write();
$this->sync();
}
/**
* Remove an item from this changeset. Will automatically remove all changes
* which own (and thus depend on) the removed item.
*
* @param DataObject $object
*/
public function removeObject(DataObject $object)
{
$item = ChangeSetItem::get()->filter([
'ObjectID' => $object->ID,
'ObjectClass' => $object->baseClass(),
'ChangeSetID' => $this->ID
])->first();
if ($item) {
// TODO: Handle case of implicit added item being removed.
$item->delete();
}
$this->sync();
}
/**
* Build identifying string key for this object
*
* @param DataObject $item
* @return string
*/
protected function implicitKey(DataObject $item)
{
if ($item instanceof ChangeSetItem) {
return $item->ObjectClass.'.'.$item->ObjectID;
}
return $item->baseClass().'.'.$item->ID;
}
protected function calculateImplicit()
{
/** @var string[][] $explicit List of all items that have been explicitly added to this ChangeSet */
$explicit = array();
/** @var string[][] $referenced List of all items that are "referenced" by items in $explicit */
$referenced = array();
/** @var string[][] $references List of which explicit items reference each thing in referenced */
$references = array();
/** @var ChangeSetItem $item */
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::EXPLICITLY]) as $item) {
$explicitKey = $this->implicitKey($item);
$explicit[$explicitKey] = true;
foreach ($item->findReferenced() as $referee) {
try {
/** @var DataObject $referee */
$key = $this->implicitKey($referee);
$referenced[$key] = [
'ObjectID' => $referee->ID,
'ObjectClass' => $referee->baseClass(),
];
$references[$key][] = $item->ID;
// Skip any bad records
} catch (UnexpectedDataException $e) {
}
}
}
/** @var string[][] $explicit List of all items that are either in $explicit, $referenced or both */
$all = array_merge($referenced, $explicit);
/** @var string[][] $implicit Anything that is in $all, but not in $explicit, is an implicit inclusion */
$implicit = array_diff_key($all, $explicit);
foreach ($implicit as $key => $object) {
$implicit[$key]['ReferencedBy'] = $references[$key];
}
return $implicit;
}
/**
* Add implicit changes that should be included in this changeset
*
* When an item is created or changed, all it's owned items which have
* changes are implicitly added
*
* When an item is deleted, it's owner (even if that owner does not have changes)
* is implicitly added
*/
public function sync()
{
// Start a transaction (if we can)
DB::get_conn()->withTransaction(function () {
// Get the implicitly included items for this ChangeSet
$implicit = $this->calculateImplicit();
// Adjust the existing implicit ChangeSetItems for this ChangeSet
/** @var ChangeSetItem $item */
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
$objectKey = $this->implicitKey($item);
// If a ChangeSetItem exists, but isn't in $implicit, it's no longer required, so delete it
if (!array_key_exists($objectKey, $implicit)) {
$item->delete();
} // Otherwise it is required, so update ReferencedBy and remove from $implicit
else {
$item->ReferencedBy()->setByIDList($implicit[$objectKey]['ReferencedBy']);
unset($implicit[$objectKey]);
}
}
// Now $implicit is all those items that are implicitly included, but don't currently have a ChangeSetItem.
// So create new ChangeSetItems to match
foreach ($implicit as $key => $props) {
$item = new ChangeSetItem($props);
$item->Added = ChangeSetItem::IMPLICITLY;
$item->ChangeSetID = $this->ID;
$item->ReferencedBy()->setByIDList($props['ReferencedBy']);
$item->write();
}
});
}
/** Verify that any objects in this changeset include all owned changes */
public function isSynced()
{
$implicit = $this->calculateImplicit();
// Check the existing implicit ChangeSetItems for this ChangeSet
foreach ($this->Changes()->filter(['Added' => ChangeSetItem::IMPLICITLY]) as $item) {
$objectKey = $this->implicitKey($item);
// If a ChangeSetItem exists, but isn't in $implicit -> validation failure
if (!array_key_exists($objectKey, $implicit)) {
return false;
}
// Exists, remove from $implicit
unset($implicit[$objectKey]);
}
// If there's anything left in $implicit -> validation failure
return empty($implicit);
}
public function canView($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canEdit($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canCreate($member = null, $context = array())
{
return $this->can(__FUNCTION__, $member, $context);
}
public function canDelete($member = null)
{
return $this->can(__FUNCTION__, $member);
}
/**
* Check if this item is allowed to be published
*
* @param Member $member
* @return bool
*/
public function canPublish($member = null)
{
foreach ($this->Changes() as $change) {
/** @var ChangeSetItem $change */
if (!$change->canPublish($member)) {
return false;
}
}
return true;
}
/**
* Determine if there are changes to publish
*
* @return bool
*/
public function hasChanges()
{
// All changes must be publishable
/** @var ChangeSetItem $change */
foreach ($this->Changes() as $change) {
if ($change->hasChange()) {
return true;
}
}
return false;
}
/**
* Check if this changeset (if published) can be reverted
*
* @param Member $member
* @return bool
*/
public function canRevert($member = null)
{
// All changes must be publishable
foreach ($this->Changes() as $change) {
/** @var ChangeSetItem $change */
if (!$change->canRevert($member)) {
return false;
}
}
// Default permission
return $this->can(__FUNCTION__, $member);
}
/**
* Default permissions for this changeset
*
* @param string $perm
* @param Member $member
* @param array $context
* @return bool
*/
public function can($perm, $member = null, $context = array())
{
if (!$member) {
$member = Member::currentUser();
}
// Allow extensions to bypass default permissions, but only if
// each change can be individually published.
$extended = $this->extendedCan($perm, $member, $context);
if ($extended !== null) {
return $extended;
}
// Default permissions
return (bool)Permission::checkMember($member, $this->config()->required_permission);
}
public function getCMSFields()
{
$fields = new FieldList(new TabSet('Root'));
if ($this->IsInferred) {
$fields->addFieldToTab('Root.Main', ReadonlyField::create('Name', $this->fieldLabel('Name')));
} else {
$fields->addFieldToTab('Root.Main', TextField::create('Name', $this->fieldLabel('Name')));
}
if ($this->isInDB()) {
$fields->addFieldToTab('Root.Main', ReadonlyField::create('State', $this->fieldLabel('State')));
}
$this->extend('updateCMSFields', $fields);
return $fields;
}
/**
* Gets summary of items in changeset
*
* @return string
*/
public function getDescription()
{
// Initialise list of items to count
$counted = [];
$countedOther = 0;
foreach ($this->config()->important_classes as $type) {
if (class_exists($type)) {
$counted[$type] = 0;
}
}
// Check each change item
/** @var ChangeSetItem $change */
foreach ($this->Changes() as $change) {
$found = false;
foreach ($counted as $class => $num) {
if (is_a($change->ObjectClass, $class, true)) {
$counted[$class]++;
$found = true;
break;
}
}
if (!$found) {
$countedOther++;
}
}
// Describe set based on this output
$counted = array_filter($counted);
// Empty state
if (empty($counted) && empty($countedOther)) {
return '';
}
// Put all parts together
$parts = [];
foreach ($counted as $class => $count) {
$parts[] = DataObject::singleton($class)->i18n_pluralise($count);
}
// Describe non-important items
if ($countedOther) {
if ($counted) {
$parts[] = i18n::_t(
'ChangeSet.DESCRIPTION_OTHER_ITEM_PLURALS',
'one other item|{count} other items',
[ 'count' => $countedOther ]
);
} else {
$parts[] = i18n::_t(
'ChangeSet.DESCRIPTION_ITEM_PLURALS',
'one item|{count} items',
[ 'count' => $countedOther ]
);
}
}
// Figure out how to join everything together
if (empty($parts)) {
return '';
}
if (count($parts) === 1) {
return $parts[0];
}
// Non-comma list
if (count($parts) === 2) {
return _t(
'ChangeSet.DESCRIPTION_AND',
'{first} and {second}',
[
'first' => $parts[0],
'second' => $parts[1],
]
);
}
// First item
$string = _t(
'ChangeSet.DESCRIPTION_LIST_FIRST',
'{item}',
['item' => $parts[0]]
);
// Middle items
for ($i = 1; $i < count($parts) - 1; $i++) {
$string = _t(
'ChangeSet.DESCRIPTION_LIST_MID',
'{list}, {item}',
[
'list' => $string,
'item' => $parts[$i]
]
);
}
// Oxford comma
$string = _t(
'ChangeSet.DESCRIPTION_LIST_LAST',
'{list}, and {item}',
[
'list' => $string,
'item' => end($parts)
]
);
return $string;
}
/**
* Required to support count display in react gridfield column
*
* @return int
*/
public function getChangesCount()
{
return $this->Changes()->count();
}
public function fieldLabels($includerelations = true)
{
$labels = parent::fieldLabels($includerelations);
$labels['Name'] = _t('ChangeSet.NAME', 'Name');
$labels['State'] = _t('ChangeSet.STATE', 'State');
return $labels;
}
}

View File

@ -1,475 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\ORM\CMSPreviewable;
use SilverStripe\Assets\Thumbnail;
use SilverStripe\Control\Controller;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\SS_List;
use SilverStripe\ORM\UnexpectedDataException;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use BadMethodCallException;
use Exception;
/**
* A single line in a changeset
*
* @property string $Added
* @property string $ObjectClass The _base_ data class for the referenced DataObject
* @property int $ObjectID The numeric ID for the referenced object
* @method ManyManyList ReferencedBy() List of explicit items that require this change
* @method ManyManyList References() List of implicit items required by this change
* @method ChangeSet ChangeSet()
*/
class ChangeSetItem extends DataObject implements Thumbnail
{
const EXPLICITLY = 'explicitly';
const IMPLICITLY = 'implicitly';
/** Represents an object deleted */
const CHANGE_DELETED = 'deleted';
/** Represents an object which was modified */
const CHANGE_MODIFIED = 'modified';
/** Represents an object added */
const CHANGE_CREATED = 'created';
/** Represents an object which hasn't been changed directly, but owns a modified many_many relationship. */
//const CHANGE_MANYMANY = 'manymany';
private static $table_name = 'ChangeSetItem';
/**
* Represents that an object has not yet been changed, but
* should be included in this changeset as soon as any changes exist
*/
const CHANGE_NONE = 'none';
private static $db = array(
'VersionBefore' => 'Int',
'VersionAfter' => 'Int',
'Added' => "Enum('explicitly, implicitly', 'implicitly')"
);
private static $has_one = array(
'ChangeSet' => 'SilverStripe\ORM\Versioning\ChangeSet',
'Object' => 'SilverStripe\ORM\DataObject',
);
private static $many_many = array(
'ReferencedBy' => 'SilverStripe\ORM\Versioning\ChangeSetItem'
);
private static $belongs_many_many = array(
'References' => 'ChangeSetItem.ReferencedBy'
);
private static $indexes = array(
'ObjectUniquePerChangeSet' => array(
'type' => 'unique',
'value' => '"ObjectID", "ObjectClass", "ChangeSetID"'
)
);
public function onBeforeWrite()
{
// Make sure ObjectClass refers to the base data class in the case of old or wrong code
$this->ObjectClass = $this->getSchema()->baseDataClass($this->ObjectClass);
parent::onBeforeWrite();
}
public function getTitle()
{
// Get title of modified object
$object = $this->getObjectLatestVersion();
if ($object) {
return $object->getTitle();
}
return $this->i18n_singular_name() . ' #' . $this->ID;
}
/**
* Get a thumbnail for this object
*
* @param int $width Preferred width of the thumbnail
* @param int $height Preferred height of the thumbnail
* @return string URL to the thumbnail, if available
*/
public function ThumbnailURL($width, $height)
{
$object = $this->getObjectLatestVersion();
if ($object instanceof Thumbnail) {
return $object->ThumbnailURL($width, $height);
}
return null;
}
/**
* Get the type of change: none, created, deleted, modified, manymany
* @return string
* @throws UnexpectedDataException
*/
public function getChangeType()
{
if (!class_exists($this->ObjectClass)) {
throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}");
}
// Get change versions
if ($this->VersionBefore || $this->VersionAfter) {
$draftVersion = $this->VersionAfter; // After publishing draft was written to stage
$liveVersion = $this->VersionBefore; // The live version before the publish
} else {
$draftVersion = Versioned::get_versionnumber_by_stage(
$this->ObjectClass,
Versioned::DRAFT,
$this->ObjectID,
false
);
$liveVersion = Versioned::get_versionnumber_by_stage(
$this->ObjectClass,
Versioned::LIVE,
$this->ObjectID,
false
);
}
// Version comparisons
if ($draftVersion == $liveVersion) {
return self::CHANGE_NONE;
} elseif (!$liveVersion) {
return self::CHANGE_CREATED;
} elseif (!$draftVersion) {
return self::CHANGE_DELETED;
} else {
return self::CHANGE_MODIFIED;
}
}
/**
* Find version of this object in the given stage
*
* @param string $stage
* @return DataObject|Versioned
* @throws UnexpectedDataException
*/
protected function getObjectInStage($stage)
{
if (!class_exists($this->ObjectClass)) {
throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}");
}
return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
}
/**
* Find latest version of this object
* @return DataObject|Versioned
* @throws UnexpectedDataException
*/
protected function getObjectLatestVersion()
{
if (!class_exists($this->ObjectClass)) {
throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}");
}
return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
}
/**
* Get all implicit objects for this change
*
* @return SS_List
*/
public function findReferenced()
{
if ($this->getChangeType() === ChangeSetItem::CHANGE_DELETED) {
// If deleted from stage, need to look at live record
$record = $this->getObjectInStage(Versioned::LIVE);
if ($record) {
return $record->findOwners(false);
}
} else {
// If changed on stage, look at owned objects there
$record = $this->getObjectInStage(Versioned::DRAFT);
if ($record) {
return $record->findOwned()->filterByCallback(function ($owned) {
/** @var Versioned|DataObject $owned */
return $owned->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
});
}
}
// Empty set
return new ArrayList();
}
/**
* Publish this item, then close it.
*
* Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive.
*/
public function publish()
{
if (!class_exists($this->ObjectClass)) {
throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}");
}
// Logical checks prior to publish
if (!$this->canPublish()) {
throw new Exception("The current member does not have permission to publish this ChangeSetItem.");
}
if ($this->VersionBefore || $this->VersionAfter) {
throw new BadMethodCallException("This ChangeSetItem has already been published");
}
// Record state changed
$this->VersionAfter = Versioned::get_versionnumber_by_stage(
$this->ObjectClass,
Versioned::DRAFT,
$this->ObjectID,
false
);
$this->VersionBefore = Versioned::get_versionnumber_by_stage(
$this->ObjectClass,
Versioned::LIVE,
$this->ObjectID,
false
);
switch ($this->getChangeType()) {
case static::CHANGE_NONE: {
break;
}
case static::CHANGE_DELETED: {
// Non-recursive delete
$object = $this->getObjectInStage(Versioned::LIVE);
$object->deleteFromStage(Versioned::LIVE);
break;
}
case static::CHANGE_MODIFIED:
case static::CHANGE_CREATED: {
// Non-recursive publish
$object = $this->getObjectInStage(Versioned::DRAFT);
$object->publishSingle();
break;
}
}
$this->write();
}
/**
* Once this item (and all owned objects) are published, unlink
* all disowned objects
*/
public function unlinkDisownedObjects()
{
$object = $this->getObjectInStage(Versioned::DRAFT);
if ($object) {
$object->unlinkDisownedObjects(Versioned::DRAFT, Versioned::LIVE);
}
}
/** Reverts this item, then close it. **/
public function revert()
{
user_error('Not implemented', E_USER_ERROR);
}
public function canView($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canEdit($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canCreate($member = null, $context = array())
{
return $this->can(__FUNCTION__, $member, $context);
}
public function canDelete($member = null)
{
return $this->can(__FUNCTION__, $member);
}
/**
* Check if the BeforeVersion of this changeset can be restored to draft
*
* @param Member $member
* @return bool
*/
public function canRevert($member)
{
// Just get the best version as this object may not even exist on either stage anymore.
/** @var Versioned|DataObject $object */
$object = $this->getObjectLatestVersion();
if (!$object) {
return false;
}
// Check change type
switch ($this->getChangeType()) {
case static::CHANGE_CREATED: {
// Revert creation by deleting from stage
return $object->canDelete($member);
}
default: {
// All other actions are typically editing draft stage
return $object->canEdit($member);
}
}
}
/**
* Check if this ChangeSetItem can be published
*
* @param Member $member
* @return bool
*/
public function canPublish($member = null)
{
// Check canMethod to invoke on object
switch ($this->getChangeType()) {
case static::CHANGE_DELETED: {
/** @var Versioned|DataObject $object */
$object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID);
if ($object) {
return $object->canUnpublish($member);
}
break;
}
default: {
/** @var Versioned|DataObject $object */
$object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID);
if ($object) {
return $object->canPublish($member);
}
break;
}
}
return true;
}
/**
* Determine if this item has changes
*
* @return bool
*/
public function hasChange()
{
return $this->getChangeType() !== ChangeSetItem::CHANGE_NONE;
}
/**
* Default permissions for this ChangeSetItem
*
* @param string $perm
* @param Member $member
* @param array $context
* @return bool
*/
public function can($perm, $member = null, $context = array())
{
if (!$member) {
$member = Member::currentUser();
}
// Allow extensions to bypass default permissions, but only if
// each change can be individually published.
$extended = $this->extendedCan($perm, $member, $context);
if ($extended !== null) {
return $extended;
}
// Default permissions
return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
}
/**
* Get the ChangeSetItems that reference a passed DataObject
*
* @param DataObject $object
* @return DataList
*/
public static function get_for_object($object)
{
return ChangeSetItem::get()->filter([
'ObjectID' => $object->ID,
'ObjectClass' => $object->baseClass(),
]);
}
/**
* Get the ChangeSetItems that reference a passed DataObject
*
* @param int $objectID The ID of the object
* @param string $objectClass The class of the object (or any parent class)
* @return DataList
*/
public static function get_for_object_by_id($objectID, $objectClass)
{
return ChangeSetItem::get()->filter([
'ObjectID' => $objectID,
'ObjectClass' => static::getSchema()->baseDataClass($objectClass)
]);
}
/**
* Gets the list of modes this record can be previewed in.
*
* {@link https://tools.ietf.org/html/draft-kelly-json-hal-07#section-5}
*
* @return array Map of links in acceptable HAL format
*/
public function getPreviewLinks()
{
$links = [];
// Preview draft
$stage = $this->getObjectInStage(Versioned::DRAFT);
if ($stage instanceof CMSPreviewable && $stage->canView() && ($link = $stage->PreviewLink())) {
$links[Versioned::DRAFT] = [
'href' => Controller::join_links($link, '?stage=' . Versioned::DRAFT),
'type' => $stage->getMimeType(),
];
}
// Preview live
$live = $this->getObjectInStage(Versioned::LIVE);
if ($live instanceof CMSPreviewable && $live->canView() && ($link = $live->PreviewLink())) {
$links[Versioned::LIVE] = [
'href' => Controller::join_links($link, '?stage=' . Versioned::LIVE),
'type' => $live->getMimeType(),
];
}
return $links;
}
/**
* Get edit link for this item
*
* @return string
*/
public function CMSEditLink()
{
$link = $this->getObjectInStage(Versioned::DRAFT);
if ($link instanceof CMSPreviewable) {
return $link->CMSEditLink();
}
return null;
}
}

View File

@ -1,273 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Assets\Image;
use SilverStripe\Core\Convert;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\View\ArrayData;
use SilverStripe\View\Parsers\Diff;
use SilverStripe\View\ViewableData;
/**
* Utility class to render views of the differences between two data objects (or two versions of the
* same data object).
*
* Construcing a diff object is done as follows:
* <code>
* $fromRecord = Versioned::get_version('SiteTree', $pageID, $fromVersion);
* $toRecord = Versioned::get_version('SiteTree, $pageID, $toVersion);
* $diff = new DataDifferencer($fromRecord, $toRecord);
* </code>
*
* And then it can be used in a number of ways. You can use the ChangedFields() method in a template:
* <pre>
* <dl class="diff">
* <% with Diff %>
* <% loop ChangedFields %>
* <dt>$Title</dt>
* <dd>$Diff</dd>
* <% end_loop %>
* <% end_with %>
* </dl>
* </pre>
*
* Or you can get the diff'ed content as another DataObject, that you can insert into a form.
* <code>
* $form->loadDataFrom($diff->diffedData());
* </code>
*
* If there are fields whose changes you aren't interested in, you can ignore them like so:
* <code>
* $diff->ignoreFields('AuthorID', 'Status');
* </code>
*/
class DataDifferencer extends ViewableData
{
protected $fromRecord;
protected $toRecord;
protected $ignoredFields = array("ID","Version","RecordID");
/**
* Construct a DataDifferencer to show the changes between $fromRecord and $toRecord.
* If $fromRecord is null, this will represent a "creation".
*
* @param DataObject $fromRecord
* @param DataObject $toRecord
*/
public function __construct(DataObject $fromRecord = null, DataObject $toRecord = null)
{
$this->fromRecord = $fromRecord;
$this->toRecord = $toRecord;
parent::__construct();
}
/**
* Specify some fields to ignore changes from. Repeated calls are cumulative.
* @param array $ignoredFields An array of field names to ignore. Alternatively, pass the field names as
* separate args.
* @return $this
*/
public function ignoreFields($ignoredFields)
{
if (!is_array($ignoredFields)) {
$ignoredFields = func_get_args();
}
$this->ignoredFields = array_merge($this->ignoredFields, $ignoredFields);
return $this;
}
/**
* Get a DataObject with altered values replaced with HTML diff strings, incorporating
* <ins> and <del> tags.
*/
public function diffedData()
{
if ($this->fromRecord) {
$diffed = clone $this->fromRecord;
$fields = array_keys($diffed->toMap() + $this->toRecord->toMap());
} else {
$diffed = clone $this->toRecord;
$fields = array_keys($this->toRecord->toMap());
}
$hasOnes = array_merge($this->fromRecord->hasOne(), $this->toRecord->hasOne());
// Loop through properties
foreach ($fields as $field) {
if (in_array($field, $this->ignoredFields)) {
continue;
}
if (in_array($field, array_keys($hasOnes))) {
continue;
}
// Check if a field from-value is comparable
$toField = $this->toRecord->obj($field);
if (!($toField instanceof DBField)) {
continue;
}
$toValue = $toField->forTemplate();
// Show only to value
if (!$this->fromRecord) {
$diffed->setField($field, "<ins>{$toValue}</ins>");
continue;
}
// Check if a field to-value is comparable
$fromField = $this->fromRecord->obj($field);
if (!($fromField instanceof DBField)) {
continue;
}
$fromValue = $fromField->forTemplate();
// Show changes between the two, if any exist
if ($fromValue != $toValue) {
$diffed->setField($field, Diff::compareHTML($fromValue, $toValue));
}
}
// Loop through has_one
foreach ($hasOnes as $relName => $relSpec) {
if (in_array($relName, $this->ignoredFields)) {
continue;
}
// Create the actual column name
$relField = "{$relName}ID";
$toTitle = '';
if ($this->toRecord->hasMethod($relName)) {
$relObjTo = $this->toRecord->$relName();
if ($relObjTo) {
$toTitle = ($relObjTo->hasMethod('Title') || $relObjTo->hasField('Title')) ? $relObjTo->Title : '';
} else {
$toTitle = '';
}
}
if (!$this->fromRecord) {
if ($relObjTo) {
if ($relObjTo instanceof Image) {
// Using relation name instead of database column name, because of FileField etc.
// TODO Use CMSThumbnail instead to limit max size, blocked by DataDifferencerTest and GC
// not playing nice with mocked images
$diffed->setField($relName, "<ins>" . $relObjTo->getTag() . "</ins>");
} else {
$diffed->setField($relField, "<ins>" . $toTitle . "</ins>");
}
}
} elseif ($this->fromRecord->$relField != $this->toRecord->$relField) {
$fromTitle = '';
if ($this->fromRecord->hasMethod($relName)) {
$relObjFrom = $this->fromRecord->$relName();
if ($relObjFrom) {
$fromTitle = ($relObjFrom->hasMethod('Title') || $relObjFrom->hasField('Title'))
? $relObjFrom->Title
: '';
} else {
$fromTitle = '';
}
}
if (isset($relObjFrom) && $relObjFrom instanceof Image) {
// TODO Use CMSThumbnail (see above)
$diffed->setField(
// Using relation name instead of database column name, because of FileField etc.
$relName,
Diff::compareHTML($relObjFrom->getTag(), $relObjTo->getTag())
);
} else {
// Set the field.
$diffed->setField(
$relField,
Diff::compareHTML($fromTitle, $toTitle)
);
}
}
}
return $diffed;
}
/**
* Get a SS_List of the changed fields.
* Each element is an array data containing
* - Name: The field name
* - Title: The human-readable field title
* - Diff: An HTML diff showing the changes
* - From: The older version of the field
* - To: The newer version of the field
*/
public function ChangedFields()
{
$changedFields = new ArrayList();
if ($this->fromRecord) {
$base = $this->fromRecord;
$fields = array_keys($this->fromRecord->toMap());
} else {
$base = $this->toRecord;
$fields = array_keys($this->toRecord->toMap());
}
foreach ($fields as $field) {
if (in_array($field, $this->ignoredFields)) {
continue;
}
if (!$this->fromRecord || $this->fromRecord->$field != $this->toRecord->$field) {
// Only show HTML diffs for fields which allow HTML values in the first place
$fieldObj = $this->toRecord->dbObject($field);
if ($this->fromRecord) {
$fieldDiff = Diff::compareHTML(
$this->fromRecord->$field,
$this->toRecord->$field,
(!$fieldObj || $fieldObj->stat('escape_type') != 'xml')
);
} else {
if ($fieldObj && $fieldObj->stat('escape_type') == 'xml') {
$fieldDiff = "<ins>" . $this->toRecord->$field . "</ins>";
} else {
$fieldDiff = "<ins>" . Convert::raw2xml($this->toRecord->$field) . "</ins>";
}
}
$changedFields->push(new ArrayData(array(
'Name' => $field,
'Title' => $base->fieldLabel($field),
'Diff' => $fieldDiff,
'From' => $this->fromRecord ? $this->fromRecord->$field : null,
'To' => $this->toRecord ? $this->toRecord->$field : null,
)));
}
}
return $changedFields;
}
/**
* Get an array of the names of every fields that has changed.
* This is simpler than {@link ChangedFields()}
*/
public function changedFieldNames()
{
$diffed = clone $this->fromRecord;
$fields = array_keys($diffed->toMap());
$changedFields = array();
foreach ($fields as $field) {
if (in_array($field, $this->ignoredFields)) {
continue;
}
if ($this->fromRecord->$field != $this->toRecord->$field) {
$changedFields[] = $field;
}
}
return $changedFields;
}
}

View File

@ -1,27 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
/**
* Minimum level extra fields required by extensions that are versonable
*/
interface VersionableExtension
{
/**
* Determine if the given table is versionable
*
* @param string $table
* @return bool True if versioned tables should be built for the given suffix
*/
public function isVersionedTable($table);
/**
* Update fields and indexes for the versonable suffix table
*
* @param string $suffix Table suffix being built
* @param array $fields List of fields in this model
* @param array $indexes List of indexes in this model
*/
public function updateVersionableFields($suffix, &$fields, &$indexes);
}

View File

@ -1,2611 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\Session;
use SilverStripe\Control\Director;
use SilverStripe\Control\Cookie;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Object;
use SilverStripe\Core\Resettable;
use SilverStripe\Dev\Deprecation;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataQuery;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\ArrayList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\ORM\Queries\SQLSelect;
use SilverStripe\ORM\Queries\SQLUpdate;
use SilverStripe\Security\Member;
use SilverStripe\Security\Permission;
use SilverStripe\View\TemplateGlobalProvider;
use InvalidArgumentException;
use LogicException;
/**
* The Versioned extension allows your DataObjects to have several versions,
* allowing you to rollback changes and view history. An example of this is
* the pages used in the CMS.
*
* @property int $Version
* @property DataObject|Versioned $owner
*/
class Versioned extends DataExtension implements TemplateGlobalProvider, Resettable
{
/**
* Versioning mode for this object.
* Note: Not related to the current versioning mode in the state / session
* Will be one of 'StagedVersioned' or 'Versioned';
*
* @var string
*/
protected $mode;
/**
* The default reading mode
*/
const DEFAULT_MODE = 'Stage.Live';
/**
* Constructor arg to specify that staging is active on this record.
* 'Staging' implies that 'Versioning' is also enabled.
*/
const STAGEDVERSIONED = 'StagedVersioned';
/**
* Constructor arg to specify that versioning only is active on this record.
*/
const VERSIONED = 'Versioned';
/**
* The Public stage.
*/
const LIVE = 'Live';
/**
* The draft (default) stage
*/
const DRAFT = 'Stage';
/**
* A version that a DataObject should be when it is 'migrating',
* that is, when it is in the process of moving from one stage to another.
* @var string
*/
public $migratingVersion;
/**
* A cache used by get_versionnumber_by_stage().
* Clear through {@link flushCache()}.
*
* @var array
*/
protected static $cache_versionnumber;
/**
* Current reading mode
*
* @var string
*/
protected static $reading_mode = null;
/**
* @var Boolean Flag which is temporarily changed during the write() process
* to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
* for the following write. Needs to be public as other classes introspect this state
* during the write process in order to adapt to this versioning behaviour.
*/
public $_nextWriteWithoutVersion = false;
/**
* Additional database columns for the new
* "_Versions" table. Used in {@link augmentDatabase()}
* and all Versioned calls extending or creating
* SELECT statements.
*
* @var array $db_for_versions_table
*/
private static $db_for_versions_table = array(
"RecordID" => "Int",
"Version" => "Int",
"WasPublished" => "Boolean",
"AuthorID" => "Int",
"PublisherID" => "Int"
);
/**
* @var array
* @config
*/
private static $db = array(
'Version' => 'Int'
);
/**
* Used to enable or disable the prepopulation of the version number cache.
* Defaults to true.
*
* @config
* @var boolean
*/
private static $prepopulate_versionnumber_cache = true;
/**
* Additional database indexes for the new
* "_Versions" table. Used in {@link augmentDatabase()}.
*
* @var array $indexes_for_versions_table
*/
private static $indexes_for_versions_table = array(
'RecordID_Version' => '("RecordID","Version")',
'RecordID' => true,
'Version' => true,
'AuthorID' => true,
'PublisherID' => true,
);
/**
* An array of DataObject extensions that may require versioning for extra tables
* The array value is a set of suffixes to form these table names, assuming a preceding '_'.
* E.g. if Extension1 creates a new table 'Class_suffix1'
* and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
*
* $versionableExtensions = array(
* 'Extension1' => 'suffix1',
* 'Extension2' => array('suffix2', 'suffix3'),
* );
*
* This can also be manipulated by updating the current loaded config
*
* SiteTree:
* versionableExtensions:
* - Extension1:
* - suffix1
* - suffix2
* - Extension2:
* - suffix1
* - suffix2
*
* or programatically:
*
* Config::inst()->update($this->owner->class, 'versionableExtensions',
* array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
*
*
* Your extension must implement VersionableExtension interface in order to
* apply custom tables for versioned.
*
* @config
* @var array
*/
private static $versionableExtensions = [];
/**
* Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
*
* @config
* @var array
*/
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.
*/
public static function reset()
{
self::$reading_mode = '';
Session::clear('readingMode');
}
/**
* Amend freshly created DataQuery objects with versioned-specific
* information.
*
* @param SQLSelect $query
* @param DataQuery $dataQuery
*/
public function augmentDataQueryCreation(SQLSelect &$query, DataQuery &$dataQuery)
{
$parts = explode('.', Versioned::get_reading_mode());
if ($parts[0] == 'Archive') {
$dataQuery->setQueryParam('Versioned.mode', 'archive');
$dataQuery->setQueryParam('Versioned.date', $parts[1]);
} elseif ($parts[0] == 'Stage' && $this->hasStages()) {
$dataQuery->setQueryParam('Versioned.mode', 'stage');
$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
}
}
/**
* Construct a new Versioned object.
*
* @var string $mode One of "StagedVersioned" or "Versioned".
*/
public function __construct($mode = self::STAGEDVERSIONED)
{
parent::__construct();
// Handle deprecated behaviour
if ($mode === 'Stage' && func_num_args() === 1) {
Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
$mode = static::VERSIONED;
} elseif (is_array($mode) || func_num_args() > 1) {
Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
$mode = func_num_args() > 1 || count($mode) > 1
? static::STAGEDVERSIONED
: static::VERSIONED;
}
if (!in_array($mode, array(static::STAGEDVERSIONED, static::VERSIONED))) {
throw new InvalidArgumentException("Invalid mode: {$mode}");
}
$this->mode = $mode;
}
/**
* Cache of version to modified dates for this objects
*
* @var array
*/
protected $versionModifiedCache = array();
/**
* Get modified date for the given version
*
* @param int $version
* @return string
*/
protected function getLastEditedForVersion($version)
{
// Cache key
$baseTable = $this->baseTable();
$id = $this->owner->ID;
$key = "{$baseTable}#{$id}/{$version}";
// Check cache
if (isset($this->versionModifiedCache[$key])) {
return $this->versionModifiedCache[$key];
}
// Build query
$table = "\"{$baseTable}_Versions\"";
$query = SQLSelect::create('"LastEdited"', $table)
->addWhere([
"{$table}.\"RecordID\"" => $id,
"{$table}.\"Version\"" => $version
]);
$date = $query->execute()->value();
if ($date) {
$this->versionModifiedCache[$key] = $date;
}
return $date;
}
/**
* Updates query parameters of relations attached to versioned dataobjects
*
* @param array $params
*/
public function updateInheritableQueryParams(&$params)
{
// Skip if versioned isn't set
if (!isset($params['Versioned.mode'])) {
return;
}
// Adjust query based on original selection criterea
switch ($params['Versioned.mode']) {
case 'all_versions': {
// Versioned.mode === all_versions doesn't inherit very well, so default to stage
$params['Versioned.mode'] = 'stage';
$params['Versioned.stage'] = static::DRAFT;
break;
}
case 'version': {
// If we selected this object from a specific version, we need
// to find the date this version was published, and ensure
// inherited queries select from that date.
$version = $params['Versioned.version'];
$date = $this->getLastEditedForVersion($version);
// Filter related objects at the same date as this version
unset($params['Versioned.version']);
if ($date) {
$params['Versioned.mode'] = 'archive';
$params['Versioned.date'] = $date;
} else {
// Fallback to default
$params['Versioned.mode'] = 'stage';
$params['Versioned.stage'] = static::DRAFT;
}
break;
}
}
}
/**
* Augment the the SQLSelect that is created by the DataQuery
*
* See {@see augmentLazyLoadFields} for lazy-loading applied prior to this.
*
* @param SQLSelect $query
* @param DataQuery $dataQuery
* @throws InvalidArgumentException
*/
public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null)
{
if (!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
return;
}
$baseTable = $this->baseTable();
$versionedMode = $dataQuery->getQueryParam('Versioned.mode');
switch ($versionedMode) {
// Reading a specific stage (Stage or Live)
case 'stage':
// Check if we need to rewrite this table
$stage = $dataQuery->getQueryParam('Versioned.stage');
if (!$this->hasStages() || $stage === static::DRAFT) {
break;
}
// Rewrite all tables to select from the live version
foreach ($query->getFrom() as $table => $dummy) {
if (!$this->isTableVersioned($table)) {
continue;
}
$stageTable = $this->stageTable($table, $stage);
$query->renameTable($table, $stageTable);
}
break;
// Reading a specific stage, but only return items that aren't in any other stage
case 'stage_unique':
if (!$this->hasStages()) {
break;
}
$stage = $dataQuery->getQueryParam('Versioned.stage');
// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
// below)
$dataQuery->setQueryParam('Versioned.mode', 'stage');
$this->augmentSQL($query, $dataQuery);
$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
// renaming all subquery references to be Versioned.stage
foreach ([static::DRAFT, static::LIVE] as $excluding) {
if ($excluding == $stage) {
continue;
}
$tempName = 'ExclusionarySource_'.$excluding;
$excludingTable = $this->baseTable($excluding);
$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
$query->renameTable($tempName, $excludingTable);
}
break;
// Return all version instances
case 'archive':
case 'all_versions':
case 'latest_versions':
case 'version':
foreach ($query->getFrom() as $alias => $join) {
if (!$this->isTableVersioned($alias)) {
continue;
}
if ($alias != $baseTable) {
// 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');
}
// Add all <basetable>_Versions columns
foreach (Config::inst()->get(static::class, 'db_for_versions_table') as $name => $type) {
$query->selectField(sprintf('"%s_Versions"."%s"', $baseTable, $name), $name);
}
// 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\"");
// However, if doing count, undo rewrite of "ID" column
$query->replaceText(
"count(DISTINCT \"{$baseTable}_Versions\".\"RecordID\")",
"count(DISTINCT \"{$baseTable}_Versions\".\"ID\")"
);
// Add additional versioning filters
switch ($versionedMode) {
case 'archive': {
$date = $dataQuery->getQueryParam('Versioned.date');
if (!$date) {
throw new InvalidArgumentException("Invalid archive date");
}
// Link to the version archived on that date
$query->addWhere([
"\"{$baseTable}_Versions\".\"Version\" IN
(SELECT LatestVersion FROM
(SELECT
\"{$baseTable}_Versions\".\"RecordID\",
MAX(\"{$baseTable}_Versions\".\"Version\") AS LatestVersion
FROM \"{$baseTable}_Versions\"
WHERE \"{$baseTable}_Versions\".\"LastEdited\" <= ?
GROUP BY \"{$baseTable}_Versions\".\"RecordID\"
) AS \"{$baseTable}_Versions_Latest\"
WHERE \"{$baseTable}_Versions_Latest\".\"RecordID\" = \"{$baseTable}_Versions\".\"RecordID\"
)" => $date
]);
break;
}
case 'latest_versions': {
// Return latest version instances, regardless of whether they are on a particular stage
// This provides "show all, including deleted" functonality
$query->addWhere(
"\"{$baseTable}_Versions\".\"Version\" IN
(SELECT LatestVersion FROM
(SELECT
\"{$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\"
)"
);
break;
}
case 'version': {
// If selecting a specific version, filter it here
$version = $dataQuery->getQueryParam('Versioned.version');
if (!$version) {
throw new InvalidArgumentException("Invalid version");
}
$query->addWhere([
"\"{$baseTable}_Versions\".\"Version\"" => $version
]);
break;
}
case 'all_versions':
default: {
// If all versions are requested, ensure that records are sorted by this field
$query->addOrderBy(sprintf('"%s_Versions"."%s"', $baseTable, 'Version'));
break;
}
}
break;
default:
throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
. $dataQuery->getQueryParam('Versioned.mode'));
}
}
/**
* Determine if the given versioned table is a part of the sub-tree of the current dataobject
* This helps prevent rewriting of other tables that get joined in, in particular, many_many tables
*
* @param string $table
* @return bool True if this table should be versioned
*/
protected function isTableVersioned($table)
{
$schema = DataObject::getSchema();
$tableClass = $schema->tableClass($table);
if (empty($tableClass)) {
return false;
}
// Check that this class belongs to the same tree
$baseClass = $schema->baseDataClass($this->owner);
if (!is_a($tableClass, $baseClass, true)) {
return false;
}
// Check that this isn't a derived table
// (e.g. _Live, or a many_many table)
$mainTable = $schema->tableName($tableClass);
if ($mainTable !== $table) {
return false;
}
return true;
}
/**
* For lazy loaded fields requiring extra sql manipulation, ie versioning.
*
* @param SQLSelect $query
* @param DataQuery $dataQuery
* @param DataObject $dataObject
*/
public function augmentLoadLazyFields(SQLSelect &$query, DataQuery &$dataQuery = null, $dataObject)
{
// The VersionedMode local variable ensures that this decorator only applies to
// queries that have originated from the Versioned object, and have the Versioned
// metadata set on the query object. This prevents regular queries from
// accidentally querying the *_Versions tables.
$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
if (!empty($dataObject->Version) &&
(!empty($versionedMode) && in_array($versionedMode, $modesToAllowVersioning))
) {
// This will ensure that augmentSQL will select only the same version as the owner,
// regardless of how this object was initially selected
$versionColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'Version');
$dataQuery->where([
$versionColumn => $dataObject->Version
]);
$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
}
}
public function augmentDatabase()
{
$owner = $this->owner;
$class = get_class($owner);
$schema = $owner->getSchema();
$baseTable = $this->baseTable();
$classTable = $schema->tableName($owner);
$isRootClass = $class === $owner->baseClass();
// Build a list of suffixes whose tables need versioning
$allSuffixes = array();
$versionableExtensions = $owner->config()->get('versionableExtensions');
if (count($versionableExtensions)) {
foreach ($versionableExtensions as $versionableExtension => $suffixes) {
if ($owner->hasExtension($versionableExtension)) {
foreach ((array)$suffixes as $suffix) {
$allSuffixes[$suffix] = $versionableExtension;
}
}
}
}
// Add the default table with an empty suffix to the list (table name = class name)
$allSuffixes[''] = null;
foreach ($allSuffixes as $suffix => $extension) {
// Check tables for this build
if ($suffix) {
$suffixBaseTable = "{$baseTable}_{$suffix}";
$suffixTable = "{$classTable}_{$suffix}";
} else {
$suffixBaseTable = $baseTable;
$suffixTable = $classTable;
}
$fields = $schema->databaseFields($class, false);
unset($fields['ID']);
if ($fields) {
$options = Config::inst()->get($class, 'create_table_options');
$indexes = $owner->databaseIndexes();
$extensionClass = $allSuffixes[$suffix];
if ($suffix && ($extension = $owner->getExtensionInstance($extensionClass))) {
if (!$extension instanceof VersionableExtension) {
throw new LogicException(
"Extension {$extensionClass} must implement VersionableExtension"
);
}
// Allow versionable extension to customise table fields and indexes
$extension->setOwner($owner);
if ($extension->isVersionedTable($suffixTable)) {
$extension->updateVersionableFields($suffix, $fields, $indexes);
}
$extension->clearOwner();
}
// Build _Live table
if ($this->hasStages()) {
$liveTable = $this->stageTable($suffixTable, static::LIVE);
DB::require_table($liveTable, $fields, $indexes, false, $options);
}
// Build _Versions table
//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
$nonUniqueIndexes = $this->uniqueToIndex($indexes);
if ($isRootClass) {
// Create table for all versions
$versionFields = array_merge(
Config::inst()->get(static::class, 'db_for_versions_table'),
(array)$fields
);
$versionIndexes = array_merge(
Config::inst()->get(static::class, 'indexes_for_versions_table'),
(array)$nonUniqueIndexes
);
} else {
// Create fields for any tables of subclasses
$versionFields = array_merge(
array(
"RecordID" => "Int",
"Version" => "Int",
),
(array)$fields
);
$versionIndexes = array_merge(
array(
'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
'RecordID' => true,
'Version' => true,
),
(array)$nonUniqueIndexes
);
}
// Cleanup any orphans
$this->cleanupVersionedOrphans("{$suffixBaseTable}_Versions", "{$suffixTable}_Versions");
// Build versions table
DB::require_table("{$suffixTable}_Versions", $versionFields, $versionIndexes, true, $options);
} else {
DB::dont_require_table("{$suffixTable}_Versions");
if ($this->hasStages()) {
$liveTable = $this->stageTable($suffixTable, static::LIVE);
DB::dont_require_table($liveTable);
}
}
}
}
/**
* Cleanup orphaned records in the _Versions table
*
* @param string $baseTable base table to use as authoritative source of records
* @param string $childTable Sub-table to clean orphans from
*/
protected function cleanupVersionedOrphans($baseTable, $childTable)
{
// Skip if child table doesn't exist
if (!DB::get_schema()->hasTable($childTable)) {
return;
}
// Skip if tables are the same
if ($childTable === $baseTable) {
return;
}
// Select all orphaned version records
$orphanedQuery = SQLSelect::create()
->selectField("\"{$childTable}\".\"ID\"")
->setFrom("\"{$childTable}\"");
// If we have a parent table limit orphaned records
// to only those that exist in this
if (DB::get_schema()->hasTable($baseTable)) {
$orphanedQuery
->addLeftJoin(
$baseTable,
"\"{$childTable}\".\"RecordID\" = \"{$baseTable}\".\"RecordID\"
AND \"{$childTable}\".\"Version\" = \"{$baseTable}\".\"Version\""
)
->addWhere("\"{$baseTable}\".\"ID\" IS NULL");
}
$count = $orphanedQuery->count();
if ($count > 0) {
DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
$ids = $orphanedQuery->execute()->column();
foreach ($ids as $id) {
DB::prepared_query("DELETE FROM \"{$childTable}\" WHERE \"ID\" = ?", array($id));
}
}
}
/**
* Helper for augmentDatabase() to find unique indexes and convert them to non-unique
*
* @param array $indexes The indexes to convert
* @return array $indexes
*/
private function uniqueToIndex($indexes)
{
$unique_regex = '/unique/i';
$results = array();
foreach ($indexes as $key => $index) {
$results[$key] = $index;
// support string descriptors
if (is_string($index)) {
if (preg_match($unique_regex, $index)) {
$results[$key] = preg_replace($unique_regex, 'index', $index);
}
} // canonical, array-based descriptors
elseif (is_array($index)) {
if (strtolower($index['type']) == 'unique') {
$results[$key]['type'] = 'index';
}
}
}
return $results;
}
/**
* Generates a ($table)_version DB manipulation and injects it into the current $manipulation
*
* @param array $manipulation Source manipulation data
* @param string $class Class
* @param string $table Table Table for this class
* @param int $recordID ID of record to version
*/
protected function augmentWriteVersioned(&$manipulation, $class, $table, $recordID)
{
$schema = DataObject::getSchema();
$baseDataClass = $schema->baseDataClass($class);
$baseDataTable = $schema->tableName($baseDataClass);
// Set up a new entry in (table)_Versions
$newManipulation = array(
"command" => "insert",
"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : [],
"class" => $class,
);
// Add any extra, unchanged fields to the version record.
$data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record();
if ($data) {
$fields = $schema->databaseFields($class, false);
if (is_array($fields)) {
$data = array_intersect_key($data, $fields);
foreach ($data as $k => $v) {
// If the value is not set at all in the manipulation currently, use the existing value from the database
if (!array_key_exists($k, $newManipulation['fields'])) {
$newManipulation['fields'][$k] = $v;
}
}
}
}
// Ensure that the ID is instead written to the RecordID field
$newManipulation['fields']['RecordID'] = $recordID;
unset($newManipulation['fields']['ID']);
// Generate next version ID to use
$nextVersion = 0;
if ($recordID) {
$nextVersion = DB::prepared_query(
"SELECT MAX(\"Version\") + 1
FROM \"{$baseDataTable}_Versions\" WHERE \"RecordID\" = ?",
array($recordID)
)->value();
}
$nextVersion = $nextVersion ?: 1;
if ($class === $baseDataClass) {
// Write AuthorID for baseclass
$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
$newManipulation['fields']['AuthorID'] = $userID;
// Update main table version if not previously known
$manipulation[$table]['fields']['Version'] = $nextVersion;
}
// Update _Versions table manipulation
$newManipulation['fields']['Version'] = $nextVersion;
$manipulation["{$table}_Versions"] = $newManipulation;
}
/**
* Rewrite the given manipulation to update the selected (non-default) stage
*
* @param array $manipulation Source manipulation data
* @param string $table Name of table
* @param int $recordID ID of record to version
*/
protected function augmentWriteStaged(&$manipulation, $table, $recordID)
{
// If the record has already been inserted in the (table), get rid of it.
if ($manipulation[$table]['command'] == 'insert') {
DB::prepared_query(
"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
array($recordID)
);
}
$newTable = $this->stageTable($table, Versioned::get_stage());
$manipulation[$newTable] = $manipulation[$table];
}
public function augmentWrite(&$manipulation)
{
// get Version number from base data table on write
$version = null;
$owner = $this->owner;
$baseDataTable = DataObject::getSchema()->baseDataTable($owner);
if (isset($manipulation[$baseDataTable]['fields'])) {
if ($this->migratingVersion) {
$manipulation[$baseDataTable]['fields']['Version'] = $this->migratingVersion;
}
if (isset($manipulation[$baseDataTable]['fields']['Version'])) {
$version = $manipulation[$baseDataTable]['fields']['Version'];
}
}
// Update all tables
$tables = array_keys($manipulation);
foreach ($tables as $table) {
// Make sure that the augmented write is being applied to a table that can be versioned
$class = isset($manipulation[$table]['class']) ? $manipulation[$table]['class'] : null;
if (!$class || !$this->canBeVersioned($class)) {
unset($manipulation[$table]);
continue;
}
// Get ID field
$id = $manipulation[$table]['id']
? $manipulation[$table]['id']
: $manipulation[$table]['fields']['ID'];
if (!$id) {
user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
}
if ($version < 0 || $this->_nextWriteWithoutVersion) {
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
unset($manipulation[$table]['fields']['Version']);
} elseif (empty($version)) {
// If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table
$this->augmentWriteVersioned($manipulation, $class, $table, $id);
}
// Remove "Version" column from subclasses of baseDataClass
if (!$this->hasVersionField($table)) {
unset($manipulation[$table]['fields']['Version']);
}
// Grab a version number - it should be the same across all tables.
if (isset($manipulation[$table]['fields']['Version'])) {
$thisVersion = $manipulation[$table]['fields']['Version'];
}
// If we're editing Live, then write to (table)_Live as well as (table)
if ($this->hasStages() && static::get_stage() === static::LIVE) {
$this->augmentWriteStaged($manipulation, $table, $id);
}
}
// Clear the migration flag
if ($this->migratingVersion) {
$this->migrateVersion(null);
}
// Add the new version # back into the data object, for accessing
// after this write
if (isset($thisVersion)) {
$owner->Version = str_replace("'", "", $thisVersion);
}
}
/**
* Perform a write without affecting the version table.
* On objects without versioning.
*
* @return int The ID of the record
*/
public function writeWithoutVersion()
{
$this->_nextWriteWithoutVersion = true;
return $this->owner->write();
}
/**
*
*/
public function onAfterWrite()
{
$this->_nextWriteWithoutVersion = false;
}
/**
* If a write was skipped, then we need to ensure that we don't leave a
* migrateVersion() value lying around for the next write.
*/
public function onAfterSkippedWrite()
{
$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)
{
if (!$list) {
$list = new ArrayList();
}
// Build reverse lookup for ownership
// @todo - Cache this more intelligently
$rules = $this->lookupReverseOwners();
// Hand off to recursive method
return $this->findOwnersRecursive($recursive, $list, $rules);
}
/**
* Find objects which own this object.
* Note that objects will only be searched in the same stage as the given record.
*
* @param bool $recursive True if recursive
* @param ArrayList $list List to add items to
* @param array $lookup List of reverse lookup rules for owned objects
* @return ArrayList list of objects
*/
public function findOwnersRecursive($recursive, $list, $lookup)
{
// First pass: find objects that are explicitly owned_by (e.g. custom relationships)
$owners = $this->findRelatedObjects('owned_by', false);
// Second pass: Find owners via reverse lookup list
foreach ($lookup as $ownedClass => $classLookups) {
// Skip owners of other objects
if (!is_a($this->owner, $ownedClass)) {
continue;
}
foreach ($classLookups as $classLookup) {
// Merge new owners into this object's owners
$ownerClass = $classLookup['class'];
$ownerRelation = $classLookup['relation'];
$result = $this->owner->inferReciprocalComponent($ownerClass, $ownerRelation);
$this->mergeRelatedObjects($owners, $result);
}
}
// Merge all objects into the main list
$newItems = $this->mergeRelatedObjects($list, $owners);
// If recursing, iterate over all newly added items
if ($recursive) {
foreach ($newItems as $item) {
/** @var Versioned|DataObject $item */
$item->findOwnersRecursive(true, $list, $lookup);
}
}
return $list;
}
/**
* Find a list of classes, each of which with a list of methods to invoke
* to lookup owners.
*
* @return array
*/
protected function lookupReverseOwners()
{
// Find all classes with 'owns' config
$lookup = array();
foreach (ClassInfo::subclassesFor('SilverStripe\ORM\DataObject') as $class) {
// Ensure this class is versioned
if (!Object::has_extension($class, static::class)) {
continue;
}
// Check owned objects for this class
$owns = Config::inst()->get($class, 'owns', Config::UNINHERITED);
if (empty($owns)) {
continue;
}
$instance = DataObject::singleton($class);
foreach ($owns as $owned) {
// Find owned class
$ownedClass = $instance->getRelationClass($owned);
// Skip custom methods that don't have db relationsm
if (!$ownedClass) {
continue;
}
if ($ownedClass === DataObject::class) {
throw new LogicException(sprintf(
"Relation %s on class %s cannot be owned as it is polymorphic",
$owned,
$class
));
}
// Add lookup for owned class
if (!isset($lookup[$ownedClass])) {
$lookup[$ownedClass] = array();
}
$lookup[$ownedClass][] = [
'class' => $class,
'relation' => $owned
];
}
}
return $lookup;
}
/**
* Find objects in the given relationships, merging them into the given list
*
* @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
$owner = $this->owner;
if (!$owner->isInDB()) {
return $list;
}
$relationships = $owner->config()->get($source);
foreach ($relationships as $relationship) {
// Warn if invalid config
if (!$owner->hasMethod($relationship)) {
trigger_error(sprintf(
"Invalid %s config value \"%s\" on object on class \"%s\"",
$source,
$relationship,
$owner->class
), E_USER_WARNING);
continue;
}
// Inspect value of this relationship
$items = $owner->{$relationship}();
// Merge any new item
$newItems = $this->mergeRelatedObjects($list, $items);
// Recurse if necessary
if ($recursive) {
foreach ($newItems as $item) {
/** @var Versioned|DataObject $item */
$item->findRelatedObjects($source, true, $list);
}
}
}
return $list;
}
/**
* Helper method to merge owned/owning items into a list.
* Items already present in the list will be skipped.
*
* @param ArrayList $list Items to merge into
* @param mixed $items List of new items to merge
* @return ArrayList List of all newly added items that did not already exist in $list
*/
protected function mergeRelatedObjects($list, $items)
{
$added = new ArrayList();
if (!$items) {
return $added;
}
if ($items instanceof DataObject) {
$items = array($items);
}
/** @var Versioned|DataObject $item */
foreach ($items as $item) {
$this->mergeRelatedObject($list, $added, $item);
}
return $added;
}
/**
* 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.
*
* Denies permission if any of the following conditions is true:
* - canPublish() on any extension returns false
* - canEdit() returns false
*
* @param Member $member
* @return bool True if the current user can publish this record.
*/
public function canPublish($member = null)
{
// Skip if invoked by extendedCan()
if (func_num_args() > 4) {
return null;
}
if (!$member) {
$member = Member::currentUser();
}
if (Permission::checkMember($member, "ADMIN")) {
return true;
}
// Standard mechanism for accepting permission changes from extensions
$owner = $this->owner;
$extended = $owner->extendedCan('canPublish', $member);
if ($extended !== null) {
return $extended;
}
// Default to relying on edit permission
return $owner->canEdit($member);
}
/**
* Check if the current user can delete this record from live
*
* @param null $member
* @return mixed
*/
public function canUnpublish($member = null)
{
// Skip if invoked by extendedCan()
if (func_num_args() > 4) {
return null;
}
if (!$member) {
$member = Member::currentUser();
}
if (Permission::checkMember($member, "ADMIN")) {
return true;
}
// Standard mechanism for accepting permission changes from extensions
$owner = $this->owner;
$extended = $owner->extendedCan('canUnpublish', $member);
if ($extended !== null) {
return $extended;
}
// Default to relying on canPublish
return $owner->canPublish($member);
}
/**
* Check if the current user is allowed to archive this record.
* If extended, ensure that both canDelete and canUnpublish are extended also
*
* @param Member $member
* @return bool
*/
public function canArchive($member = null)
{
// Skip if invoked by extendedCan()
if (func_num_args() > 4) {
return null;
}
if (!$member) {
$member = Member::currentUser();
}
// Standard mechanism for accepting permission changes from extensions
$owner = $this->owner;
$extended = $owner->extendedCan('canArchive', $member);
if ($extended !== null) {
return $extended;
}
// Admin permissions allow
if (Permission::checkMember($member, "ADMIN")) {
return true;
}
// Check if this record can be deleted from stage
if (!$owner->canDelete($member)) {
return false;
}
// Check if we can delete from live
if (!$owner->canUnpublish($member)) {
return false;
}
return true;
}
/**
* Check if the user can revert this record to live
*
* @param Member $member
* @return bool
*/
public function canRevertToLive($member = null)
{
$owner = $this->owner;
// Skip if invoked by extendedCan()
if (func_num_args() > 4) {
return null;
}
// Can't revert if not on live
if (!$owner->isPublished()) {
return false;
}
if (!$member) {
$member = Member::currentUser();
}
if (Permission::checkMember($member, "ADMIN")) {
return true;
}
// Standard mechanism for accepting permission changes from extensions
$extended = $owner->extendedCan('canRevertToLive', $member);
if ($extended !== null) {
return $extended;
}
// Default to canEdit
return $owner->canEdit($member);
}
/**
* Extend permissions to include additional security for objects that are not published to live.
*
* @param Member $member
* @return bool|null
*/
public function canView($member = null)
{
// Invoke default version-gnostic canView
if ($this->owner->canViewVersioned($member) === false) {
return false;
}
return null;
}
/**
* Determine if there are any additional restrictions on this object for the given reading version.
*
* Override this in a subclass to customise any additional effect that Versioned applies to canView.
*
* This is expected to be called by canView, and thus is only responsible for denying access if
* the default canView would otherwise ALLOW access. Thus it should not be called in isolation
* as an authoritative permission check.
*
* This has the following extension points:
* - canViewDraft is invoked if Mode = stage and Stage = stage
* - canViewArchived is invoked if Mode = archive
*
* @param Member $member
* @return bool False is returned if the current viewing mode denies visibility
*/
public function canViewVersioned($member = null)
{
// Bypass when live stage
$owner = $this->owner;
$mode = $owner->getSourceQueryParam("Versioned.mode");
$stage = $owner->getSourceQueryParam("Versioned.stage");
if ($mode === 'stage' && $stage === static::LIVE) {
return true;
}
// Bypass if site is unsecured
if (Session::get('unsecuredDraftSite')) {
return true;
}
// Bypass if record doesn't have a live stage
if (!$this->hasStages()) {
return true;
}
// If we weren't definitely loaded from live, and we can't view non-live content, we need to
// check to make sure this version is the live version and so can be viewed
$latestVersion = Versioned::get_versionnumber_by_stage($owner->class, static::LIVE, $owner->ID);
if ($latestVersion == $owner->Version) {
// Even if this is loaded from a non-live stage, this is the live version
return true;
}
// Extend versioned behaviour
$extended = $owner->extendedCan('canViewNonLive', $member);
if ($extended !== null) {
return (bool)$extended;
}
// Fall back to default permission check
$permissions = Config::inst()->get($owner->class, 'non_live_permissions');
$check = Permission::checkMember($member, $permissions);
return (bool)$check;
}
/**
* Determines canView permissions for the latest version of this object on a specific stage.
* Usually the stage is read from {@link Versioned::current_stage()}.
*
* This method should be invoked by user code to check if a record is visible in the given stage.
*
* This method should not be called via ->extend('canViewStage'), but rather should be
* overridden in the extended class.
*
* @param string $stage
* @param Member $member
* @return bool
*/
public function canViewStage($stage = 'Live', $member = null)
{
$oldMode = Versioned::get_reading_mode();
Versioned::set_stage($stage);
$owner = $this->owner;
$versionFromStage = DataObject::get($owner->class)->byID($owner->ID);
Versioned::set_reading_mode($oldMode);
return $versionFromStage ? $versionFromStage->canView($member) : false;
}
/**
* Determine if a class is supporting the Versioned extensions (e.g.
* $table_Versions does exists).
*
* @param string $class Class name
* @return boolean
*/
public function canBeVersioned($class)
{
return ClassInfo::exists($class)
&& is_subclass_of($class, DataObject::class)
&& DataObject::getSchema()->classHasTable($class);
}
/**
* Check if a certain table has the 'Version' field.
*
* @param string $table Table name
*
* @return boolean Returns false if the field isn't in the table, true otherwise
*/
public function hasVersionField($table)
{
// Base table has version field
$class = DataObject::getSchema()->tableClass($table);
return $class === DataObject::getSchema()->baseDataClass($class);
}
/**
* @param string $table
*
* @return string
*/
public function extendWithSuffix($table)
{
$owner = $this->owner;
$versionableExtensions = $owner->config()->get('versionableExtensions');
if (count($versionableExtensions)) {
foreach ($versionableExtensions as $versionableExtension => $suffixes) {
if ($owner->hasExtension($versionableExtension)) {
$ext = $owner->getExtensionInstance($versionableExtension);
$ext->setOwner($owner);
$table = $ext->extendWithSuffix($table);
$ext->clearOwner();
}
}
}
return $table;
}
/**
* Determines if the current draft version is the same as live or rather, that there are no outstanding draft changes
*
* @return bool
*/
public function latestPublished()
{
// Get the root data object class - this will have the version field
$owner = $this->owner;
$draftTable = $this->baseTable();
$liveTable = $this->stageTable($draftTable, static::LIVE);
return DB::prepared_query(
"SELECT \"$draftTable\".\"Version\" = \"$liveTable\".\"Version\" FROM \"$draftTable\"
INNER JOIN \"$liveTable\" ON \"$draftTable\".\"ID\" = \"$liveTable\".\"ID\"
WHERE \"$draftTable\".\"ID\" = ?",
array($owner->ID)
)->value();
}
/**
* @deprecated 4.0..5.0
*/
public function doPublish()
{
Deprecation::notice('5.0', 'Use publishRecursive instead');
return $this->owner->publishRecursive();
}
/**
* Publish this object and all owned objects to Live
*
* @return bool
*/
public function publishRecursive()
{
// Create a new changeset for this item and publish it
$changeset = ChangeSet::create();
$changeset->IsInferred = true;
$changeset->Name = _t(
'Versioned.INFERRED_TITLE',
"Generated by publish of '{title}' at {created}",
[
'title' => $this->owner->Title,
'created' => DBDatetime::now()->Nice()
]
);
$changeset->write();
$changeset->addObject($this->owner);
return $changeset->publish();
}
/**
* Publishes this object to Live, but doesn't publish owned objects.
*
* @return bool True if publish was successful
*/
public function publishSingle()
{
$owner = $this->owner;
if (!$owner->canPublish()) {
return false;
}
$owner->invokeWithExtensions('onBeforePublish');
$owner->write();
$owner->copyVersionToStage(static::DRAFT, static::LIVE);
$owner->invokeWithExtensions('onAfterPublish');
return true;
}
/**
* Set foreign keys of has_many objects to 0 where those objects were
* disowned as a result of a partial publish / unpublish.
* I.e. this object and its owned objects were recently written to $targetStage,
* but deleted objects were not.
*
* Note that this operation does not create any new Versions
*
* @param string $sourceStage Objects in this stage will not be unlinked.
* @param string $targetStage Objects which exist in this stage but not $sourceStage
* will be unlinked.
*/
public function unlinkDisownedObjects($sourceStage, $targetStage)
{
$owner = $this->owner;
// after publishing, objects which used to be owned need to be
// dis-connected from this object (set ForeignKeyID = 0)
$owns = $owner->config()->get('owns');
$hasMany = $owner->config()->get('has_many');
if (empty($owns) || empty($hasMany)) {
return;
}
$schema = DataObject::getSchema();
$ownedHasMany = array_intersect($owns, array_keys($hasMany));
foreach ($ownedHasMany as $relationship) {
// Find metadata on relationship
$joinClass = $schema->hasManyComponent(get_class($owner), $relationship);
$joinField = $schema->getRemoteJoinField(get_class($owner), $relationship, 'has_many', $polymorphic);
$idField = $polymorphic ? "{$joinField}ID" : $joinField;
$joinTable = DataObject::getSchema()->tableForField($joinClass, $idField);
// Generate update query which will unlink disowned objects
$targetTable = $this->stageTable($joinTable, $targetStage);
$disowned = new SQLUpdate("\"{$targetTable}\"");
$disowned->assign("\"{$idField}\"", 0);
$disowned->addWhere(array(
"\"{$targetTable}\".\"{$idField}\"" => $owner->ID
));
// Build exclusion list (items to owned objects we need to keep)
$sourceTable = $this->stageTable($joinTable, $sourceStage);
$owned = new SQLSelect("\"{$sourceTable}\".\"ID\"", "\"{$sourceTable}\"");
$owned->addWhere(array(
"\"{$sourceTable}\".\"{$idField}\"" => $owner->ID
));
// Apply class condition if querying on polymorphic has_one
if ($polymorphic) {
$disowned->assign("\"{$joinField}Class\"", null);
$disowned->addWhere(array(
"\"{$targetTable}\".\"{$joinField}Class\"" => get_class($owner)
));
$owned->addWhere(array(
"\"{$sourceTable}\".\"{$joinField}Class\"" => get_class($owner)
));
}
// Merge queries and perform unlink
$ownedSQL = $owned->sql($ownedParams);
$disowned->addWhere(array(
"\"{$targetTable}\".\"ID\" NOT IN ({$ownedSQL})" => $ownedParams
));
$owner->extend('updateDisownershipQuery', $disowned, $sourceStage, $targetStage, $relationship);
$disowned->execute();
}
}
/**
* Removes the record from both live and stage
*
* @return bool Success
*/
public function doArchive()
{
$owner = $this->owner;
if (!$owner->canArchive()) {
return false;
}
$owner->invokeWithExtensions('onBeforeArchive', $this);
$owner->doUnpublish();
$owner->deleteFromStage(static::DRAFT);
$owner->invokeWithExtensions('onAfterArchive', $this);
return true;
}
/**
* Removes this record from the live site
*
* @return bool Flag whether the unpublish was successful
*/
public function doUnpublish()
{
$owner = $this->owner;
if (!$owner->canUnpublish()) {
return false;
}
// Skip if this record isn't saved
if (!$owner->isInDB()) {
return false;
}
// Skip if this record isn't on live
if (!$owner->isPublished()) {
return false;
}
$owner->invokeWithExtensions('onBeforeUnpublish');
$origReadingMode = static::get_reading_mode();
static::set_stage(static::LIVE);
// This way our ID won't be unset
$clone = clone $owner;
$clone->delete();
static::set_reading_mode($origReadingMode);
$owner->invokeWithExtensions('onAfterUnpublish');
return true;
}
/**
* Trigger unpublish of owning objects
*/
public function onAfterUnpublish()
{
$owner = $this->owner;
// Any objects which owned (and thus relied on the unpublished object) are now unpublished automatically.
foreach ($owner->findOwners(false) as $object) {
/** @var Versioned|DataObject $object */
$object->doUnpublish();
}
}
/**
* Revert the draft changes: replace the draft content with the content on live
*
* @return bool True if the revert was successful
*/
public function doRevertToLive()
{
$owner = $this->owner;
if (!$owner->canRevertToLive()) {
return false;
}
$owner->invokeWithExtensions('onBeforeRevertToLive');
$owner->copyVersionToStage(static::LIVE, static::DRAFT, false);
$owner->invokeWithExtensions('onAfterRevertToLive');
return true;
}
/**
* Trigger revert of all owned objects to stage
*/
public function onAfterRevertToLive()
{
$owner = $this->owner;
/** @var Versioned|DataObject $liveOwner */
$liveOwner = static::get_by_stage(get_class($owner), static::LIVE)
->byID($owner->ID);
// Revert any owned objects from the live stage only
foreach ($liveOwner->findOwned(false) as $object) {
/** @var Versioned|DataObject $object */
$object->doRevertToLive();
}
// Unlink any objects disowned as a result of this action
// I.e. objects which aren't owned anymore by this record, but are by the old draft record
$owner->unlinkDisownedObjects(Versioned::LIVE, Versioned::DRAFT);
}
/**
* @deprecated 4.0..5.0
*/
public function publish($fromStage, $toStage, $createNewVersion = false)
{
Deprecation::notice('5.0', 'Use copyVersionToStage instead');
$this->owner->copyVersionToStage($fromStage, $toStage, $createNewVersion);
}
/**
* Move a database record from one stage to the other.
*
* @param int|string $fromStage Place to copy from. Can be either a stage name or a version number.
* @param string $toStage Place to copy to. Must be a stage name.
* @param bool $createNewVersion Set this to true to create a new version number.
* By default, the existing version number will be copied over. Note if copying
* to the live stage, the draft stage will also be updated with the new version.
*/
public function copyVersionToStage($fromStage, $toStage, $createNewVersion = false)
{
$owner = $this->owner;
$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
$baseClass = $owner->baseClass();
$baseTable = $owner->baseTable();
/** @var Versioned|DataObject $from */
if (is_numeric($fromStage)) {
$from = Versioned::get_version($baseClass, $owner->ID, $fromStage);
} else {
$owner->flushCache();
$from = Versioned::get_one_by_stage($baseClass, $fromStage, array(
"\"{$baseTable}\".\"ID\" = ?" => $owner->ID
));
}
if (!$from) {
throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
}
$from->forceChange();
if ($createNewVersion) {
// Clear version to be automatically created on write
$from->Version = null;
} else {
$from->migrateVersion($from->Version);
// Mark this version as having been published at some stage
$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
$extTable = $this->extendWithSuffix($baseTable);
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
$oldMode = Versioned::get_reading_mode();
Versioned::set_stage($toStage);
// Migrate stage prior to write
$from->setSourceQueryParam('Versioned.mode', 'stage');
$from->setSourceQueryParam('Versioned.stage', $toStage);
$conn = DB::get_conn();
if (method_exists($conn, 'allowPrimaryKeyEditing')) {
$conn->allowPrimaryKeyEditing($baseTable, true);
$from->write();
$conn->allowPrimaryKeyEditing($baseTable, false);
} else {
$from->write();
}
$from->destroy();
Versioned::set_reading_mode($oldMode);
$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
}
/**
* Set the migrating version.
*
* @param string $version The version.
*/
public function migrateVersion($version)
{
$this->migratingVersion = $version;
}
/**
* Compare two stages to see if they're different.
*
* Only checks the version numbers, not the actual content.
*
* @param string $stage1 The first stage to check.
* @param string $stage2
* @return bool
*/
public function stagesDiffer($stage1, $stage2)
{
$table1 = $this->baseTable($stage1);
$table2 = $this->baseTable($stage2);
$id = $this->owner->ID ?: $this->owner->OldID;
if (!$id) {
return true;
}
// We test for equality - if one of the versions doesn't exist, this
// will be false.
// TODO: DB Abstraction: if statement here:
$stagesAreEqual = DB::prepared_query(
"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
AND \"$table1\".\"ID\" = ?",
array($id)
)->value();
return !$stagesAreEqual;
}
/**
* @param string $filter
* @param string $sort
* @param string $limit
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
* @param string $having
* @return ArrayList
*/
public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "")
{
return $this->allVersions($filter, $sort, $limit, $join, $having);
}
/**
* Return a list of all the versions available.
*
* @param string $filter
* @param string $sort
* @param string $limit
* @param string $join @deprecated use leftJoin($table, $joinClause) instead
* @param string $having @deprecated
* @return ArrayList
*/
public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "")
{
// Make sure the table names are not postfixed (e.g. _Live)
$oldMode = static::get_reading_mode();
static::set_stage(static::DRAFT);
$owner = $this->owner;
$list = DataObject::get(get_class($owner), $filter, $sort, $join, $limit);
if ($having) {
// @todo - This method doesn't exist on DataList
$list->having($having);
}
$query = $list->dataQuery()->query();
$baseTable = null;
foreach ($query->getFrom() as $table => $tableJoin) {
if (is_string($tableJoin) && $tableJoin[0] == '"') {
$baseTable = str_replace('"', '', $tableJoin);
} elseif (is_string($tableJoin) && substr($tableJoin, 0, 5) != 'INNER') {
$query->setFrom(array(
$table => "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\"=\"{$baseTable}_Versions\".\"RecordID\""
. " AND \"$table\".\"Version\" = \"{$baseTable}_Versions\".\"Version\""
));
}
$query->renameTable($table, $table . '_Versions');
}
// Add all <basetable>_Versions columns
foreach (Config::inst()->get(static::class, 'db_for_versions_table') as $name => $type) {
$query->selectField(sprintf('"%s_Versions"."%s"', $baseTable, $name), $name);
}
$query->addWhere(array(
"\"{$baseTable}_Versions\".\"RecordID\" = ?" => $owner->ID
));
$query->setOrderBy(($sort) ? $sort
: "\"{$baseTable}_Versions\".\"LastEdited\" DESC, \"{$baseTable}_Versions\".\"Version\" DESC");
$records = $query->execute();
$versions = new ArrayList();
foreach ($records as $record) {
$versions->push(new Versioned_Version($record));
}
Versioned::set_reading_mode($oldMode);
return $versions;
}
/**
* Compare two version, and return the diff between them.
*
* @param string $from The version to compare from.
* @param string $to The version to compare to.
*
* @return DataObject
*/
public function compareVersions($from, $to)
{
$owner = $this->owner;
$fromRecord = Versioned::get_version($owner->class, $owner->ID, $from);
$toRecord = Versioned::get_version($owner->class, $owner->ID, $to);
$diff = new DataDifferencer($fromRecord, $toRecord);
return $diff->diffedData();
}
/**
* Return the base table - the class that directly extends DataObject.
*
* Protected so it doesn't conflict with DataObject::baseTable()
*
* @param string $stage
* @return string
*/
protected function baseTable($stage = null)
{
$baseTable = $this->owner->baseTable();
return $this->stageTable($baseTable, $stage);
}
/**
* Given a table and stage determine the table name.
*
* Note: Stages this asset does not exist in will default to the draft table.
*
* @param string $table Main table
* @param string $stage
* @return string Staged table name
*/
public function stageTable($table, $stage)
{
if ($this->hasStages() && $stage === static::LIVE) {
return "{$table}_{$stage}";
}
return $table;
}
//-----------------------------------------------------------------------------------------------//
/**
* Determine if the current user is able to set the given site stage / archive
*
* @param HTTPRequest $request
* @return bool
*/
public static function can_choose_site_stage($request)
{
// Request is allowed if stage isn't being modified
if ((!$request->getVar('stage') || $request->getVar('stage') === static::LIVE)
&& !$request->getVar('archiveDate')
) {
return true;
}
// Check permissions with member ID in session.
$member = Member::currentUser();
$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
return $member && Permission::checkMember($member, $permissions);
}
/**
* Choose the stage the site is currently on.
*
* If $_GET['stage'] is set, then it will use that stage, and store it in
* the session.
*
* if $_GET['archiveDate'] is set, it will use that date, and store it in
* the session.
*
* If neither of these are set, it checks the session, otherwise the stage
* is set to 'Live'.
*/
public static function choose_site_stage()
{
// Check any pre-existing session mode
$preexistingMode = Session::get('readingMode');
// Determine the reading mode
if (isset($_GET['stage'])) {
$stage = ucfirst(strtolower($_GET['stage']));
if (!in_array($stage, array(static::DRAFT, static::LIVE))) {
$stage = static::LIVE;
}
$mode = 'Stage.' . $stage;
} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
$mode = 'Archive.' . $_GET['archiveDate'];
} elseif ($preexistingMode) {
$mode = $preexistingMode;
} else {
$mode = static::DEFAULT_MODE;
}
// Save reading mode
Versioned::set_reading_mode($mode);
// Try not to store the mode in the session if not needed
if (($preexistingMode && $preexistingMode !== $mode)
|| (!$preexistingMode && $mode !== static::DEFAULT_MODE)
) {
Session::set('readingMode', $mode);
}
if (!headers_sent() && !Director::is_cli()) {
if (Versioned::get_stage() == 'Live') {
// clear the cookie if it's set
if (Cookie::get('bypassStaticCache')) {
Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
}
} else {
// set the cookie if it's cleared
if (!Cookie::get('bypassStaticCache')) {
Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
}
}
}
}
/**
* Set the current reading mode.
*
* @param string $mode
*/
public static function set_reading_mode($mode)
{
self::$reading_mode = $mode;
}
/**
* Get the current reading mode.
*
* @return string
*/
public static function get_reading_mode()
{
return self::$reading_mode;
}
/**
* Get the current reading stage.
*
* @return string
*/
public static function get_stage()
{
$parts = explode('.', Versioned::get_reading_mode());
if ($parts[0] == 'Stage') {
return $parts[1];
}
return null;
}
/**
* Get the current archive date.
*
* @return string
*/
public static function current_archived_date()
{
$parts = explode('.', Versioned::get_reading_mode());
if ($parts[0] == 'Archive') {
return $parts[1];
}
return null;
}
/**
* Set the reading stage.
*
* @param string $stage New reading stage.
* @throws InvalidArgumentException
*/
public static function set_stage($stage)
{
if (!in_array($stage, [static::LIVE, static::DRAFT])) {
throw new \InvalidArgumentException("Invalid stage name \"{$stage}\"");
}
static::set_reading_mode('Stage.' . $stage);
}
/**
* Set the reading archive date.
*
* @param string $date New reading archived date.
*/
public static function reading_archived_date($date)
{
Versioned::set_reading_mode('Archive.' . $date);
}
/**
* Get a singleton instance of a class in the given stage.
*
* @param string $class The name of the class.
* @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 $sort A sort expression to be inserted into the ORDER BY clause.
*
* @return DataObject
*/
public static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '')
{
// TODO: No identity cache operating
$items = static::get_by_stage($class, $stage, $filter, $sort, null, 1);
return $items->first();
}
/**
* Gets the current version number of a specific record.
*
* @param string $class
* @param string $stage
* @param int $id
* @param boolean $cache
*
* @return int
*/
public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true)
{
$baseClass = DataObject::getSchema()->baseDataClass($class);
$stageTable = DataObject::getSchema()->tableName($baseClass);
if ($stage === static::LIVE) {
$stageTable .= "_{$stage}";
}
// cached call
if ($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
return self::$cache_versionnumber[$baseClass][$stage][$id];
}
// get version as performance-optimized SQL query (gets called for each record in the sitetree)
$version = DB::prepared_query(
"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
array($id)
)->value();
// cache value (if required)
if ($cache) {
if (!isset(self::$cache_versionnumber[$baseClass])) {
self::$cache_versionnumber[$baseClass] = array();
}
if (!isset(self::$cache_versionnumber[$baseClass][$stage])) {
self::$cache_versionnumber[$baseClass][$stage] = array();
}
self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
}
return $version;
}
/**
* Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
* a list of record IDs, for more efficient database querying. If $idList
* is null, then every record will be pre-cached.
*
* @param string $class
* @param string $stage
* @param array $idList
*/
public static function prepopulate_versionnumber_cache($class, $stage, $idList = null)
{
if (!Config::inst()->get(static::class, 'prepopulate_versionnumber_cache')) {
return;
}
$filter = "";
$parameters = array();
if ($idList) {
// Validate the ID list
foreach ($idList as $id) {
if (!is_numeric($id)) {
user_error(
"Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
E_USER_ERROR
);
}
}
$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
$parameters = $idList;
}
/** @var Versioned|DataObject $singleton */
$singleton = DataObject::singleton($class);
$baseClass = $singleton->baseClass();
$baseTable = $singleton->baseTable();
$stageTable = $singleton->stageTable($baseTable, $stage);
$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
foreach ($versions as $id => $version) {
self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
}
}
/**
* Get a set of class instances by the given stage.
*
* @param string $class The name of the class.
* @param string $stage The name of the stage.
* @param string $filter A filter to be inserted into the WHERE clause.
* @param string $sort A sort expression to be inserted into the ORDER BY clause.
* @param string $join Deprecated, use leftJoin($table, $joinClause) instead
* @param int $limit A limit on the number of records returned from the database.
* @param string $containerClass The container class for the result set (default is DataList)
*
* @return DataList A modified DataList designated to the specified stage
*/
public static function get_by_stage(
$class,
$stage,
$filter = '',
$sort = '',
$join = '',
$limit = null,
$containerClass = 'SilverStripe\ORM\DataList'
) {
$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
return $result->setDataQueryParam(array(
'Versioned.mode' => 'stage',
'Versioned.stage' => $stage
));
}
/**
* Delete this record from the given stage
*
* @param string $stage
*/
public function deleteFromStage($stage)
{
$oldMode = Versioned::get_reading_mode();
Versioned::set_stage($stage);
$owner = $this->owner;
$clone = clone $owner;
$clone->delete();
Versioned::set_reading_mode($oldMode);
// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
$baseClass = $owner->baseClass();
self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
}
/**
* Write the given record to the given stage.
* Note: If writing to live, this will write to stage as well.
*
* @param string $stage
* @param boolean $forceInsert
* @return int The ID of the record
*/
public function writeToStage($stage, $forceInsert = false)
{
$oldMode = Versioned::get_reading_mode();
Versioned::set_stage($stage);
$owner = $this->owner;
$owner->forceChange();
$result = $owner->write(false, $forceInsert);
Versioned::set_reading_mode($oldMode);
return $result;
}
/**
* Roll the draft version of this record to match the published record.
* Caution: Doesn't overwrite the object properties with the rolled back version.
*
* {@see doRevertToLive()} to reollback to live
*
* @param int $version Version number
*/
public function doRollbackTo($version)
{
$owner = $this->owner;
$owner->extend('onBeforeRollback', $version);
$owner->copyVersionToStage($version, static::DRAFT, true);
$owner->writeWithoutVersion();
$owner->extend('onAfterRollback', $version);
}
public function onAfterRollback($version)
{
// Find record at this version
$baseClass = DataObject::getSchema()->baseDataClass($this->owner);
/** @var Versioned|DataObject $recordVersion */
$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
// Note that unlike other publishing actions, rollback is NOT recursive;
// The owner collects all objects and writes them back using writeToStage();
foreach ($recordVersion->findOwned() as $object) {
/** @var Versioned|DataObject $object */
$object->writeToStage(static::DRAFT);
}
}
/**
* 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 = DataObject::getSchema()->baseDataClass($class);
$list = DataList::create($baseClass)
->setDataQueryParam("Versioned.mode", "latest_versions");
return $list->byID($id);
}
/**
* Returns whether the current record is the latest one.
*
* @todo Performance - could do this directly via SQL.
*
* @see get_latest_version()
* @see latestPublished
*
* @return boolean
*/
public function isLatestVersion()
{
$owner = $this->owner;
if (!$owner->isInDB()) {
return false;
}
$version = static::get_latest_version($owner->class, $owner->ID);
return ($version->Version == $owner->Version);
}
/**
* Check if this record exists on live
*
* @return bool
*/
public function isPublished()
{
$id = $this->owner->ID ?: $this->owner->OldID;
if (!$id) {
return false;
}
// Non-staged objects are considered "published" if saved
if (!$this->hasStages()) {
return true;
}
$table = $this->baseTable(static::LIVE);
$result = DB::prepared_query(
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
array($id)
);
return (bool)$result->value();
}
/**
* Check if page doesn't exist on any stage, but used to be
*
* @return bool
*/
public function isArchived()
{
$id = $this->owner->ID ?: $this->owner->OldID;
return $id && !$this->isOnDraft() && !$this->isPublished();
}
/**
* Check if this record exists on the draft stage
*
* @return bool
*/
public function isOnDraft()
{
$id = $this->owner->ID ?: $this->owner->OldID;
if (!$id) {
return false;
}
$table = $this->baseTable();
$result = DB::prepared_query(
"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
array($id)
);
return (bool)$result->value();
}
/**
* Compares current draft with live version, and returns true if no draft version of this page exists but the page
* is still published (eg, after triggering "Delete from draft site" in the CMS).
*
* @return bool
*/
public function isOnLiveOnly()
{
return $this->isPublished() && !$this->isOnDraft();
}
/**
* Compares current draft with live version, and returns true if no live version exists, meaning the page was never
* published.
*
* @return bool
*/
public function isOnDraftOnly()
{
return $this->isOnDraft() && !$this->isPublished();
}
/**
* Compares current draft with live version, and returns true if these versions differ, meaning there have been
* unpublished changes to the draft site.
*
* @return bool
*/
public function isModifiedOnDraft()
{
return $this->isOnDraft() && $this->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
}
/**
* Return the equivalent of a DataList::create() call, querying the latest
* version of each record stored in the (class)_Versions tables.
*
* In particular, this will query deleted records as well as active ones.
*
* @param string $class
* @param string $filter
* @param string $sort
* @return DataList
*/
public static function get_including_deleted($class, $filter = "", $sort = "")
{
$list = DataList::create($class)
->where($filter)
->sort($sort)
->setDataQueryParam("Versioned.mode", "latest_versions");
return $list;
}
/**
* Return the specific version of the given id.
*
* Caution: The record is retrieved as a DataObject, but saving back
* modifications via write() will create a new version, rather than
* modifying the existing one.
*
* @param string $class
* @param int $id
* @param int $version
*
* @return DataObject
*/
public static function get_version($class, $id, $version)
{
$baseClass = DataObject::getSchema()->baseDataClass($class);
$list = DataList::create($baseClass)
->setDataQueryParam([
"Versioned.mode" => 'version',
"Versioned.version" => $version
]);
return $list->byID($id);
}
/**
* Return a list of all versions for a given id.
*
* @param string $class
* @param int $id
*
* @return DataList
*/
public static function get_all_versions($class, $id)
{
$list = DataList::create($class)
->filter('ID', $id)
->setDataQueryParam('Versioned.mode', 'all_versions');
return $list;
}
/**
* @param array $labels
*/
public function updateFieldLabels(&$labels)
{
$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
}
/**
* @param FieldList $fields
*/
public function updateCMSFields(FieldList $fields)
{
// remove the version field from the CMS as this should be left
// entirely up to the extension (not the cms user).
$fields->removeByName('Version');
}
/**
* Ensure version ID is reset to 0 on duplicate
*
* @param DataObject $source Record this was duplicated from
* @param bool $doWrite
*/
public function onBeforeDuplicate($source, $doWrite)
{
$this->owner->Version = 0;
}
public function flushCache()
{
self::$cache_versionnumber = array();
}
/**
* Return a piece of text to keep DataObject cache keys appropriately specific.
*
* @return string
*/
public function cacheKeyComponent()
{
return 'versionedmode-'.static::get_reading_mode();
}
/**
* Returns an array of possible stages.
*
* @return array
*/
public function getVersionedStages()
{
if ($this->hasStages()) {
return [static::DRAFT, static::LIVE];
} else {
return [static::DRAFT];
}
}
public static function get_template_global_variables()
{
return array(
'CurrentReadingMode' => 'get_reading_mode'
);
}
/**
* Check if this object has stages
*
* @return bool True if this object is staged
*/
public function hasStages()
{
return $this->mode === static::STAGEDVERSIONED;
}
/**
* Merge single object into a list
*
* @param ArrayList $list Global list. Object will not be added if already added to this list.
* @param ArrayList $added Additional list to insert into
* @param DataObject $item Item to add
* @return mixed
*/
protected function mergeRelatedObject($list, $added, $item)
{
// Identify item
$itemKey = get_class($item) . '/' . $item->ID;
// Write if saved, versioned, and not already added
if ($item->isInDB() && $item->has_extension(static::class) && !isset($list[$itemKey])) {
$list[$itemKey] = $item;
$added[$itemKey] = $item;
}
// Add joined record (from many_many through) automatically
$joined = $item->getJoin();
if ($joined) {
$this->mergeRelatedObject($list, $added, $joined);
}
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Core\Extension;
use SilverStripe\Forms\GridField\GridField;
use SilverStripe\ORM\DataObject;
/**
* Extends {@see GridFieldDetailForm}
*/
class VersionedGridFieldDetailForm extends Extension
{
/**
* @param string $class
* @param GridField $gridField
* @param DataObject $record
* @param RequestHandler $requestHandler
*/
public function updateItemRequestClass(&$class, $gridField, $record, $requestHandler)
{
// Conditionally use a versioned item handler
if ($record && $record->has_extension('SilverStripe\ORM\Versioning\Versioned')) {
$class = 'SilverStripe\ORM\Versioning\VersionedGridFieldItemRequest';
}
}
}

View File

@ -1,199 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Core\Convert;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ValidationResult;
/**
* Provides versioned dataobject support to {@see GridFieldDetailForm_ItemRequest}
*
* @property GridFieldDetailForm_ItemRequest $owner
*/
class VersionedGridFieldItemRequest extends GridFieldDetailForm_ItemRequest
{
protected function getFormActions()
{
$actions = parent::getFormActions();
// Check if record is versionable
/** @var Versioned|DataObject $record */
$record = $this->getRecord();
if (!$record || !$record->has_extension(Versioned::class)) {
return $actions;
}
// Save & Publish action
if ($record->canPublish()) {
// "publish", as with "save", it supports an alternate state to show when action is needed.
$publish = FormAction::create(
'doPublish',
_t('VersionedGridFieldItemRequest.BUTTONPUBLISH', 'Publish')
)
->setUseButtonTag(true)
->addExtraClass('btn btn-primary font-icon-rocket');
// Insert after save
if ($actions->fieldByName('action_doSave')) {
$actions->insertAfter('action_doSave', $publish);
} else {
$actions->push($publish);
}
}
// Unpublish action
$isPublished = $record->isPublished();
if ($isPublished && $record->canUnpublish()) {
$actions->push(
FormAction::create(
'doUnpublish',
_t('VersionedGridFieldItemRequest.BUTTONUNPUBLISH', 'Unpublish')
)
->setUseButtonTag(true)
->setDescription(_t(
'VersionedGridFieldItemRequest.BUTTONUNPUBLISHDESC',
'Remove this record from the published site'
))
->addExtraClass('btn-secondary')
);
}
// Archive action
if ($record->canArchive()) {
// Replace "delete" action
$actions->removeByName('action_doDelete');
// "archive"
$actions->push(
FormAction::create('doArchive', _t('VersionedGridFieldItemRequest.ARCHIVE', 'Archive'))
->setDescription(_t(
'VersionedGridFieldItemRequest.BUTTONARCHIVEDESC',
'Unpublish and send to archive'
))
->addExtraClass('delete btn-secondary')
);
}
return $actions;
}
/**
* Archive this versioned record
*
* @param array $data
* @param Form $form
* @return HTTPResponse
*/
public function doArchive($data, $form)
{
/** @var Versioned|DataObject $record */
$record = $this->getRecord();
if (!$record->canArchive()) {
return $this->httpError(403);
}
// Record name before it's deleted
$title = $record->Title;
$record->doArchive();
$message = sprintf(
_t('VersionedGridFieldItemRequest.Archived', 'Archived %s %s'),
$record->i18n_singular_name(),
Convert::raw2xml($title)
);
$this->setFormMessage($form, $message);
//when an item is deleted, redirect to the parent controller
$controller = $this->getToplevelController();
$controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh
return $controller->redirect($this->getBackLink(), 302); //redirect back to admin section
}
/**
* Publish this versioned record
*
* @param array $data
* @param Form $form
* @return HTTPResponse
*/
public function doPublish($data, $form)
{
/** @var Versioned|DataObject $record */
$record = $this->getRecord();
$isNewRecord = $record->ID == 0;
// Check permission
if (!$record->canPublish()) {
return $this->httpError(403);
}
// Initial save and reload
$record = $this->saveFormIntoRecord($data, $form);
$record->publishRecursive();
$editURL = $this->Link('edit');
$xmlTitle = Convert::raw2xml($record->Title);
$link = "<a href=\"{$editURL}\">{$xmlTitle}</a>";
$message = _t(
'VersionedGridFieldItemRequest.Published',
'Published {name} {link}',
array(
'name' => $record->i18n_singular_name(),
'link' => $link
)
);
$this->setFormMessage($form, $message);
return $this->redirectAfterSave($isNewRecord);
}
/**
* Delete this record from the live site
*
* @param array $data
* @param Form $form
* @return HTTPResponse
*/
public function doUnpublish($data, $form)
{
/** @var Versioned|DataObject $record */
$record = $this->getRecord();
if (!$record->canUnpublish()) {
return $this->httpError(403);
}
// Record name before it's deleted
$title = $record->Title;
$record->doUnpublish();
$message = sprintf(
_t('VersionedGridFieldItemRequest.Unpublished', 'Unpublished %s %s'),
$record->i18n_singular_name(),
Convert::raw2xml($title)
);
$this->setFormMessage($form, $message);
// Redirect back to edit
return $this->redirectAfterSave(false);
}
/**
* @param Form $form
* @param string $message
*/
protected function setFormMessage($form, $message)
{
$form->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
$controller = $this->getToplevelController();
if ($controller->hasMethod('getEditForm')) {
/** @var Form $backForm */
$backForm = $controller->getEditForm();
$backForm->sessionMessage($message, 'good', ValidationResult::CAST_HTML);
}
}
}

View File

@ -1,132 +0,0 @@
<?php
namespace SilverStripe\ORM\Versioning;
use SilverStripe\Core\ClassInfo;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\SS_List;
use SilverStripe\Security\Member;
use SilverStripe\View\ViewableData;
/**
* Represents a single version of a record.
*
* @see Versioned
*/
class Versioned_Version extends ViewableData
{
/**
* @var array
*/
protected $record;
/**
* @var DataObject
*/
protected $object;
/**
* Create a new version from a database row
*
* @param array $record
*/
public function __construct($record)
{
$this->record = $record;
$record['ID'] = $record['RecordID'];
$className = $record['ClassName'];
$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
$this->failover = $this->object;
parent::__construct();
}
/**
* Either 'published' if published, or 'internal' if not.
*
* @return string
*/
public function PublishedClass()
{
return $this->record['WasPublished'] ? 'published' : 'internal';
}
/**
* Author of this DataObject
*
* @return Member
*/
public function Author()
{
return Member::get()->byID($this->record['AuthorID']);
}
/**
* Member object of the person who last published this record
*
* @return Member
*/
public function Publisher()
{
if (!$this->record['WasPublished']) {
return null;
}
return Member::get()->byID($this->record['PublisherID']);
}
/**
* True if this record is published via publish() method
*
* @return boolean
*/
public function Published()
{
return !empty($this->record['WasPublished']);
}
/**
* 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) {
$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();
} 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 {
user_error("$relation is not a relation/field on " . get_class($component), E_USER_ERROR);
}
}
}
// Bail if the component is null
if (!$component) {
return null;
}
if ($component->hasMethod($fieldName)) {
return $component->$fieldName();
}
return $component->$fieldName;
}
}

View File

@ -17,6 +17,7 @@ use SilverStripe\Core\Tests\ObjectTest\MySubObject;
use SilverStripe\Core\Tests\ObjectTest\TestExtension;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Versioned\Versioned;
/**
* @todo tests for addStaticVars()
@ -432,18 +433,18 @@ class ObjectTest extends SapphireTest
{
// Simple case
$this->assertEquals(
array('SilverStripe\\ORM\\Versioning\\Versioned',array('Stage', 'Live')),
Object::parse_class_spec("SilverStripe\\ORM\\Versioning\\Versioned('Stage','Live')")
array(Versioned::class,array('Stage', 'Live')),
Object::parse_class_spec("SilverStripe\\Versioned\\Versioned('Stage','Live')")
);
// String with commas
$this->assertEquals(
array('SilverStripe\\ORM\\Versioning\\Versioned',array('Stage,Live', 'Stage')),
Object::parse_class_spec("SilverStripe\\ORM\\Versioning\\Versioned('Stage,Live','Stage')")
array(Versioned::class,array('Stage,Live', 'Stage')),
Object::parse_class_spec("SilverStripe\\Versioned\\Versioned('Stage,Live','Stage')")
);
// String with quotes
$this->assertEquals(
array('SilverStripe\\ORM\\Versioning\\Versioned',array('Stage\'Stage,Live\'Live', 'Live')),
Object::parse_class_spec("SilverStripe\\ORM\\Versioning\\Versioned('Stage\'Stage,Live\'Live','Live')")
array(Versioned::class,array('Stage\'Stage,Live\'Live', 'Live')),
Object::parse_class_spec("SilverStripe\\Versioned\\Versioned('Stage\\'Stage,Live\\'Live','Live')")
);
// True, false and null values

View File

@ -9,7 +9,7 @@ use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TextField;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
class FormFactoryTest extends SapphireTest
{
@ -19,6 +19,25 @@ class FormFactoryTest extends SapphireTest
protected static $fixture_file = 'FormFactoryTest.yml';
protected function getExtraDataObjects()
{
// Prevent setup breaking if versioned module absent
if (class_exists(Versioned::class)) {
return parent::getExtraDataObjects();
}
return [];
}
public function setUp()
{
parent::setUp();
// Note: Soft support for versioned module optionality
if (!class_exists(Versioned::class)) {
$this->markTestSkipped('FormFactoryTest requires the Versioned extension');
}
}
/**
* Test versioned form
*/
@ -34,9 +53,7 @@ class FormFactoryTest extends SapphireTest
// Check preview link
/**
* @var LiteralField $previewLink
*/
/** @var LiteralField $previewLink */
$previewLink = $form->Fields()->fieldByName('PreviewLink');
$this->assertInstanceOf(LiteralField::class, $previewLink);
$this->assertEquals(

View File

@ -8,7 +8,7 @@ use SilverStripe\Core\Extension;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\LiteralField;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* Provides versionable extensions to a controller / scaffolder

View File

@ -5,7 +5,7 @@ namespace SilverStripe\Forms\Tests\FormFactoryTest;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\Form;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* Edit controller for this form

View File

@ -4,7 +4,7 @@ namespace SilverStripe\Forms\Tests\FormFactoryTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* @mixin Versioned

View File

@ -1,102 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\Dev\SapphireTest;
class ChangeSetItemTest extends SapphireTest
{
protected $extraDataObjects = [
ChangeSetItemTest\VersionedObject::class
];
public function testChangeType()
{
$this->logInWithPermission('ADMIN');
$object = new ChangeSetItemTest\VersionedObject(['Foo' => 1]);
$object->write();
$item = new ChangeSetItem(
[
'ObjectID' => $object->ID,
'ObjectClass' => $object->baseClass(),
]
);
$this->assertEquals(
ChangeSetItem::CHANGE_CREATED,
$item->ChangeType,
'New objects that aren\'t yet published should return created'
);
$object->publishRecursive();
$this->assertEquals(
ChangeSetItem::CHANGE_NONE,
$item->ChangeType,
'Objects that have just been published should return no change'
);
$object->Foo += 1;
$object->write();
$this->assertEquals(
ChangeSetItem::CHANGE_MODIFIED,
$item->ChangeType,
'Object that have unpublished changes written to draft should show as modified'
);
$object->publishRecursive();
$this->assertEquals(
ChangeSetItem::CHANGE_NONE,
$item->ChangeType,
'Objects that have just been published should return no change'
);
// We need to use a copy, because ID is set to 0 by delete, causing the following unpublish to fail
$objectCopy = clone $object;
$objectCopy->delete();
$this->assertEquals(
ChangeSetItem::CHANGE_DELETED,
$item->ChangeType,
'Objects that have been deleted from draft (but not yet unpublished) should show as deleted'
);
$object->doUnpublish();
$this->assertEquals(
ChangeSetItem::CHANGE_NONE,
$item->ChangeType,
'Objects that have been deleted and then unpublished should return no change'
);
}
public function testGetForObject()
{
$this->logInWithPermission('ADMIN');
$object = new ChangeSetItemTest\VersionedObject(['Foo' => 1]);
$object->write();
$item = new ChangeSetItem(
[
'ObjectID' => $object->ID,
'ObjectClass' => $object->baseClass(),
]
);
$item->write();
$this->assertEquals(
ChangeSetItemTest\VersionedObject::get()->byID($object->ID)->toMap(),
ChangeSetItem::get_for_object($object)->first()->Object()->toMap()
);
$this->assertEquals(
ChangeSetItemTest\VersionedObject::get()->byID($object->ID)->toMap(),
ChangeSetItem::get_for_object_by_id($object->ID, $object->ClassName)->first()->Object()->toMap()
);
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetItemTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class VersionedObject extends DataObject implements TestOnly
{
private static $table_name = 'ChangeSetItemTest_Versioned';
private static $db = [
'Foo' => 'Int'
];
private static $extensions = [
Versioned::class
];
function canEdit($member = null)
{
return true;
}
}

View File

@ -1,511 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SebastianBergmann\Comparator\ComparisonFailure;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Tests\ChangeSetTest\BaseObject;
use SilverStripe\ORM\Tests\ChangeSetTest\MidObject;
use SilverStripe\ORM\Versioning\ChangeSet;
use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Session;
use PHPUnit_Framework_ExpectationFailedException;
/**
* Test {@see ChangeSet} and {@see ChangeSetItem} models
*/
class ChangeSetTest extends SapphireTest
{
protected static $fixture_file = 'ChangeSetTest.yml';
protected $extraDataObjects = [
ChangeSetTest\BaseObject::class,
ChangeSetTest\MidObject::class,
ChangeSetTest\EndObject::class,
ChangeSetTest\EndObjectChild::class,
];
/**
* Automatically publish all objects
*/
protected function publishAllFixtures()
{
$this->logInWithPermission('ADMIN');
foreach ($this->fixtureFactory->getFixtures() as $class => $fixtures) {
foreach ($fixtures as $handle => $id) {
/**
* @var Versioned|DataObject $object
*/
$object = $this->objFromFixture($class, $handle);
$object->publishSingle();
}
}
}
/**
* Check that the changeset includes the given items
*
* @param ChangeSet $cs
* @param array $match Array of object fixture keys with change type values
*/
protected function assertChangeSetLooksLike($cs, $match)
{
$items = $cs->Changes()->toArray();
foreach ($match as $key => $mode) {
list($class, $identifier) = explode('.', $key);
$object = $this->objFromFixture($class, $identifier);
foreach ($items as $i => $item) {
if ($item->ObjectClass == $object->baseClass()
&& $item->ObjectID == $object->ID
&& $item->Added == $mode
) {
unset($items[$i]);
continue 2;
}
}
throw new PHPUnit_Framework_ExpectationFailedException(
'Change set didn\'t include expected item',
new ComparisonFailure(array('Class' => $class, 'ID' => $object->ID, 'Added' => $mode), null, "$key => $mode", '')
);
}
if (count($items)) {
$extra = [];
foreach ($items as $item) {
$extra[] = ['Class' => $item->ObjectClass, 'ID' => $item->ObjectID, 'Added' => $item->Added, 'ChangeType' => $item->getChangeType()];
}
throw new PHPUnit_Framework_ExpectationFailedException(
'Change set included items that weren\'t expected',
new ComparisonFailure(array(), $extra, '', print_r($extra, true))
);
}
}
public function testAddObject()
{
$cs = new ChangeSet();
$cs->write();
$cs->addObject($this->objFromFixture(ChangeSetTest\EndObject::class, 'end1'));
$cs->addObject($this->objFromFixture(ChangeSetTest\EndObjectChild::class, 'endchild1'));
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\EndObject::class.'.end1' => ChangeSetItem::EXPLICITLY,
ChangeSetTest\EndObjectChild::class.'.endchild1' => ChangeSetItem::EXPLICITLY
]
);
}
public function testDescription()
{
$cs = new ChangeSet();
$cs->write();
$cs->addObject($this->objFromFixture(ChangeSetTest\EndObject::class, 'end1'));
$this->assertEquals('one item', $cs->getDescription());
$cs->addObject($this->objFromFixture(ChangeSetTest\EndObjectChild::class, 'endchild1'));
$this->assertEquals('2 items', $cs->getDescription());
}
public function testRepeatedSyncIsNOP()
{
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY
]
);
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY
]
);
}
public function testSync()
{
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY
]
);
$end = $this->objFromFixture(ChangeSetTest\EndObject::class, 'end1');
$end->Baz = 3;
$end->write();
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY,
ChangeSetTest\EndObject::class.'.end1' => ChangeSetItem::IMPLICITLY
]
);
$baseItem = ChangeSetItem::get_for_object($base)->first();
$endItem = ChangeSetItem::get_for_object($end)->first();
$this->assertEquals(
[$baseItem->ID],
$endItem->ReferencedBy()->column("ID")
);
$this->assertDOSEquals(
[
[
'Added' => ChangeSetItem::EXPLICITLY,
'ObjectClass' => ChangeSetTest\BaseObject::class,
'ObjectID' => $base->ID,
'ChangeSetID' => $cs->ID
]
],
$endItem->ReferencedBy()
);
}
/**
* Test that sync includes implicit items
*/
public function testIsSynced()
{
$this->publishAllFixtures();
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$cs->addObject($base);
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY
]
);
$this->assertTrue($cs->isSynced());
$end = $this->objFromFixture(ChangeSetTest\EndObject::class, 'end1');
$end->Baz = 3;
$end->write();
$this->assertFalse($cs->isSynced());
$cs->sync();
$this->assertChangeSetLooksLike(
$cs,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY,
ChangeSetTest\EndObject::class.'.end1' => ChangeSetItem::IMPLICITLY
]
);
$this->assertTrue($cs->isSynced());
}
public function testCanPublish()
{
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Test un-authenticated user cannot publish
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canPublish());
// campaign admin only permission doesn't grant publishing rights
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertFalse($changeSet->canPublish());
// With model publish permissions only publish is allowed
$this->logInWithPermission('PERM_canPublish');
$this->assertTrue($changeSet->canPublish());
// Test user with the necessary minimum permissions can login
$this->logInWithPermission(
[
'CMS_ACCESS_CampaignAdmin',
'PERM_canPublish'
]
);
$this->assertTrue($changeSet->canPublish());
}
public function testHasChanges()
{
// Create changeset containing all items (unpublished)
Versioned::set_stage(Versioned::DRAFT);
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = new ChangeSetTest\BaseObject();
$base->Foo = 1;
$base->write();
$changeSet->addObject($base);
// New changeset with changes can be published
$this->assertTrue($changeSet->canPublish());
$this->assertTrue($changeSet->hasChanges());
// Writing the record to live dissolves the changes in this changeset
$base->publishSingle();
$this->assertTrue($changeSet->canPublish());
$this->assertFalse($changeSet->hasChanges());
// Changeset can be safely published without error
$changeSet->publish();
}
public function testCanRevert()
{
$this->markTestSkipped("Requires ChangeSet::revert to be implemented first");
}
public function testCanEdit()
{
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canEdit
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canEdit());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canEdit());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canEdit());
}
public function testCanCreate()
{
// Check canCreate
Session::clear("loggedInAs");
$this->assertFalse(ChangeSet::singleton()->canCreate());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse(ChangeSet::singleton()->canCreate());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue(ChangeSet::singleton()->canCreate());
}
public function testCanDelete()
{
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canDelete
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canDelete());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canDelete());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canDelete());
}
public function testCanView()
{
// Create changeset containing all items (unpublished)
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
// Check canView
Session::clear("loggedInAs");
$this->assertFalse($changeSet->canView());
$this->logInWithPermission('SomeWrongPermission');
$this->assertFalse($changeSet->canView());
$this->logInWithPermission('CMS_ACCESS_CampaignAdmin');
$this->assertTrue($changeSet->canView());
}
public function testPublish()
{
$this->publishAllFixtures();
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
$baseID = $base->ID;
$baseBefore = $base->Version;
$end1 = $this->objFromFixture(ChangeSetTest\EndObject::class, 'end1');
$end1ID = $end1->ID;
$end1Before = $end1->Version;
// Create a new changest
$changeset = new ChangeSet();
$changeset->write();
$changeset->addObject($base);
$changeset->addObject($end1);
// Make a lot of changes
// - ChangeSetTest_Base.base modified
// - ChangeSetTest_End.end1 deleted
// - new ChangeSetTest_Mid added
$base->Foo = 343;
$base->write();
$baseAfter = $base->Version;
$midNew = new ChangeSetTest\MidObject();
$midNew->Bar = 39;
$midNew->write();
$midNewID = $midNew->ID;
$midNewAfter = $midNew->Version;
$end1->delete();
$changeset->addObject($midNew);
// Publish
$this->logInWithPermission('ADMIN');
$this->assertTrue($changeset->canPublish());
$this->assertTrue($changeset->isSynced());
$changeset->publish();
$this->assertEquals(ChangeSet::STATE_PUBLISHED, $changeset->State);
// Check each item has the correct before/after version applied
$baseChange = $changeset->Changes()->filter(
[
'ObjectClass' => ChangeSetTest\BaseObject::class,
'ObjectID' => $baseID,
]
)->first();
$this->assertEquals((int)$baseBefore, (int)$baseChange->VersionBefore);
$this->assertEquals((int)$baseAfter, (int)$baseChange->VersionAfter);
$this->assertEquals((int)$baseChange->VersionBefore + 1, (int)$baseChange->VersionAfter);
$this->assertEquals(
(int)$baseChange->VersionAfter,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest\BaseObject::class, Versioned::LIVE, $baseID)
);
$end1Change = $changeset->Changes()->filter(
[
'ObjectClass' => ChangeSetTest\EndObject::class,
'ObjectID' => $end1ID,
]
)->first();
$this->assertEquals((int)$end1Before, (int)$end1Change->VersionBefore);
$this->assertEquals(0, (int)$end1Change->VersionAfter);
$this->assertEquals(
0,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest\EndObject::class, Versioned::LIVE, $end1ID)
);
$midNewChange = $changeset->Changes()->filter(
[
'ObjectClass' => ChangeSetTest\MidObject::class,
'ObjectID' => $midNewID,
]
)->first();
$this->assertEquals(0, (int)$midNewChange->VersionBefore);
$this->assertEquals((int)$midNewAfter, (int)$midNewChange->VersionAfter);
$this->assertEquals(
(int)$midNewAfter,
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest\MidObject::class, Versioned::LIVE, $midNewID)
);
// Test trying to re-publish is blocked
$this->setExpectedException(
'BadMethodCallException',
"ChangeSet can't be published if it has been already published or reverted."
);
$changeset->publish();
}
/**
* Ensure that related objects are disassociated on live
*/
public function testUnlinkDisassociated()
{
$this->publishAllFixtures();
/**
* @var BaseObject $base
*/
$base = $this->objFromFixture(ChangeSetTest\BaseObject::class, 'base');
/**
* @var MidObject $mid1 $mid2
*/
$mid1 = $this->objFromFixture(ChangeSetTest\MidObject::class, 'mid1');
$mid2 = $this->objFromFixture(ChangeSetTest\MidObject::class, 'mid2');
// Remove mid1 from stage
$this->assertEquals($base->ID, $mid1->BaseID);
$this->assertEquals($base->ID, $mid2->BaseID);
$mid1->deleteFromStage(Versioned::DRAFT);
// Publishing recursively should unlinkd this object
$changeset = new ChangeSet();
$changeset->write();
$changeset->addObject($base);
// Assert changeset only contains root object
$this->assertChangeSetLooksLike(
$changeset,
[
ChangeSetTest\BaseObject::class.'.base' => ChangeSetItem::EXPLICITLY
]
);
$changeset->publish();
// mid1 on live exists, but has BaseID set to zero
$mid1Live = Versioned::get_by_stage(ChangeSetTest\MidObject::class, Versioned::LIVE)
->byID($mid1->ID);
$this->assertNotNull($mid1Live);
$this->assertEquals($mid1->ID, $mid1Live->ID);
$this->assertEquals(0, $mid1Live->BaseID);
// mid2 on live exists and retains BaseID
$mid2Live = Versioned::get_by_stage(ChangeSetTest\MidObject::class, Versioned::LIVE)
->byID($mid2->ID);
$this->assertNotNull($mid2Live);
$this->assertEquals($mid2->ID, $mid2Live->ID);
$this->assertEquals($base->ID, $mid2Live->BaseID);
}
}

View File

@ -1,21 +0,0 @@
SilverStripe\ORM\Tests\ChangeSetTest\BaseObject:
base:
Foo: 1
SilverStripe\ORM\Tests\ChangeSetTest\EndObject:
end1:
Baz: 1
end2:
Baz: 2
SilverStripe\ORM\Tests\ChangeSetTest\EndObjectChild:
endchild1:
Baz: 3
Qux: 3
SilverStripe\ORM\Tests\ChangeSetTest\MidObject:
mid1:
Bar: 1
Base: =>SilverStripe\ORM\Tests\ChangeSetTest\BaseObject.base
End: =>SilverStripe\ORM\Tests\ChangeSetTest\EndObject.end1
mid2:
Bar: 2
Base: =>SilverStripe\ORM\Tests\ChangeSetTest\BaseObject.base
End: =>SilverStripe\ORM\Tests\ChangeSetTest\EndObject.end2

View File

@ -1,33 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class BaseObject extends DataObject implements TestOnly
{
use Permissions;
private static $table_name = 'ChangeSetTest_Base';
private static $db = [
'Foo' => 'Int',
];
private static $has_many = [
'Mids' => MidObject::class,
];
private static $owns = [
'Mids',
];
private static $extensions = [
Versioned::class,
];
}

View File

@ -1,25 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class EndObject extends DataObject implements TestOnly
{
use Permissions;
private static $table_name = 'ChangeSetTest_End';
private static $db = [
'Baz' => 'Int',
];
private static $extensions = [
Versioned::class,
];
}

View File

@ -1,18 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class EndObjectChild extends EndObject implements TestOnly
{
private static $table_name = 'ChangeSetTest_EndObjectChild';
private static $db = [
'Qux' => 'Int',
];
}

View File

@ -1,34 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class MidObject extends DataObject implements TestOnly
{
use Permissions;
private static $table_name = 'ChangeSetTest_Mid';
private static $db = [
'Bar' => 'Int',
];
private static $has_one = [
'Base' => BaseObject::class,
'End' => EndObject::class,
];
private static $owns = [
'End',
];
private static $extensions = [
Versioned::class,
];
}

View File

@ -1,50 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ChangeSetTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Security\Permission;
/**
* Provides a set of targettable permissions for tested models
*
* @mixin Versioned
* @mixin DataObject
*/
trait Permissions
{
public function canEdit($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canDelete($member = null)
{
return $this->can(__FUNCTION__, $member);
}
public function canCreate($member = null, $context = array())
{
return $this->can(__FUNCTION__, $member, $context);
}
public function canPublish($member = null, $context = array())
{
return $this->can(__FUNCTION__, $member, $context);
}
public function canUnpublish($member = null, $context = array())
{
return $this->can(__FUNCTION__, $member, $context);
}
public function can($perm, $member = null, $context = array())
{
$perms = [
"PERM_{$perm}",
'CAN_ALL',
];
return Permission::checkMember($member, $perms);
}
}

View File

@ -4,16 +4,11 @@ namespace SilverStripe\ORM\Tests\DBClassNameTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'DBClassNameTest_Object';
private static $extensions = array(
Versioned::class
);
private static $db = array(
'DefaultClass' => 'DBClassName',
'AnyClass' => 'DBClassName(\'SilverStripe\\ORM\\DataObject\')',

View File

@ -1,106 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Assets\Folder;
use SilverStripe\Assets\Image;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\Versioning\DataDifferencer;
use SilverStripe\Assets\File;
use SilverStripe\Assets\Filesystem;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Assets\Tests\Storage\AssetStoreTest\TestAssetStore;
class DataDifferencerTest extends SapphireTest
{
protected static $fixture_file = 'DataDifferencerTest.yml';
protected $extraDataObjects = array(
DataDifferencerTest\TestObject::class,
DataDifferencerTest\HasOneRelationObject::class
);
protected function setUp()
{
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
// Set backend root to /DataDifferencerTest
TestAssetStore::activate('DataDifferencerTest');
// Create a test files for each of the fixture references
$files = File::get()->exclude('ClassName', Folder::class);
foreach ($files as $file) {
$fromPath = __DIR__ . '/DataDifferencerTest/images/' . $file->Name;
$destPath = TestAssetStore::getLocalPath($file); // Only correct for test asset store
Filesystem::makeFolder(dirname($destPath));
copy($fromPath, $destPath);
}
}
protected function tearDown()
{
TestAssetStore::reset();
parent::tearDown();
}
public function testArrayValues()
{
$obj1 = $this->objFromFixture(DataDifferencerTest\TestObject::class, 'obj1');
$beforeVersion = $obj1->Version;
// create a new version
$obj1->Choices = 'a';
$obj1->write();
$afterVersion = $obj1->Version;
$obj1v1 = Versioned::get_version(DataDifferencerTest\TestObject::class, $obj1->ID, $beforeVersion);
$obj1v2 = Versioned::get_version(DataDifferencerTest\TestObject::class, $obj1->ID, $afterVersion);
$differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData();
// TODO Using getter would split up field again, bug only caused by simulating
// an array-based value in the first place.
$this->assertContains('<ins>a</ins><del>a,b</del>', str_replace(' ', '', $obj1Diff->getField('Choices')));
}
public function testHasOnes()
{
/**
* @var DataDifferencerTest\TestObject $obj1
*/
$obj1 = $this->objFromFixture(DataDifferencerTest\TestObject::class, 'obj1');
$image1 = $this->objFromFixture(Image::class, 'image1');
$image2 = $this->objFromFixture(Image::class, 'image2');
$relobj2 = $this->objFromFixture(DataDifferencerTest\HasOneRelationObject::class, 'relobj2');
// create a new version
$beforeVersion = $obj1->Version;
$obj1->ImageID = $image2->ID;
$obj1->HasOneRelationID = $relobj2->ID;
$obj1->write();
$afterVersion = $obj1->Version;
$this->assertNotEquals($beforeVersion, $afterVersion);
/**
* @var DataDifferencerTest\TestObject $obj1v1
*/
$obj1v1 = Versioned::get_version(DataDifferencerTest\TestObject::class, $obj1->ID, $beforeVersion);
/**
* @var DataDifferencerTest\TestObject $obj1v2
*/
$obj1v2 = Versioned::get_version(DataDifferencerTest\TestObject::class, $obj1->ID, $afterVersion);
$differ = new DataDifferencer($obj1v1, $obj1v2);
$obj1Diff = $differ->diffedData();
/**
* @skipUpgrade
*/
$this->assertContains($image1->Name, $obj1Diff->getField('Image'));
/**
* @skipUpgrade
*/
$this->assertContains($image2->Name, $obj1Diff->getField('Image'));
$this->assertContains(
'<ins>obj2</ins><del>obj1</del>',
str_replace(' ', '', $obj1Diff->getField('HasOneRelationID'))
);
}
}

View File

@ -1,19 +0,0 @@
SilverStripe\Assets\Image:
image1:
FileFilename: test-image.png
FileHash: 444065542b5dd5187166d8e1cd684e0d724c5a97
Name: test-image.png
image2:
FileFilename: test.image.with.dots.png
FileHash: 46affab7043cfd9f1ded919dd24affd08e926eca
Name: test.image.with.dots.png
SilverStripe\ORM\Tests\DataDifferencerTest\HasOneRelationObject:
relobj1:
Title: obj1
relobj2:
Title: obj2
SilverStripe\ORM\Tests\DataDifferencerTest\TestObject:
obj1:
Choices: a,b
Image: =>SilverStripe\Assets\Image.image1
HasOneRelation: =>SilverStripe\ORM\Tests\DataDifferencerTest\HasOneRelationObject.relobj1

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataDifferencerTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class HasOneRelationObject extends DataObject implements TestOnly
{
private static $table_name = 'DataDifferencerTest_HasOneRelationObject';
private static $db = array(
'Title' => 'Varchar'
);
private static $has_many = array(
'Objects' => TestObject::class
);
}

View File

@ -1,46 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataDifferencerTest;
use SilverStripe\Assets\Image;
use SilverStripe\Dev\TestOnly;
use SilverStripe\Forms\ListboxField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @property string $Choices
* @method Image Image()
* @method HasOneRelationObject HasOneRelation()
*/
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'DataDifferencerTest_Object';
private static $extensions = array(
Versioned::class
);
private static $db = array(
'Choices' => "Varchar",
);
private static $has_one = array(
'Image' => Image::class,
'HasOneRelation' => HasOneRelationObject::class
);
public function getCMSFields()
{
$fields = parent::getCMSFields();
$choices = array(
'a' => 'a',
'b' => 'b',
'c' => 'c',
);
$listField = new ListboxField('Choices', 'Choices', $choices);
$fields->push($listField);
return $fields;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -5,32 +5,21 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Tests\DataObjectLazyLoadingTest\VersionedObject;
use SilverStripe\ORM\Tests\DataObjectLazyLoadingTest\VersionedSubObject;
use SilverStripe\ORM\Tests\DataObjectTest\SubTeam;
use SilverStripe\ORM\Tests\DataObjectTest\Team;
use SilverStripe\ORM\Tests\VersionedTest\Subclass;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Dev\SapphireTest;
class DataObjectLazyLoadingTest extends SapphireTest
{
protected static $fixture_file = array(
'DataObjectTest.yml',
'VersionedTest.yml'
);
protected function getExtraDataObjects()
{
return array_merge(
DataObjectTest::$extra_data_objects,
ManyManyListTest::$extra_data_objects,
VersionedTest::$extra_data_objects,
[
VersionedObject::class,
VersionedSubObject::class,
]
ManyManyListTest::$extra_data_objects
);
}
@ -302,154 +291,4 @@ class DataObjectLazyLoadingTest extends SapphireTest
$this->assertArrayNotHasKey('SubclassDatabaseField_Lazy', $subteam1Lazy->toMap());
$this->assertArrayHasKey('SubclassDatabaseField', $subteam1Lazy->toMap());
}
public function testLazyLoadedFieldsOnVersionedRecords()
{
// Save another record, sanity check that we're getting the right one
$obj2 = new Subclass();
$obj2->Name = "test2";
$obj2->ExtraField = "foo2";
$obj2->write();
// Save the actual inspected record
$obj1 = new Subclass();
$obj1->Name = "test";
$obj1->ExtraField = "foo";
$obj1->write();
$version1 = $obj1->Version;
$obj1->Name = "test2";
$obj1->ExtraField = "baz";
$obj1->write();
$version2 = $obj1->Version;
$reloaded = Versioned::get_version(VersionedTest\Subclass::class, $obj1->ID, $version1);
$this->assertEquals($reloaded->Name, 'test');
$this->assertEquals($reloaded->ExtraField, 'foo');
$reloaded = Versioned::get_version(VersionedTest\Subclass::class, $obj1->ID, $version2);
$this->assertEquals($reloaded->Name, 'test2');
$this->assertEquals($reloaded->ExtraField, 'baz');
$reloaded = Versioned::get_latest_version(VersionedTest\Subclass::class, $obj1->ID);
$this->assertEquals($reloaded->Version, $version2);
$this->assertEquals($reloaded->Name, 'test2');
$this->assertEquals($reloaded->ExtraField, 'baz');
$allVersions = Versioned::get_all_versions(VersionedTest\Subclass::class, $obj1->ID);
$this->assertEquals(2, $allVersions->count());
$this->assertEquals($allVersions->first()->Version, $version1);
$this->assertEquals($allVersions->first()->Name, 'test');
$this->assertEquals($allVersions->first()->ExtraField, 'foo');
$this->assertEquals($allVersions->last()->Version, $version2);
$this->assertEquals($allVersions->last()->Name, 'test2');
$this->assertEquals($allVersions->last()->ExtraField, 'baz');
$obj1->delete();
}
public function testLazyLoadedFieldsDoNotReferenceVersionsTable()
{
// Save another record, sanity check that we're getting the right one
$obj2 = new Subclass();
$obj2->Name = "test2";
$obj2->ExtraField = "foo2";
$obj2->write();
$obj1 = new VersionedSubObject();
$obj1->PageName = "old-value";
$obj1->ExtraField = "old-value";
$obj1ID = $obj1->write();
$obj1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$obj1 = VersionedSubObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching base class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching base class"
);
$obj1 = VersionedObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching sub class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching sub class"
);
// Force inconsistent state to test behaviour (shouldn't select from *_versions)
DB::query(
sprintf(
"UPDATE \"VersionedLazy_DataObject_Versions\" SET \"PageName\" = 'versioned-value' " .
"WHERE \"RecordID\" = %d",
$obj1ID
)
);
DB::query(
sprintf(
"UPDATE \"VersionedLazySub_DataObject_Versions\" SET \"ExtraField\" = 'versioned-value' " .
"WHERE \"RecordID\" = %d",
$obj1ID
)
);
$obj1 = VersionedSubObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching base class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching base class"
);
$obj1 = VersionedObject::get()->byID($obj1ID);
$this->assertEquals(
'old-value',
$obj1->PageName,
"Correct value on base table when fetching sub class"
);
$this->assertEquals(
'old-value',
$obj1->ExtraField,
"Correct value on sub table when fetching sub class"
);
// Update live table only to test behaviour (shouldn't select from *_versions or stage)
DB::query(
sprintf(
'UPDATE "VersionedLazy_DataObject_Live" SET "PageName" = \'live-value\' WHERE "ID" = %d',
$obj1ID
)
);
DB::query(
sprintf(
'UPDATE "VersionedLazySub_DataObject_Live" SET "ExtraField" = \'live-value\' WHERE "ID" = %d',
$obj1ID
)
);
Versioned::set_stage(Versioned::LIVE);
$obj1 = VersionedObject::get()->byID($obj1ID);
$this->assertEquals(
'live-value',
$obj1->PageName,
"Correct value from base table when fetching base class on live stage"
);
$this->assertEquals(
'live-value',
$obj1->ExtraField,
"Correct value from sub table when fetching base class on live stage"
);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectLazyLoadingTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class VersionedObject extends DataObject implements TestOnly
{
private static $table_name = 'VersionedLazy_DataObject';
private static $db = [
"PageName" => "Varchar"
];
private static $extensions = [
Versioned::class
];
}

View File

@ -1,20 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\DataObjectLazyLoadingTest;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class VersionedSubObject extends VersionedObject
{
private static $table_name = 'VersionedLazySub_DataObject';
private static $db = array(
"ExtraField" => "Varchar",
);
private static $extensions = array(
Versioned::class
);
}

View File

@ -3,14 +3,13 @@
namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
class HierarchyTest extends SapphireTest
{
protected static $fixture_file = 'HierarchyTest.yml';
protected $extraDataObjects = array(
@ -19,6 +18,25 @@ class HierarchyTest extends SapphireTest
HierarchyTest\HideTestSubObject::class,
);
protected function getExtraDataObjects()
{
// Prevent setup breaking if versioned module absent
if (class_exists(Versioned::class)) {
return parent::getExtraDataObjects();
}
return [];
}
public function setUp()
{
parent::setUp();
// Note: Soft support for versioned module optionality
if (!class_exists(Versioned::class)) {
$this->markTestSkipped('HierarchyTest requires the Versioned extension');
}
}
/**
* Test the Hierarchy prevents infinite loops.
*/

View File

@ -5,7 +5,7 @@ namespace SilverStripe\ORM\Tests\HierarchyTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* @mixin Versioned

View File

@ -3,7 +3,7 @@
namespace SilverStripe\ORM\Tests\HierarchyTest;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* @mixin Versioned

View File

@ -5,7 +5,7 @@ namespace SilverStripe\ORM\Tests\HierarchyTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Hierarchy\Hierarchy;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
/**
* @mixin Versioned

View File

@ -5,7 +5,6 @@ namespace SilverStripe\ORM\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Versioning\Versioned;
use InvalidArgumentException;
class ManyManyThroughListTest extends SapphireTest
@ -15,10 +14,7 @@ class ManyManyThroughListTest extends SapphireTest
protected $extraDataObjects = [
ManyManyThroughListTest\Item::class,
ManyManyThroughListTest\JoinObject::class,
ManyManyThroughListTest\TestObject::class,
ManyManyThroughListTest\VersionedItem::class,
ManyManyThroughListTest\VersionedJoinObject::class,
ManyManyThroughListTest\VersionedObject::class,
ManyManyThroughListTest\TestObject::class
];
protected function setUp()
@ -35,9 +31,7 @@ class ManyManyThroughListTest extends SapphireTest
public function testSelectJoin()
{
/**
* @var \SilverStripe\ORM\Tests\ManyManyThroughListTest\ManyManyThroughListTest_Object $parent
*/
/** @var ManyManyThroughListTest\TestObject $parent */
$parent = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$this->assertDOSEquals(
[
@ -102,9 +96,7 @@ class ManyManyThroughListTest extends SapphireTest
public function testAdd()
{
/**
* @var \SilverStripe\ORM\Tests\ManyManyThroughListTest\ManyManyThroughListTest_Object $parent
*/
/** @var ManyManyThroughListTest\TestObject $parent */
$parent = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$newItem = new ManyManyThroughListTest\Item();
$newItem->Title = 'my new item';
@ -128,9 +120,7 @@ class ManyManyThroughListTest extends SapphireTest
public function testRemove()
{
/**
* @var \SilverStripe\ORM\Tests\ManyManyThroughListTest\ManyManyThroughListTest_Object $parent
*/
/** @var ManyManyThroughListTest\TestObject $parent */
$parent = $this->objFromFixture(ManyManyThroughListTest\TestObject::class, 'parent1');
$this->assertDOSEquals(
[
@ -147,73 +137,6 @@ class ManyManyThroughListTest extends SapphireTest
);
}
public function testPublishing()
{
/**
* @var \SilverStripe\ORM\Tests\ManyManyThroughListTest\ManyManyThroughListTest_VersionedObject $draftParent
*/
$draftParent = $this->objFromFixture(ManyManyThroughListTest\VersionedObject::class, 'parent1');
$draftParent->publishRecursive();
// Modify draft stage
$item1 = $draftParent->Items()->filter(['Title' => 'versioned item 1'])->first();
$item1->Title = 'new versioned item 1';
$item1->getJoin()->Title = 'new versioned join 1';
$item1->write(false, false, false, true); // Write joined components
$draftParent->Title = 'new versioned title';
$draftParent->write();
// Check owned objects on stage
$draftOwnedObjects = $draftParent->findOwned(true);
$this->assertDOSEquals(
[
['Title' => 'new versioned join 1'],
['Title' => 'versioned join 2'],
['Title' => 'new versioned item 1'],
['Title' => 'versioned item 2'],
],
$draftOwnedObjects
);
// Check live record is still old values
// This tests that both the join table and many_many tables
// inherit the necessary query parameters from the parent object.
/**
* @var \SilverStripe\ORM\Tests\ManyManyThroughListTest\ManyManyThroughListTest_VersionedObject $liveParent
*/
$liveParent = Versioned::get_by_stage(
ManyManyThroughListTest\VersionedObject::class,
Versioned::LIVE
)->byID($draftParent->ID);
$liveOwnedObjects = $liveParent->findOwned(true);
$this->assertDOSEquals(
[
['Title' => 'versioned join 1'],
['Title' => 'versioned join 2'],
['Title' => 'versioned item 1'],
['Title' => 'versioned item 2'],
],
$liveOwnedObjects
);
// Publish draft changes
$draftParent->publishRecursive();
$liveParent = Versioned::get_by_stage(
ManyManyThroughListTest\VersionedObject::class,
Versioned::LIVE
)->byID($draftParent->ID);
$liveOwnedObjects = $liveParent->findOwned(true);
$this->assertDOSEquals(
[
['Title' => 'new versioned join 1'],
['Title' => 'versioned join 2'],
['Title' => 'new versioned item 1'],
['Title' => 'versioned item 2'],
],
$liveOwnedObjects
);
}
/**
* Test validation
*/

View File

@ -17,20 +17,3 @@ SilverStripe\ORM\Tests\ManyManyThroughListTest\JoinObject:
Sort: 2
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\TestObject.parent1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\Item.child2
SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedObject:
parent1:
Title: 'versioned object'
SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedItem:
child1:
Title: 'versioned item 1'
child2:
Title: 'versioned item 2'
SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedJoinObject:
join1:
Title: 'versioned join 1'
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedObject.parent1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedItem.child1
join2:
Title: 'versioned join 2'
Parent: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedObject.parent1
Child: =>SilverStripe\ORM\Tests\ManyManyThroughListTest\VersionedItem.child2

View File

@ -1,30 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @property string $Title
* @method ManyManyThroughList Objects()
* @mixin Versioned
*/
class VersionedItem extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_VersionedItem';
private static $db = [
'Title' => 'Varchar'
];
private static $extensions = [
Versioned::class
];
private static $belongs_many_many = [
'Objects' => 'SilverStripe\\ORM\\Tests\\ManyManyThroughListTest\\VersionedObject.Items'
];
}

View File

@ -1,31 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @property string $Title
* @method VersionedObject Parent()
* @method VersionedItem Child()
* @mixin Versioned
*/
class VersionedJoinObject extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_VersionedJoinObject';
private static $db = [
'Title' => 'Varchar'
];
private static $extensions = [
Versioned::class
];
private static $has_one = [
'Parent' => VersionedObject::class,
'Child' => VersionedItem::class,
];
}

View File

@ -1,40 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\ManyManyThroughListTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\ManyManyThroughList;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Basic parent object
*
* @property string $Title
* @method ManyManyThroughList Items()
* @mixin Versioned
*/
class VersionedObject extends DataObject implements TestOnly
{
private static $table_name = 'ManyManyThroughListTest_VersionedObject';
private static $db = [
'Title' => 'Varchar',
];
private static $extensions = [
Versioned::class,
];
private static $owns = [
'Items', // Should automatically own both mapping and child records
];
private static $many_many = [
'Items' => [
'through' => VersionedJoinObject::class,
'from' => 'Parent',
'to' => 'Child',
],
];
}

View File

@ -1,3 +0,0 @@
SilverStripe\ORM\Tests\VersionableExtensionsTest\TestObject:
object:
Title: "Test"

View File

@ -1,30 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\DB;
use SilverStripe\Dev\SapphireTest;
class VersionableExtensionsTest extends SapphireTest
{
protected static $fixture_file = 'VersionableExtensionsFixtures.yml';
protected $extraDataObjects = array(
VersionableExtensionsTest\TestObject::class,
);
public function testTablesAreCreated()
{
$tables = DB::table_list();
$check = array(
'versionableextensionstest_dataobject_test1_live', 'versionableextensionstest_dataobject_test2_live', 'versionableextensionstest_dataobject_test3_live',
'versionableextensionstest_dataobject_test1_versions', 'versionableextensionstest_dataobject_test2_versions', 'versionableextensionstest_dataobject_test3_versions'
);
// Check that the right tables exist
foreach ($check as $tableName) {
$this->assertContains($tableName, array_keys($tables), 'Contains table: '.$tableName);
}
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionableExtensionsTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataExtension;
use SilverStripe\ORM\Versioning\VersionableExtension;
class TestExtension extends DataExtension implements VersionableExtension, TestOnly
{
public function isVersionedTable($table)
{
return true;
}
/**
* Update fields and indexes for the versonable suffix table
*
* @param string $suffix Table suffix being built
* @param array $fields List of fields in this model
* @param array $indexes List of indexes in this model
*/
public function updateVersionableFields($suffix, &$fields, &$indexes)
{
$indexes['ExtraField'] = true;
$fields['ExtraField'] = 'Varchar()';
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionableExtensionsTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'VersionableExtensionsTest_DataObject';
private static $db = array(
'Title' => 'Varchar'
);
private static $extensions = [
Versioned::class,
TestExtension::class,
];
private static $versionableExtensions = [
TestExtension::class => ['test1', 'test2', 'test3']
];
}

View File

@ -1,646 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\ORM\Versioning\ChangeSet;
use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Dev\SapphireTest;
use DateTime;
/**
* Tests ownership API of versioned DataObjects
*/
class VersionedOwnershipTest extends SapphireTest
{
protected $extraDataObjects = array(
VersionedOwnershipTest\TestObject::class,
VersionedOwnershipTest\Subclass::class,
VersionedOwnershipTest\Related::class,
VersionedOwnershipTest\Attachment::class,
VersionedOwnershipTest\RelatedMany::class,
VersionedOwnershipTest\TestPage::class,
VersionedOwnershipTest\Banner::class,
VersionedOwnershipTest\Image::class,
VersionedOwnershipTest\CustomRelation::class,
);
protected static $fixture_file = 'VersionedOwnershipTest.yml';
protected function setUp()
{
parent::setUp();
Versioned::set_stage(Versioned::DRAFT);
// 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->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
}
}
}
}
/**
* Virtual "sleep" that doesn't actually slow execution, only advances DBDateTime::now()
*
* @param int $minutes
*/
protected function sleep($minutes)
{
$now = DBDatetime::now();
$date = DateTime::createFromFormat('Y-m-d H:i:s', $now->getValue());
$date->modify("+{$minutes} minutes");
DBDatetime::set_mock_now($date->format('Y-m-d H:i:s'));
}
/**
* Test basic findOwned() in stage mode
*/
public function testFindOwned()
{
/** @var VersionedOwnershipTest\Subclass $subclass1 */
$subclass1 = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass1_published');
$this->assertDOSEquals(
[
['Title' => 'Related 1'],
['Title' => 'Attachment 1'],
['Title' => 'Attachment 2'],
['Title' => 'Attachment 5'],
['Title' => 'Related Many 1'],
['Title' => 'Related Many 2'],
['Title' => 'Related Many 3'],
],
$subclass1->findOwned()
);
// Non-recursive search
$this->assertDOSEquals(
[
['Title' => 'Related 1'],
['Title' => 'Related Many 1'],
['Title' => 'Related Many 2'],
['Title' => 'Related Many 3'],
],
$subclass1->findOwned(false)
);
/** @var VersionedOwnershipTest\Subclass $subclass2 */
$subclass2 = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass2_published');
$this->assertDOSEquals(
[
['Title' => 'Related 2'],
['Title' => 'Attachment 3'],
['Title' => 'Attachment 4'],
['Title' => 'Attachment 5'],
['Title' => 'Related Many 4'],
],
$subclass2->findOwned()
);
// Non-recursive search
$this->assertDOSEquals(
[
['Title' => 'Related 2'],
['Title' => 'Related Many 4'],
],
$subclass2->findOwned(false)
);
/** @var VersionedOwnershipTest\Related $related1 */
$related1 = $this->objFromFixture(VersionedOwnershipTest\Related::class, 'related1');
$this->assertDOSEquals(
[
['Title' => 'Attachment 1'],
['Title' => 'Attachment 2'],
['Title' => 'Attachment 5'],
],
$related1->findOwned()
);
/** @var VersionedOwnershipTest\Related $related2 */
$related2 = $this->objFromFixture(VersionedOwnershipTest\Related::class, '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::class, '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::class, '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::class, '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::class, 'related2_published');
$related2->Title .= ' Modified';
$related2->write();
$attachment3 = $this->objFromFixture(VersionedOwnershipTest\Attachment::class, 'attachment3_published');
$attachment3->Title .= ' Modified';
$attachment3->write();
$attachment4 = $this->objFromFixture(VersionedOwnershipTest\Attachment::class, 'attachment4_published');
$attachment4->delete();
$subclass2ID = $this->idFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass2_published');
// Check that stage record is ok
/** @var VersionedOwnershipTest\Subclass $subclass2Stage */
$subclass2Stage = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, 'Stage')->byID($subclass2ID);
$this->assertDOSEquals(
[
['Title' => 'Related 2 Modified'],
['Title' => 'Attachment 3 Modified'],
['Title' => 'Attachment 5'],
['Title' => 'Related Many 4'],
],
$subclass2Stage->findOwned()
);
// Non-recursive
$this->assertDOSEquals(
[
['Title' => 'Related 2 Modified'],
['Title' => 'Related Many 4'],
],
$subclass2Stage->findOwned(false)
);
// Live records are unchanged
/** @var VersionedOwnershipTest\Subclass $subclass2Live */
$subclass2Live = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, 'Live')->byID($subclass2ID);
$this->assertDOSEquals(
[
['Title' => 'Related 2'],
['Title' => 'Attachment 3'],
['Title' => 'Attachment 4'],
['Title' => 'Attachment 5'],
['Title' => 'Related Many 4'],
],
$subclass2Live->findOwned()
);
// Test non-recursive
$this->assertDOSEquals(
[
['Title' => 'Related 2'],
['Title' => 'Related Many 4'],
],
$subclass2Live->findOwned(false)
);
}
/**
* Test that objects are correctly published recursively
*/
public function testRecursivePublish()
{
/** @var VersionedOwnershipTest\Subclass $parent */
$parent = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass1_published');
$parentID = $parent->ID;
$banner1 = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany1_published');
$banner2 = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany2_published');
$banner2ID = $banner2->ID;
// Modify, Add, and Delete banners on stage
$banner1->Title = 'Renamed Banner 1';
$banner1->write();
$banner2->delete();
$banner4 = new VersionedOwnershipTest\RelatedMany();
$banner4->Title = 'New Banner';
$parent->Banners()->add($banner4);
// Check state of objects before publish
$oldLiveBanners = [
['Title' => 'Related Many 1'],
['Title' => 'Related Many 2'], // Will be unlinked (but not deleted)
// `Related Many 3` isn't published
];
$newBanners = [
['Title' => 'Renamed Banner 1'], // Renamed
['Title' => 'Related Many 3'], // Published without changes
['Title' => 'New Banner'], // Created
];
$parentDraft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($parentID);
$this->assertDOSEquals($newBanners, $parentDraft->Banners());
$parentLive = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::LIVE)
->byID($parentID);
$this->assertDOSEquals($oldLiveBanners, $parentLive->Banners());
// On publishing of owner, all children should now be updated
$now = DBDatetime::now();
DBDatetime::set_mock_now($now); // Lock 'now' to predictable time
$parent->publishRecursive();
// Now check each object has the correct state
$parentDraft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($parentID);
$this->assertDOSEquals($newBanners, $parentDraft->Banners());
$parentLive = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::LIVE)
->byID($parentID);
$this->assertDOSEquals($newBanners, $parentLive->Banners());
// Check that the deleted banner hasn't actually been deleted from the live stage,
// but in fact has been unlinked.
$banner2Live = Versioned::get_by_stage(VersionedOwnershipTest\RelatedMany::class, Versioned::LIVE)
->byID($banner2ID);
$this->assertEmpty($banner2Live->PageID);
// Test that a changeset was created
/** @var ChangeSet $changeset */
$changeset = ChangeSet::get()->sort('"ChangeSet"."ID" DESC')->first();
$this->assertNotEmpty($changeset);
// Test that this changeset is inferred
$this->assertTrue((bool)$changeset->IsInferred);
$this->assertEquals(
"Generated by publish of 'Subclass 1' at ".$now->Nice(),
$changeset->getTitle()
);
// Test that this changeset contains all items
$this->assertDOSContains(
[
[
'ObjectID' => $parent->ID,
'ObjectClass' => $parent->baseClass(),
'Added' => ChangeSetItem::EXPLICITLY
],
[
'ObjectID' => $banner1->ID,
'ObjectClass' => $banner1->baseClass(),
'Added' => ChangeSetItem::IMPLICITLY
],
[
'ObjectID' => $banner4->ID,
'ObjectClass' => $banner4->baseClass(),
'Added' => ChangeSetItem::IMPLICITLY
]
],
$changeset->Changes()
);
// Objects that are unlinked should not need to be a part of the changeset
$this->assertNotDOSContains(
[[ 'ObjectID' => $banner2ID, 'ObjectClass' => $banner2->baseClass() ]],
$changeset->Changes()
);
}
/**
* Test that owning objects get unpublished as needed
*/
public function testRecursiveUnpublish()
{
// Unsaved objects can't be unpublished
$unsaved = new VersionedOwnershipTest\Subclass();
$this->assertFalse($unsaved->doUnpublish());
// Draft-only objects can't be unpublished
/** @var VersionedOwnershipTest\RelatedMany $banner3Unpublished */
$banner3Unpublished = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany3');
$this->assertFalse($banner3Unpublished->doUnpublish());
// First test: mid-level unpublish; We expect that owners should be unpublished, but not
// owned objects, nor other siblings shared by the same owner.
$related2 = $this->objFromFixture(VersionedOwnershipTest\Related::class, 'related2_published');
/** @var VersionedOwnershipTest\Attachment $attachment3 */
$attachment3 = $this->objFromFixture(VersionedOwnershipTest\Attachment::class, 'attachment3_published');
/** @var VersionedOwnershipTest\RelatedMany $relatedMany4 */
$relatedMany4 = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany4_published');
/** @var VersionedOwnershipTest\Related $related2 */
$this->assertTrue($related2->doUnpublish());
$subclass2 = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass2_published');
/** @var VersionedOwnershipTest\Subclass $subclass2 */
$this->assertFalse($subclass2->isPublished()); // Owner IS unpublished
$this->assertTrue($attachment3->isPublished()); // Owned object is NOT unpublished
$this->assertTrue($relatedMany4->isPublished()); // Owned object by owner is NOT unpublished
// Second test: multi-level unpublish should recursively cascade down all owning objects
// Publish related2 again
$subclass2->publishRecursive();
$this->assertTrue($subclass2->isPublished());
$this->assertTrue($related2->isPublished());
$this->assertTrue($attachment3->isPublished());
// Unpublish leaf node
$this->assertTrue($attachment3->doUnpublish());
// Now all owning objects (only) are unpublished
$this->assertFalse($attachment3->isPublished()); // Unpublished because we just unpublished it
$this->assertFalse($related2->isPublished()); // Unpublished because it owns attachment3
$this->assertFalse($subclass2->isPublished()); // Unpublished ecause it owns related2
$this->assertTrue($relatedMany4->isPublished()); // Stays live because recursion only affects owners not owned.
}
public function testRecursiveArchive()
{
// When archiving an object, any published owners should be unpublished at the same time
// but NOT achived
/** @var VersionedOwnershipTest\Attachment $attachment3 */
$attachment3 = $this->objFromFixture(VersionedOwnershipTest\Attachment::class, 'attachment3_published');
$attachment3ID = $attachment3->ID;
$this->assertTrue($attachment3->doArchive());
// This object is on neither stage nor live
$stageAttachment = Versioned::get_by_stage(VersionedOwnershipTest\Attachment::class, Versioned::DRAFT)
->byID($attachment3ID);
$liveAttachment = Versioned::get_by_stage(VersionedOwnershipTest\Attachment::class, Versioned::LIVE)
->byID($attachment3ID);
$this->assertEmpty($stageAttachment);
$this->assertEmpty($liveAttachment);
// Owning object is unpublished only
/** @var VersionedOwnershipTest\Related $stageOwner */
$stageOwner = $this->objFromFixture(VersionedOwnershipTest\Related::class, 'related2_published');
$this->assertTrue($stageOwner->isOnDraft());
$this->assertFalse($stageOwner->isPublished());
// Bottom level owning object is also unpublished
/** @var VersionedOwnershipTest\Subclass $stageTopOwner */
$stageTopOwner = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass2_published');
$this->assertTrue($stageTopOwner->isOnDraft());
$this->assertFalse($stageTopOwner->isPublished());
}
public function testRecursiveRevertToLive()
{
/** @var VersionedOwnershipTest\Subclass $parent */
$parent = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass1_published');
$parentID = $parent->ID;
$banner1 = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany1_published');
$banner2 = $this->objFromFixture(VersionedOwnershipTest\RelatedMany::class, 'relatedmany2_published');
$banner2ID = $banner2->ID;
// Modify, Add, and Delete banners on stage
$banner1->Title = 'Renamed Banner 1';
$banner1->write();
$banner2->delete();
$banner4 = new VersionedOwnershipTest\RelatedMany();
$banner4->Title = 'New Banner';
$banner4->write();
$parent->Banners()->add($banner4);
// Check state of objects before publish
$liveBanners = [
['Title' => 'Related Many 1'],
['Title' => 'Related Many 2'],
];
$modifiedBanners = [
['Title' => 'Renamed Banner 1'], // Renamed
['Title' => 'Related Many 3'], // Published without changes
['Title' => 'New Banner'], // Created
];
$parentDraft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($parentID);
$this->assertDOSEquals($modifiedBanners, $parentDraft->Banners());
$parentLive = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::LIVE)
->byID($parentID);
$this->assertDOSEquals($liveBanners, $parentLive->Banners());
// When reverting parent, all records should be put back on stage
$this->assertTrue($parent->doRevertToLive());
// Now check each object has the correct state
$parentDraft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($parentID);
$this->assertDOSEquals($liveBanners, $parentDraft->Banners());
$parentLive = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::LIVE)
->byID($parentID);
$this->assertDOSEquals($liveBanners, $parentLive->Banners());
// Check that the newly created banner, even though it still exist, has been
// unlinked from the reverted draft record
/** @var VersionedOwnershipTest\RelatedMany $banner4Draft */
$banner4Draft = Versioned::get_by_stage(VersionedOwnershipTest\RelatedMany::class, Versioned::DRAFT)
->byID($banner4->ID);
$this->assertTrue($banner4Draft->isOnDraft());
$this->assertFalse($banner4Draft->isPublished());
$this->assertEmpty($banner4Draft->PageID);
}
/**
* Test that rolling back to a single version works recursively
*/
public function testRecursiveRollback()
{
/** @var VersionedOwnershipTest\Subclass $subclass2 */
$this->sleep(1);
$subclass2 = $this->objFromFixture(VersionedOwnershipTest\Subclass::class, 'subclass2_published');
// Create a few new versions
$versions = [];
for ($version = 1; $version <= 3; $version++) {
// Write owned objects
$this->sleep(1);
foreach ($subclass2->findOwned(true) as $obj) {
$obj->Title .= " - v{$version}";
$obj->write();
}
// Write parent
$this->sleep(1);
$subclass2->Title .= " - v{$version}";
$subclass2->write();
$versions[$version] = $subclass2->Version;
}
// Check reverting to first version
$this->sleep(1);
$subclass2->doRollbackTo($versions[1]);
/** @var VersionedOwnershipTest\Subclass $subclass2Draft */
$subclass2Draft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($subclass2->ID);
$this->assertEquals('Subclass 2 - v1', $subclass2Draft->Title);
$this->assertDOSEquals(
[
['Title' => 'Related 2 - v1'],
['Title' => 'Attachment 3 - v1'],
['Title' => 'Attachment 4 - v1'],
['Title' => 'Attachment 5 - v1'],
['Title' => 'Related Many 4 - v1'],
],
$subclass2Draft->findOwned(true)
);
// Check rolling forward to a later version
$this->sleep(1);
$subclass2->doRollbackTo($versions[3]);
/** @var VersionedOwnershipTest\Subclass $subclass2Draft */
$subclass2Draft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($subclass2->ID);
$this->assertEquals('Subclass 2 - v1 - v2 - v3', $subclass2Draft->Title);
$this->assertDOSEquals(
[
['Title' => 'Related 2 - v1 - v2 - v3'],
['Title' => 'Attachment 3 - v1 - v2 - v3'],
['Title' => 'Attachment 4 - v1 - v2 - v3'],
['Title' => 'Attachment 5 - v1 - v2 - v3'],
['Title' => 'Related Many 4 - v1 - v2 - v3'],
],
$subclass2Draft->findOwned(true)
);
// And rolling back one version
$this->sleep(1);
$subclass2->doRollbackTo($versions[2]);
/** @var VersionedOwnershipTest\Subclass $subclass2Draft */
$subclass2Draft = Versioned::get_by_stage(VersionedOwnershipTest\Subclass::class, Versioned::DRAFT)
->byID($subclass2->ID);
$this->assertEquals('Subclass 2 - v1 - v2', $subclass2Draft->Title);
$this->assertDOSEquals(
[
['Title' => 'Related 2 - v1 - v2'],
['Title' => 'Attachment 3 - v1 - v2'],
['Title' => 'Attachment 4 - v1 - v2'],
['Title' => 'Attachment 5 - v1 - v2'],
['Title' => 'Related Many 4 - v1 - v2'],
],
$subclass2Draft->findOwned(true)
);
}
/**
* Test that you can find owners without owned_by being defined explicitly
*/
public function testInferedOwners()
{
// Make sure findOwned() works
/** @var VersionedOwnershipTest\TestPage $page1 */
$page1 = $this->objFromFixture(VersionedOwnershipTest\TestPage::class, 'page1_published');
/** @var VersionedOwnershipTest\TestPage $page2 */
$page2 = $this->objFromFixture(VersionedOwnershipTest\TestPage::class, 'page2_published');
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Image 1'],
['Title' => 'Custom 1'],
],
$page1->findOwned()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 2'],
['Title' => 'Banner 3'],
['Title' => 'Image 1'],
['Title' => 'Image 2'],
['Title' => 'Custom 2'],
],
$page2->findOwned()
);
// Check that findOwners works
/** @var VersionedOwnershipTest\Image $image1 */
$image1 = $this->objFromFixture(VersionedOwnershipTest\Image::class, 'image1_published');
/** @var VersionedOwnershipTest\Image $image2 */
$image2 = $this->objFromFixture(VersionedOwnershipTest\Image::class, 'image2_published');
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Banner 2'],
['Title' => 'Page 1'],
['Title' => 'Page 2'],
],
$image1->findOwners()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 1'],
['Title' => 'Banner 2'],
],
$image1->findOwners(false)
);
$this->assertDOSEquals(
[
['Title' => 'Banner 3'],
['Title' => 'Page 2'],
],
$image2->findOwners()
);
$this->assertDOSEquals(
[
['Title' => 'Banner 3'],
],
$image2->findOwners(false)
);
// Test custom relation can findOwners()
/** @var VersionedOwnershipTest\CustomRelation $custom1 */
$custom1 = $this->objFromFixture(VersionedOwnershipTest\CustomRelation::class, 'custom1_published');
$this->assertDOSEquals(
[['Title' => 'Page 1']],
$custom1->findOwners()
);
}
}

View File

@ -1,84 +0,0 @@
SilverStripe\ORM\Tests\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'
SilverStripe\ORM\Tests\VersionedOwnershipTest\Related:
related1:
Title: 'Related 1'
Attachments:
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment1
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment2
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment5_published
related2_published:
Title: 'Related 2'
Attachments:
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment3_published
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment4_published
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Attachment.attachment5_published
SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass:
subclass1_published:
Title: 'Subclass 1'
Related: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Related.related1
subclass2_published:
Title: 'Subclass 2'
Related: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Related.related2_published
SilverStripe\ORM\Tests\VersionedOwnershipTest\RelatedMany:
relatedmany1_published:
Title: 'Related Many 1'
Page: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass.subclass1_published
relatedmany2_published:
Title: 'Related Many 2'
Page: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass.subclass1_published
relatedmany3:
Title: 'Related Many 3'
Page: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass.subclass1_published
relatedmany4_published:
Title: 'Related Many 4'
Page: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Subclass.subclass2_published
SilverStripe\ORM\Tests\VersionedOwnershipTest\TestObject:
object1:
Title: 'Object 1'
SilverStripe\ORM\Tests\VersionedOwnershipTest\Image:
image1_published:
Title: 'Image 1'
image2_published:
Title: 'Image 2'
SilverStripe\ORM\Tests\VersionedOwnershipTest\Banner:
banner1_published:
Title: 'Banner 1'
Image: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Image.image1_published
banner2_published:
Title: 'Banner 2'
Image: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Image.image1_published
banner3_published:
Title: 'Banner 3'
Image: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Image.image2_published
SilverStripe\ORM\Tests\VersionedOwnershipTest\TestPage:
page1_published:
Title: 'Page 1'
Banners: =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Banner.banner1_published
page2_published:
Title: 'Page 2'
Banners:
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Banner.banner2_published
- =>SilverStripe\ORM\Tests\VersionedOwnershipTest\Banner.banner3_published
SilverStripe\ORM\Tests\VersionedOwnershipTest\CustomRelation:
custom1_published:
Title: 'Custom 1'
custom2_published:
Title: 'Custom 2'

View File

@ -1,32 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class Attachment extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Attachment';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $belongs_many_many = array(
'AttachedTo' => 'SilverStripe\\ORM\\Tests\\VersionedOwnershipTest\\Related.Attachments'
);
private static $owned_by = array(
'AttachedTo'
);
}

View File

@ -1,34 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Banner which doesn't declare its belongs_many_many, but owns an Image
*
* @mixin Versioned
*/
class Banner extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Banner';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $has_one = array(
'Image' => VersionedOwnershipTest\Image::class,
);
private static $owns = array(
'Image',
);
}

View File

@ -1,41 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Object which is owned via a custom PHP method rather than DB relation
*
* @mixin Versioned
*/
class CustomRelation extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_CustomRelation';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $owned_by = array(
'Pages'
);
/**
* All pages with the same number. E.g. 'Page 1' owns 'Custom 1'
*
* @return DataList
*/
public function Pages()
{
$title = str_replace('Custom', 'Page', $this->Title);
return TestPage::get()->filter('Title', $title);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Simple versioned dataobject
*
* @mixin Versioned
*/
class Image extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Image';
private static $db = array(
'Title' => 'Varchar(255)',
);
}

View File

@ -1,44 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Object which:
* - owned by has_many objects
* - owns many_many Objects
*
* @mixin Versioned
*/
class Related extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Related';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $has_many = array(
'Parents' => 'SilverStripe\\ORM\\Tests\\VersionedOwnershipTest\\Subclass.Related',
);
private static $owned_by = array(
'Parents',
);
private static $many_many = array(
// Note : Currently unversioned, take care
'Attachments' => Attachment::class,
);
private static $owns = array(
'Attachments',
);
}

View File

@ -1,33 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Object which is owned by a has_one object
*
* @mixin Versioned
*/
class RelatedMany extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_RelatedMany';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $has_one = array(
'Page' => Subclass::class,
);
private static $owned_by = array(
'Page'
);
}

View File

@ -1,33 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\Tests\VersionedOwnershipTest;
/**
* Object which:
* - owns a has_one object
* - owns has_many objects
*/
class Subclass extends TestObject implements TestOnly
{
private static $db = array(
'Description' => 'Text',
);
private static $has_one = array(
'Related' => Related::class,
);
private static $has_many = array(
'Banners' => RelatedMany::class,
);
private static $table_name = 'VersionedOwnershipTest_Subclass';
private static $owns = array(
'Related',
'Banners',
);
}

View File

@ -1,24 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class TestObject extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Object';
private static $db = array(
'Title' => 'Varchar(255)',
'Content' => 'Text',
);
}

View File

@ -1,46 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedOwnershipTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Page which owns a lits of banners
*
* @mixin Versioned
*/
class TestPage extends DataObject implements TestOnly
{
private static $extensions = array(
Versioned::class,
);
private static $table_name = 'VersionedOwnershipTest_Page';
private static $db = array(
'Title' => 'Varchar(255)',
);
private static $many_many = array(
'Banners' => Banner::class,
);
private static $owns = array(
'Banners',
'Custom'
);
/**
* All custom objects with the same number. E.g. 'Page 1' owns 'Custom 1'
*
* @return DataList
*/
public function Custom()
{
$title = str_replace('Page', 'Custom', $this->Title);
return CustomRelation::get()->filter('Title', $title);
}
}

View File

@ -1,1332 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\ORM\DataObjectSchema;
use SilverStripe\ORM\DB;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Core\Convert;
use SilverStripe\Core\ClassInfo;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Director;
use SilverStripe\Control\Session;
use DateTime;
class VersionedTest extends SapphireTest
{
protected static $fixture_file = 'VersionedTest.yml';
public static $extra_data_objects = [
VersionedTest\TestObject::class,
VersionedTest\Subclass::class,
VersionedTest\AnotherSubclass::class,
VersionedTest\RelatedWithoutversion::class,
VersionedTest\SingleStage::class,
VersionedTest\WithIndexes::class,
VersionedTest\PublicStage::class,
VersionedTest\PublicViaExtension::class,
VersionedTest\CustomTable::class,
];
protected function getExtraDataObjects()
{
return static::$extra_data_objects;
}
public function testUniqueIndexes()
{
$tableExpectations = array(
'VersionedTest_WithIndexes' =>
array('value' => true, 'message' => 'Unique indexes are unique in main table'),
'VersionedTest_WithIndexes_Versions' =>
array('value' => false, 'message' => 'Unique indexes are no longer unique in _Versions table'),
'VersionedTest_WithIndexes_Live' =>
array('value' => true, 'message' => 'Unique indexes are unique in _Live table'),
);
// Test each table's performance
foreach ($tableExpectations as $tableName => $expectation) {
$indexes = DB::get_schema()->indexList($tableName);
// Check for presence of all unique indexes
$indexColumns = array_map(
function ($index) {
return $index['value'];
},
$indexes
);
sort($indexColumns);
$expectedColumns = array('"UniqA"', '"UniqS"');
$this->assertEquals(
array_values($expectedColumns),
array_values(array_intersect($indexColumns, $expectedColumns)),
"$tableName has both indexes"
);
// Check unique -> non-unique conversion
foreach ($indexes as $indexKey => $indexSpec) {
if (in_array($indexSpec['value'], $expectedColumns)) {
$isUnique = $indexSpec['type'] === 'unique';
$this->assertEquals($isUnique, $expectation['value'], $expectation['message']);
}
}
}
}
public function testDeletingOrphanedVersions()
{
$obj = new VersionedTest\Subclass();
$obj->ExtraField = 'Foo'; // ensure that child version table gets written
$obj->write();
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$obj->ExtraField = 'Bar'; // ensure that child version table gets written
$obj->write();
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$versions = DB::query(
"SELECT COUNT(*) FROM \"VersionedTest_Subclass_Versions\""
. " WHERE \"RecordID\" = '$obj->ID'"
)->value();
$this->assertGreaterThan(0, $versions, 'At least 1 version exists in the history of the page');
// Force orphaning of all versions created earlier, only on parent record.
// The child versiones table should still have the correct relationship
DB::query("DELETE FROM \"VersionedTest_DataObject_Versions\" WHERE \"RecordID\" = $obj->ID");
// insert a record with no primary key (ID)
DB::query("INSERT INTO \"VersionedTest_DataObject_Versions\" (\"RecordID\") VALUES ($obj->ID)");
// run the script which should clean that up
$obj->augmentDatabase();
$versions = DB::query(
"SELECT COUNT(*) FROM \"VersionedTest_Subclass_Versions\""
. " WHERE \"RecordID\" = '$obj->ID'"
)->value();
$this->assertEquals(0, $versions, 'Orphaned versions on child tables are removed');
// test that it doesn't delete records that we need
$obj->write();
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$count = DB::query(
"SELECT COUNT(*) FROM \"VersionedTest_Subclass_Versions\""
. " WHERE \"RecordID\" = '$obj->ID'"
)->value();
$obj->augmentDatabase();
$count2 = DB::query(
"SELECT COUNT(*) FROM \"VersionedTest_Subclass_Versions\""
. " WHERE \"RecordID\" = '$obj->ID'"
)->value();
$this->assertEquals($count, $count2);
}
public function testCustomTable()
{
$obj = new VersionedTest\CustomTable();
$obj->Title = 'my object';
$obj->write();
$id = $obj->ID;
$obj->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$obj->Title = 'new title';
$obj->write();
$liveRecord = Versioned::get_by_stage(VersionedTest\CustomTable::class, Versioned::LIVE)->byID($id);
$draftRecord = Versioned::get_by_stage(VersionedTest\CustomTable::class, Versioned::DRAFT)->byID($id);
$this->assertEquals('my object', $liveRecord->Title);
$this->assertEquals('new title', $draftRecord->Title);
}
/**
* Test that publishing from invalid stage will throw exception
*/
public function testInvalidPublish()
{
$obj = new VersionedTest\Subclass();
$obj->ExtraField = 'Foo'; // ensure that child version table gets written
$obj->write();
$class = VersionedTest\TestObject::class;
$this->setExpectedException(
'InvalidArgumentException',
"Can't find {$class}#{$obj->ID} in stage Live"
);
// Fail publishing from live to stage
$obj->copyVersionToStage(Versioned::LIVE, Versioned::DRAFT);
}
public function testDuplicate()
{
$obj1 = new VersionedTest\Subclass();
$obj1->ExtraField = 'Foo';
$obj1->write(); // version 1
$obj1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$obj1->ExtraField = 'Foo2';
$obj1->write(); // version 2
// Make duplicate
$obj2 = $obj1->duplicate();
// Check records differ
$this->assertNotEquals($obj1->ID, $obj2->ID);
$this->assertEquals(2, $obj1->Version);
$this->assertEquals(1, $obj2->Version);
}
public function testForceChangeUpdatesVersion()
{
$obj = new VersionedTest\TestObject();
$obj->Name = "test";
$obj->write();
$oldVersion = $obj->Version;
$obj->forceChange();
$obj->write();
$this->assertTrue(
($obj->Version > $oldVersion),
"A object Version is increased when just calling forceChange() without any other changes"
);
}
/**
* Test Versioned::get_including_deleted()
*/
public function testGetIncludingDeleted()
{
// Get all ids of pages
$allPageIDs = DataObject::get(
VersionedTest\TestObject::class,
"\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"
)->column('ID');
// Modify a page, ensuring that the Version ID and Record ID will differ,
// and then subsequently delete it
$targetPage = $this->objFromFixture(VersionedTest\TestObject::class, 'page3');
$targetPage->Content = 'To be deleted';
$targetPage->write();
$targetPage->delete();
// Get all items, ignoring deleted
$remainingPages = DataObject::get(
VersionedTest\TestObject::class,
"\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"
);
// Check that page 3 has gone
$this->assertNotNull($remainingPages);
$this->assertEquals(array("Page 1", "Page 2", "Subclass Page 1"), $remainingPages->column('Title'));
// Get all including deleted
$allPages = Versioned::get_including_deleted(
VersionedTest\TestObject::class,
"\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"
);
// Check that page 3 is still there
$this->assertEquals(array("Page 1", "Page 2", "Page 3", "Subclass Page 1"), $allPages->column('Title'));
// Check that the returned pages have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
// Check that this still works if we switch to reading the other stage
Versioned::set_stage(Versioned::LIVE);
$allPages = Versioned::get_including_deleted(
VersionedTest\TestObject::class,
"\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"
);
$this->assertEquals(array("Page 1", "Page 2", "Page 3", "Subclass Page 1"), $allPages->column('Title'));
// Check that the returned pages still have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID'));
}
public function testVersionedFieldsAdded()
{
$obj = new VersionedTest\TestObject();
// Check that the Version column is added as a full-fledged column
$this->assertInstanceOf('SilverStripe\\ORM\\FieldType\\DBInt', $obj->dbObject('Version'));
$obj2 = new VersionedTest\Subclass();
// Check that the Version column is added as a full-fledged column
$this->assertInstanceOf('SilverStripe\\ORM\\FieldType\\DBInt', $obj2->dbObject('Version'));
}
public function testVersionedFieldsNotInCMS()
{
$obj = new VersionedTest\TestObject();
// the version field in cms causes issues with Versioned::augmentWrite()
$this->assertNull($obj->getCMSFields()->dataFieldByName('Version'));
}
public function testPublishCreateNewVersion()
{
/** @var VersionedTest\TestObject $page1 */
$page1 = $this->objFromFixture(VersionedTest\TestObject::class, 'page1');
$page1->Content = 'orig';
$page1->write();
$firstVersion = $page1->Version;
$page1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE, false);
$this->assertEquals(
$firstVersion,
$page1->Version,
'publish() with $createNewVersion=FALSE does not create a new version'
);
$page1->Content = 'changed';
$page1->write();
$secondVersion = $page1->Version;
$this->assertTrue($firstVersion < $secondVersion, 'write creates new version');
$page1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE, true);
$thirdVersion = Versioned::get_latest_version(VersionedTest\TestObject::class, $page1->ID)->Version;
$liveVersion = Versioned::get_versionnumber_by_stage(VersionedTest\TestObject::class, 'Live', $page1->ID);
$stageVersion = Versioned::get_versionnumber_by_stage(VersionedTest\TestObject::class, 'Stage', $page1->ID);
$this->assertTrue(
$secondVersion < $thirdVersion,
'publish() with $createNewVersion=TRUE creates a new version'
);
$this->assertEquals(
$liveVersion,
$thirdVersion,
'publish() with $createNewVersion=TRUE publishes to live'
);
$this->assertEquals(
$stageVersion,
$thirdVersion,
'publish() with $createNewVersion=TRUE also updates draft'
);
}
public function testRollbackTo()
{
$page1 = $this->objFromFixture(VersionedTest\AnotherSubclass::class, 'subclass1');
$page1->Content = 'orig';
$page1->write();
$page1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$origVersion = $page1->Version;
$page1->Content = 'changed';
$page1->write();
$page1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$changedVersion = $page1->Version;
$page1->doRollbackTo($origVersion);
$page1 = Versioned::get_one_by_stage(
VersionedTest\TestObject::class,
'Stage',
array(
'"VersionedTest_DataObject"."ID" = ?' => $page1->ID
)
);
$this->assertTrue($page1->Version == $changedVersion + 1, 'Create a new higher version number');
$this->assertEquals('orig', $page1->Content, 'Copies the content from the old version');
// check db entries
$version = DB::prepared_query(
"SELECT MAX(\"Version\") FROM \"VersionedTest_DataObject_Versions\" WHERE \"RecordID\" = ?",
array($page1->ID)
)->value();
$this->assertEquals($page1->Version, $version, 'Correct entry in VersionedTest_DataObject_Versions');
$version = DB::prepared_query(
"SELECT MAX(\"Version\") FROM \"VersionedTest_AnotherSubclass_Versions\" WHERE \"RecordID\" = ?",
array($page1->ID)
)->value();
$this->assertEquals($page1->Version, $version, 'Correct entry in VersionedTest_AnotherSubclass_Versions');
}
public function testDeleteFromStage()
{
$page1 = $this->objFromFixture(VersionedTest\TestObject::class, 'page1');
$pageID = $page1->ID;
$page1->Content = 'orig';
$page1->write();
$page1->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertEquals(
1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value()
);
$this->assertEquals(
1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value()
);
$page1->deleteFromStage('Live');
// Confirm that deleteFromStage() doesn't manipulate the original record
$this->assertEquals($pageID, $page1->ID);
$this->assertEquals(
1,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value()
);
$this->assertEquals(
0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value()
);
$page1->delete();
$this->assertEquals(0, $page1->ID);
$this->assertEquals(
0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject" WHERE "ID" = '.$pageID)->value()
);
$this->assertEquals(
0,
DB::query('SELECT COUNT(*) FROM "VersionedTest_DataObject_Live" WHERE "ID" = '.$pageID)->value()
);
}
public function testWritingNewToStage()
{
$origReadingMode = Versioned::get_reading_mode();
Versioned::set_stage(Versioned::DRAFT);
$page = new VersionedTest\TestObject();
$page->Title = "testWritingNewToStage";
$page->URLSegment = "testWritingNewToStage";
$page->write();
$live = Versioned::get_by_stage(
VersionedTest\TestObject::class,
'Live',
array(
'"VersionedTest_DataObject_Live"."ID"' => $page->ID
)
);
$this->assertEquals(0, $live->count());
$stage = Versioned::get_by_stage(
VersionedTest\TestObject::class,
'Stage',
array(
'"VersionedTest_DataObject"."ID"' => $page->ID
)
);
$this->assertEquals(1, $stage->count());
$this->assertEquals($stage->First()->Title, 'testWritingNewToStage');
Versioned::set_reading_mode($origReadingMode);
}
/**
* Writing a page to live should update both draft and live tables
*/
public function testWritingNewToLive()
{
$origReadingMode = Versioned::get_reading_mode();
Versioned::set_stage(Versioned::LIVE);
$page = new VersionedTest\TestObject();
$page->Title = "testWritingNewToLive";
$page->URLSegment = "testWritingNewToLive";
$page->write();
$live = Versioned::get_by_stage(
VersionedTest\TestObject::class,
'Live',
array(
'"VersionedTest_DataObject_Live"."ID"' => $page->ID
)
);
$this->assertEquals(1, $live->count());
$liveRecord = $live->First();
$this->assertEquals($liveRecord->Title, 'testWritingNewToLive');
$stage = Versioned::get_by_stage(
VersionedTest\TestObject::class,
'Stage',
array(
'"VersionedTest_DataObject"."ID"' => $page->ID
)
);
$this->assertEquals(1, $stage->count());
$stageRecord = $stage->first();
$this->assertEquals($stageRecord->Title, 'testWritingNewToLive');
// Both records have the same version
$this->assertEquals($liveRecord->Version, $stageRecord->Version);
Versioned::set_reading_mode($origReadingMode);
}
/**
* Tests DataObject::hasOwnTableDatabaseField
*/
public function testHasOwnTableDatabaseFieldWithVersioned()
{
$schema = DataObject::getSchema();
$this->assertNull(
$schema->fieldSpec(DataObject::class, 'Version', DataObjectSchema::UNINHERITED),
'Plain models have no version field.'
);
$this->assertEquals(
'Int',
$schema->fieldSpec(VersionedTest\TestObject::class, 'Version', DataObjectSchema::UNINHERITED),
'The versioned ext adds an Int version field.'
);
$this->assertNull(
$schema->fieldSpec(VersionedTest\Subclass::class, 'Version', DataObjectSchema::UNINHERITED),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertNull(
$schema->fieldSpec(VersionedTest\AnotherSubclass::class, 'Version', DataObjectSchema::UNINHERITED),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
'Varchar(255)',
$schema->fieldSpec(VersionedTest\UnversionedWithField::class, 'Version', DataObjectSchema::UNINHERITED),
'Models w/o Versioned can have their own Version field.'
);
}
/**
* Test that SQLSelect::queriedTables() applies the version-suffixes properly.
*/
public function testQueriedTables()
{
Versioned::set_stage(Versioned::LIVE);
$this->assertEquals(
array(
'VersionedTest_DataObject_Live',
'VersionedTest_Subclass_Live',
),
DataObject::get(VersionedTest\Subclass::class)->dataQuery()->query()->queriedTables()
);
}
/**
* Virtual "sleep" that doesn't actually slow execution, only advances DBDateTime::now()
*
* @param int $minutes
*/
protected function sleep($minutes)
{
$now = DBDatetime::now();
$date = DateTime::createFromFormat('Y-m-d H:i:s', $now->getValue());
$date->modify("+{$minutes} minutes");
DBDatetime::set_mock_now($date->format('Y-m-d H:i:s'));
}
/**
* Tests records selected by specific version
*/
public function testGetVersion()
{
// Create a few initial versions to ensure this version
// doesn't clash with child versions
$this->sleep(1);
/** @var VersionedTest\TestObject $page2 */
$page2 = $this->objFromFixture(VersionedTest\TestObject::class, 'page2');
$page2->Title = 'dummy1';
$page2->write();
$this->sleep(1);
$page2->Title = 'dummy2';
$page2->write();
$this->sleep(1);
$page2->Title = 'Page 2 - v1';
$page2->write();
$version1Date = $page2->LastEdited;
$version1 = $page2->Version;
// Create another version where this object and some
// child records have been modified
$this->sleep(1);
/** @var VersionedTest\TestObject $page2a */
$page2a = $this->objFromFixture(VersionedTest\TestObject::class, 'page2a');
$page2a->Title = 'Page 2a - v2';
$page2a->write();
$this->sleep(1);
$page2->Title = 'Page 2 - v2';
$page2->write();
$version2Date = $page2->LastEdited;
$version2 = $page2->Version;
$this->assertGreaterThan($version1, $version2);
$this->assertDOSEquals(
[
['Title' => 'Page 2a - v2'],
['Title' => 'Page 2b'],
],
$page2->Children()
);
// test selecting v1
/** @var VersionedTest\TestObject $page2v1 */
$page2v1 = Versioned::get_version(VersionedTest\TestObject::class, $page2->ID, $version1);
$this->assertEquals('Page 2 - v1', $page2v1->Title);
// When selecting v1, related records should by filtered by
// the modified date of that version
$archiveParms = [
'Versioned.mode' => 'archive',
'Versioned.date' => $version1Date
];
$this->assertEquals($archiveParms, $page2v1->getInheritableQueryParams());
$this->assertArraySubset($archiveParms, $page2v1->Children()->getQueryParams());
$this->assertDOSEquals(
[
['Title' => 'Page 2a'],
['Title' => 'Page 2b'],
],
$page2v1->Children()
);
// When selecting v2, we get the same as on stage
/** @var VersionedTest\TestObject $page2v2 */
$page2v2 = Versioned::get_version(VersionedTest\TestObject::class, $page2->ID, $version2);
$this->assertEquals('Page 2 - v2', $page2v2->Title);
// When selecting v2, related records should by filtered by
// the modified date of that version
$archiveParms = [
'Versioned.mode' => 'archive',
'Versioned.date' => $version2Date
];
$this->assertEquals($archiveParms, $page2v2->getInheritableQueryParams());
$this->assertArraySubset($archiveParms, $page2v2->Children()->getQueryParams());
$this->assertDOSEquals(
[
['Title' => 'Page 2a - v2'],
['Title' => 'Page 2b'],
],
$page2v2->Children()
);
}
public function testGetVersionWhenClassnameChanged()
{
$obj = new VersionedTest\TestObject;
$obj->Name = "test";
$obj->write();
$obj->Name = "test2";
$obj->ClassName = VersionedTest\Subclass::class;
$obj->write();
$subclassVersion = $obj->Version;
$obj->Name = "test3";
$obj->ClassName = VersionedTest\TestObject::class;
$obj->write();
// We should be able to pass the subclass and still get the correct class back
$obj2 = Versioned::get_version(VersionedTest\Subclass::class, $obj->ID, $subclassVersion);
$this->assertInstanceOf(VersionedTest\Subclass::class, $obj2);
$this->assertEquals("test2", $obj2->Name);
$obj3 = Versioned::get_latest_version(VersionedTest\Subclass::class, $obj->ID);
$this->assertEquals("test3", $obj3->Name);
$this->assertInstanceOf(VersionedTest\TestObject::class, $obj3);
}
public function testArchiveVersion()
{
// In 2005 this file was created
DBDatetime::set_mock_now('2005-01-01 00:00:00');
$testPage = new VersionedTest\Subclass();
$testPage->Title = 'Archived page';
$testPage->Content = 'This is the content from 2005';
$testPage->ExtraField = '2005';
$testPage->write();
// In 2007 we updated it
DBDatetime::set_mock_now('2007-01-01 00:00:00');
$testPage->Content = "It's 2007 already!";
$testPage->ExtraField = '2007';
$testPage->write();
// In 2009 we updated it again
DBDatetime::set_mock_now('2009-01-01 00:00:00');
$testPage->Content = "I'm enjoying 2009";
$testPage->ExtraField = '2009';
$testPage->write();
// End mock, back to the present day:)
DBDatetime::clear_mock_now();
// Test 1 - 2006 Content
singleton(VersionedTest\Subclass::class)->flushCache(true);
Versioned::set_reading_mode('Archive.2006-01-01 00:00:00');
$testPage2006 = DataObject::get(VersionedTest\Subclass::class)->filter(array('Title' => 'Archived page'))->first();
$this->assertInstanceOf(VersionedTest\Subclass::class, $testPage2006);
$this->assertEquals("2005", $testPage2006->ExtraField);
$this->assertEquals("This is the content from 2005", $testPage2006->Content);
// Test 2 - 2008 Content
singleton(VersionedTest\Subclass::class)->flushCache(true);
Versioned::set_reading_mode('Archive.2008-01-01 00:00:00');
$testPage2008 = DataObject::get(VersionedTest\Subclass::class)->filter(array('Title' => 'Archived page'))->first();
$this->assertInstanceOf(VersionedTest\Subclass::class, $testPage2008);
$this->assertEquals("2007", $testPage2008->ExtraField);
$this->assertEquals("It's 2007 already!", $testPage2008->Content);
// Test 3 - Today
singleton(VersionedTest\Subclass::class)->flushCache(true);
Versioned::set_reading_mode('Stage.Stage');
$testPageCurrent = DataObject::get(VersionedTest\Subclass::class)->filter(array('Title' => 'Archived page'))
->first();
$this->assertInstanceOf(VersionedTest\Subclass::class, $testPageCurrent);
$this->assertEquals("2009", $testPageCurrent->ExtraField);
$this->assertEquals("I'm enjoying 2009", $testPageCurrent->Content);
}
/**
* Test that archive works on live stage
*/
public function testArchiveLive()
{
Versioned::set_stage(Versioned::LIVE);
$this->logInWithPermission('ADMIN');
$record = new VersionedTest\TestObject();
$record->Name = 'test object';
// Writing in live mode should write to draft as well
$record->write();
$recordID = $record->ID;
$this->assertTrue($record->isPublished());
$this->assertTrue($record->isOnDraft());
// Delete in live
/** @var VersionedTest\TestObject $recordLive */
$recordLive = VersionedTest\TestObject::get()->byID($recordID);
$recordLive->doArchive();
$this->assertFalse($recordLive->isPublished());
$this->assertFalse($recordLive->isOnDraft());
}
/**
* Test archive works on draft
*/
public function testArchiveDraft()
{
Versioned::set_stage(Versioned::DRAFT);
$this->logInWithPermission('ADMIN');
$record = new VersionedTest\TestObject();
$record->Name = 'test object';
// Writing in draft mode requires publishing to effect on live
$record->write();
$record->publishRecursive();
$recordID = $record->ID;
$this->assertTrue($record->isPublished());
$this->assertTrue($record->isOnDraft());
// Delete in draft
/** @var VersionedTest\TestObject $recordDraft */
$recordDraft = VersionedTest\TestObject::get()->byID($recordID);
$recordDraft->doArchive();
$this->assertFalse($recordDraft->isPublished());
$this->assertFalse($recordDraft->isOnDraft());
}
public function testAllVersions()
{
// In 2005 this file was created
DBDatetime::set_mock_now('2005-01-01 00:00:00');
$testPage = new VersionedTest\Subclass();
$testPage->Title = 'Archived page';
$testPage->Content = 'This is the content from 2005';
$testPage->ExtraField = '2005';
$testPage->write();
// In 2007 we updated it
DBDatetime::set_mock_now('2007-01-01 00:00:00');
$testPage->Content = "It's 2007 already!";
$testPage->ExtraField = '2007';
$testPage->write();
// Check both versions are returned
$versions = Versioned::get_all_versions(VersionedTest\Subclass::class, $testPage->ID);
$content = array();
$extraFields = array();
foreach ($versions as $version) {
$content[] = $version->Content;
$extraFields[] = $version->ExtraField;
}
$this->assertEquals($versions->Count(), 2, 'All versions returned');
$this->assertEquals(
$content,
array('This is the content from 2005', "It's 2007 already!"),
'Version fields returned'
);
$this->assertEquals($extraFields, array('2005', '2007'), 'Version fields returned');
// In 2009 we updated it again
DBDatetime::set_mock_now('2009-01-01 00:00:00');
$testPage->Content = "I'm enjoying 2009";
$testPage->ExtraField = '2009';
$testPage->write();
// End mock, back to the present day:)
DBDatetime::clear_mock_now();
$versions = Versioned::get_all_versions(VersionedTest\Subclass::class, $testPage->ID);
$content = array();
$extraFields = array();
foreach ($versions as $version) {
$content[] = $version->Content;
$extraFields[] = $version->ExtraField;
}
$this->assertEquals($versions->Count(), 3, 'Additional all versions returned');
$this->assertEquals(
$content,
array('This is the content from 2005', "It's 2007 already!", "I'm enjoying 2009"),
'Additional version fields returned'
);
$this->assertEquals($extraFields, array('2005', '2007', '2009'), 'Additional version fields returned');
}
public function testArchiveRelatedDataWithoutVersioned()
{
DBDatetime::set_mock_now('2009-01-01 00:00:00');
$relatedData = new VersionedTest\RelatedWithoutversion();
$relatedData->Name = 'Related Data';
$relatedDataId = $relatedData->write();
$testData = new VersionedTest\TestObject();
$testData->Title = 'Test';
$testData->Content = 'Before Content';
$testData->Related()->add($relatedData);
$id = $testData->write();
DBDatetime::set_mock_now('2010-01-01 00:00:00');
$testData->Content = 'After Content';
$testData->write();
Versioned::reading_archived_date('2009-01-01 19:00:00');
$fetchedData = VersionedTest\TestObject::get()->byId($id);
$this->assertEquals('Before Content', $fetchedData->Content, 'We see the correct content of the older version');
$relatedData = VersionedTest\RelatedWithoutversion::get()->byId($relatedDataId);
$this->assertEquals(
1,
$relatedData->Related()->count(),
'We have a relation, with no version table, querying it still works'
);
}
public function testVersionedWithSingleStage()
{
$tables = DB::table_list();
$this->assertContains(
'versionedtest_singlestage',
array_keys($tables),
'Contains base table'
);
$this->assertContains(
'versionedtest_singlestage_versions',
array_keys($tables),
'Contains versions table'
);
$this->assertNotContains(
'versionedtest_singlestage_live',
array_keys($tables),
'Does not contain separate table with _Live suffix'
);
$this->assertNotContains(
'versionedtest_singlestage_stage',
array_keys($tables),
'Does not contain separate table with _Stage suffix'
);
Versioned::set_stage(Versioned::DRAFT);
$obj = new VersionedTest\SingleStage(array('Name' => 'MyObj'));
$obj->write();
$this->assertNotNull(
VersionedTest\SingleStage::get()->byID($obj->ID),
'Writes to and reads from default stage if its set explicitly'
);
Versioned::set_stage(Versioned::LIVE);
$obj = new VersionedTest\SingleStage(array('Name' => 'MyObj'));
$obj->write();
$this->assertNotNull(
VersionedTest\SingleStage::get()->byID($obj->ID),
'Writes to and reads from default stage even if a non-matching stage is set'
);
}
/**
* Test that publishing processes respects lazy loaded fields
*/
public function testLazyLoadFields()
{
$originalMode = Versioned::get_reading_mode();
// Generate staging record and retrieve it from stage in live mode
Versioned::set_stage(Versioned::DRAFT);
$obj = new VersionedTest\Subclass();
$obj->Name = 'bob';
$obj->ExtraField = 'Field Value';
$obj->write();
$objID = $obj->ID;
$filter = sprintf('"VersionedTest_DataObject"."ID" = \'%d\'', Convert::raw2sql($objID));
Versioned::set_stage(Versioned::LIVE);
// Check fields are unloaded prior to access
$objLazy = Versioned::get_one_by_stage(VersionedTest\TestObject::class, 'Stage', $filter, false);
$lazyFields = $objLazy->getQueriedDatabaseFields();
$this->assertTrue(isset($lazyFields['ExtraField_Lazy']));
$this->assertEquals(VersionedTest\Subclass::class, $lazyFields['ExtraField_Lazy']);
// Check lazy loading works when viewing a Stage object in Live mode
$this->assertEquals('Field Value', $objLazy->ExtraField);
// Test that writeToStage respects lazy loaded fields
$objLazy = Versioned::get_one_by_stage(VersionedTest\TestObject::class, 'Stage', $filter, false);
$objLazy->writeToStage('Live');
$objLive = Versioned::get_one_by_stage(VersionedTest\TestObject::class, 'Live', $filter, false);
$liveLazyFields = $objLive->getQueriedDatabaseFields();
// Check fields are unloaded prior to access
$this->assertTrue(isset($liveLazyFields['ExtraField_Lazy']));
$this->assertEquals(VersionedTest\Subclass::class, $liveLazyFields['ExtraField_Lazy']);
// Check that live record has original value
$this->assertEquals('Field Value', $objLive->ExtraField);
Versioned::set_reading_mode($originalMode);
}
public function testLazyLoadFieldsRetrieval()
{
// Set reading mode to Stage
Versioned::set_stage(Versioned::DRAFT);
// Create object only in reading stage
$original = new VersionedTest\Subclass();
$original->ExtraField = 'Foo';
$original->write();
// Query for object using base class
$query = VersionedTest\TestObject::get()->filter('ID', $original->ID);
// Set reading mode to Live
Versioned::set_stage(Versioned::LIVE);
$fetched = $query->first();
$this->assertTrue($fetched instanceof VersionedTest\Subclass);
$this->assertEquals($original->ID, $fetched->ID); // Eager loaded
$this->assertEquals($original->ExtraField, $fetched->ExtraField); // Lazy loaded
}
/**
* Tests that reading mode persists between requests
*/
public function testReadingPersistent()
{
$session = Injector::inst()->create('SilverStripe\\Control\\Session', array());
$adminID = $this->logInWithPermission('ADMIN');
$session->inst_set('loggedInAs', $adminID);
// Set to stage
Director::test('/?stage=Stage', null, $session);
$this->assertEquals(
'Stage.Stage',
$session->inst_get('readingMode'),
'Check querystring changes reading mode to Stage'
);
Director::test('/', null, $session);
$this->assertEquals(
'Stage.Stage',
$session->inst_get('readingMode'),
'Check that subsequent requests in the same session remain in Stage mode'
);
// Test live persists
Director::test('/?stage=Live', null, $session);
$this->assertEquals(
'Stage.Live',
$session->inst_get('readingMode'),
'Check querystring changes reading mode to Live'
);
Director::test('/', null, $session);
$this->assertEquals(
'Stage.Live',
$session->inst_get('readingMode'),
'Check that subsequent requests in the same session remain in Live mode'
);
// Test that session doesn't redundantly store the default stage if it doesn't need to
$session2 = Injector::inst()->create('SilverStripe\\Control\\Session', array());
$session2->inst_set('loggedInAs', $adminID);
Director::test('/', null, $session2);
$this->assertArrayNotHasKey('readingMode', $session2->inst_changedData());
Director::test('/?stage=Live', null, $session2);
$this->assertArrayNotHasKey('readingMode', $session2->inst_changedData());
// Test choose_site_stage
unset($_GET['stage']);
unset($_GET['archiveDate']);
Session::set('readingMode', 'Stage.Stage');
Versioned::choose_site_stage();
$this->assertEquals('Stage.Stage', Versioned::get_reading_mode());
Session::set('readingMode', 'Archive.2014-01-01');
Versioned::choose_site_stage();
$this->assertEquals('Archive.2014-01-01', Versioned::get_reading_mode());
Session::clear('readingMode');
Versioned::choose_site_stage();
$this->assertEquals('Stage.Live', Versioned::get_reading_mode());
}
/**
* Test that stage parameter is blocked by non-administrative users
*/
public function testReadingModeSecurity()
{
$this->setExpectedException(HTTPResponse_Exception::class);
$session = Injector::inst()->create(Session::class, array());
Director::test('/?stage=Stage', null, $session);
}
/**
* Ensures that the latest version of a record is the expected value
*
* @param DataObject $record
* @param int $version
*/
protected function assertRecordHasLatestVersion($record, $version)
{
$schema = DataObject::getSchema();
foreach (ClassInfo::ancestry(get_class($record), true) as $class) {
$table = $schema->tableName($class);
$versionForClass = DB::prepared_query(
$sql = "SELECT MAX(\"Version\") FROM \"{$table}_Versions\" WHERE \"RecordID\" = ?",
array($record->ID)
)->value();
$this->assertEquals($version, $versionForClass, "That the table $table has the latest version $version");
}
}
/**
* Test that that stage a record was queried from cascades to child relations, even if the
* global stage has changed
*/
public function testStageCascadeOnRelations()
{
$origReadingMode = Versioned::get_reading_mode();
// Stage record - 2 children
Versioned::set_stage(Versioned::DRAFT);
$draftPage = $this->objFromFixture(VersionedTest\TestObject::class, 'page2');
$draftPage->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertEquals(2, $draftPage->Children()->Count());
// Live record - no children
Versioned::set_stage(Versioned::LIVE);
$livePage = $this->objFromFixture(VersionedTest\TestObject::class, 'page2');
$this->assertEquals(0, $livePage->Children()->Count());
// Validate that draft page still queries draft children even though global stage is live
$this->assertEquals(2, $draftPage->Children()->Count());
// Validate that live page still queries live children even though global stage is live
Versioned::set_stage(Versioned::DRAFT);
$this->assertEquals(0, $livePage->Children()->Count());
Versioned::set_reading_mode($origReadingMode);
}
/**
* Tests that multi-table dataobjects are correctly versioned
*/
public function testWriteToStage()
{
// Test subclass with versioned extension directly added
$record = VersionedTest\Subclass::create();
$record->Title = "Test A";
$record->ExtraField = "Test A";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->Title = "Test A2";
$record->ExtraField = "Test A2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
// Test subclass without changes to base class
$record = VersionedTest\Subclass::create();
$record->ExtraField = "Test B";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->ExtraField = "Test B2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
// Test subclass without changes to sub class
$record = VersionedTest\Subclass::create();
$record->Title = "Test C";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->Title = "Test C2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
// Test subclass with versioned extension only added to the base clases
$record = VersionedTest\AnotherSubclass::create();
$record->Title = "Test A";
$record->AnotherField = "Test A";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->Title = "Test A2";
$record->AnotherField = "Test A2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
// Test subclass without changes to base class
$record = VersionedTest\AnotherSubclass::create();
$record->AnotherField = "Test B";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->AnotherField = "Test B2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
// Test subclass without changes to sub class
$record = VersionedTest\AnotherSubclass::create();
$record->Title = "Test C";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 1);
$record->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertRecordHasLatestVersion($record, 1);
$record->Title = "Test C2";
$record->writeToStage("Stage");
$this->assertRecordHasLatestVersion($record, 2);
}
public function testVersionedHandlesRenamedDataObjectFields()
{
Config::inst()->remove(VersionedTest\RelatedWithoutversion::class, 'db', 'Name', 'Varchar');
Config::inst()->update(
VersionedTest\RelatedWithoutversion::class,
'db',
array(
"NewField" => "Varchar",
)
);
VersionedTest\RelatedWithoutversion::add_extension(Versioned::class);
$this->resetDBSchema(true);
$testData = new VersionedTest\RelatedWithoutversion();
$testData->NewField = 'Test';
$testData->write();
}
public function testCanView()
{
$public1ID = $this->idFromFixture(VersionedTest\PublicStage::class, 'public1');
$public2ID = $this->idFromFixture(VersionedTest\PublicViaExtension::class, 'public2');
$privateID = $this->idFromFixture(VersionedTest\TestObject::class, 'page1');
$singleID = $this->idFromFixture(VersionedTest\SingleStage::class, 'single');
// Test that all (and only) public pages are viewable in stage mode
Session::clear("loggedInAs");
Versioned::set_stage(Versioned::DRAFT);
$public1 = Versioned::get_one_by_stage(VersionedTest\PublicStage::class, 'Stage', array('"ID"' => $public1ID));
$public2 = Versioned::get_one_by_stage(VersionedTest\PublicViaExtension::class, 'Stage', array('"ID"' => $public2ID));
$private = Versioned::get_one_by_stage(VersionedTest\TestObject::class, 'Stage', array('"ID"' => $privateID));
// Also test an object that has just a single-stage (eg. is only versioned)
$single = Versioned::get_one_by_stage(VersionedTest\SingleStage::class, 'Stage', array('"ID"' => $singleID));
$this->assertTrue($public1->canView());
$this->assertTrue($public2->canView());
$this->assertFalse($private->canView());
$this->assertFalse($single->canView());
// Adjusting the current stage should not allow objects loaded in stage to be viewable
Versioned::set_stage(Versioned::LIVE);
$this->assertTrue($public1->canView());
$this->assertTrue($public2->canView());
$this->assertFalse($private->canView());
$this->assertFalse($single->canView());
// Writing the private page to live should be fine though
$private->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$privateLive = Versioned::get_one_by_stage(VersionedTest\TestObject::class, 'Live', array('"ID"' => $privateID));
$this->assertTrue($private->canView());
$this->assertTrue($privateLive->canView());
// But if the private version becomes different to the live version, it's once again disallowed
Versioned::set_stage(Versioned::DRAFT);
$private->Title = 'Secret Title';
$private->write();
$this->assertFalse($private->canView());
$this->assertTrue($privateLive->canView());
// And likewise, viewing a live page (when mode is draft) should be ok
Versioned::set_stage(Versioned::DRAFT);
$this->assertFalse($private->canView());
$this->assertTrue($privateLive->canView());
// Logging in as admin should allow all permissions
$this->logInWithPermission('ADMIN');
Versioned::set_stage(Versioned::DRAFT);
$this->assertTrue($public1->canView());
$this->assertTrue($public2->canView());
$this->assertTrue($private->canView());
$this->assertTrue($single->canView());
}
public function testCanViewStage()
{
$public = $this->objFromFixture(VersionedTest\PublicStage::class, 'public1');
$private = $this->objFromFixture(VersionedTest\TestObject::class, 'page1');
Session::clear("loggedInAs");
Versioned::set_stage(Versioned::DRAFT);
// Test that all (and only) public pages are viewable in stage mode
// Unpublished records are not viewable in live regardless of permissions
$this->assertTrue($public->canViewStage('Stage'));
$this->assertFalse($private->canViewStage('Stage'));
$this->assertFalse($public->canViewStage('Live'));
$this->assertFalse($private->canViewStage('Live'));
// Writing records to live should make both stage and live modes viewable
$private->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$public->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE);
$this->assertTrue($public->canViewStage('Stage'));
$this->assertTrue($private->canViewStage('Stage'));
$this->assertTrue($public->canViewStage('Live'));
$this->assertTrue($private->canViewStage('Live'));
// If the draft mode changes, the live mode remains public, although the updated
// draft mode is secured for non-public records.
$private->Title = 'Secret Title';
$private->write();
$public->Title = 'Public Title';
$public->write();
$this->assertTrue($public->canViewStage('Stage'));
$this->assertFalse($private->canViewStage('Stage'));
$this->assertTrue($public->canViewStage('Live'));
$this->assertTrue($private->canViewStage('Live'));
}
/**
* Values that are overwritten with null are saved to the _versions table correctly.
*/
public function testWriteNullValueToVersion()
{
$record = VersionedTest\Subclass::create();
$record->Title = "Test A";
$record->write();
$version = Versioned::get_latest_version($record->ClassName, $record->ID);
$this->assertEquals(1, $version->Version);
$this->assertEquals($record->Title, $version->Title);
$record->Title = null;
$record->write();
$version = Versioned::get_latest_version($record->ClassName, $record->ID);
$this->assertEquals(2, $version->Version);
$this->assertEquals($record->Title, $version->Title);
}
public function testStageStates()
{
// newly created page
$createdPage = new VersionedTest\TestObject();
$createdPage->write();
$this->assertTrue($createdPage->isOnDraft());
$this->assertFalse($createdPage->isPublished());
$this->assertTrue($createdPage->isOnDraftOnly());
$this->assertTrue($createdPage->isModifiedOnDraft());
// published page
$publishedPage = new VersionedTest\TestObject();
$publishedPage->write();
$publishedPage->copyVersionToStage('Stage', 'Live');
$this->assertTrue($publishedPage->isOnDraft());
$this->assertTrue($publishedPage->isPublished());
$this->assertFalse($publishedPage->isOnDraftOnly());
$this->assertFalse($publishedPage->isOnLiveOnly());
$this->assertFalse($publishedPage->isModifiedOnDraft());
// published page, deleted from stage
$deletedFromDraftPage = new VersionedTest\TestObject();
$deletedFromDraftPage->write();
$deletedFromDraftPage->copyVersionToStage('Stage', 'Live');
$deletedFromDraftPage->deleteFromStage('Stage');
$this->assertFalse($deletedFromDraftPage->isArchived());
$this->assertFalse($deletedFromDraftPage->isOnDraft());
$this->assertTrue($deletedFromDraftPage->isPublished());
$this->assertFalse($deletedFromDraftPage->isOnDraftOnly());
$this->assertTrue($deletedFromDraftPage->isOnLiveOnly());
$this->assertFalse($deletedFromDraftPage->isModifiedOnDraft());
// published page, deleted from live
$deletedFromLivePage = new VersionedTest\TestObject();
$deletedFromLivePage->write();
$deletedFromLivePage->copyVersionToStage('Stage', 'Live');
$deletedFromLivePage->deleteFromStage('Live');
$this->assertFalse($deletedFromLivePage->isArchived());
$this->assertTrue($deletedFromLivePage->isOnDraft());
$this->assertFalse($deletedFromLivePage->isPublished());
$this->assertTrue($deletedFromLivePage->isOnDraftOnly());
$this->assertFalse($deletedFromLivePage->isOnLiveOnly());
$this->assertTrue($deletedFromLivePage->isModifiedOnDraft());
// published page, deleted from both stages
$deletedFromAllStagesPage = new VersionedTest\TestObject();
$deletedFromAllStagesPage->write();
$deletedFromAllStagesPage->copyVersionToStage('Stage', 'Live');
$deletedFromAllStagesPage->doArchive();
$this->assertTrue($deletedFromAllStagesPage->isArchived());
$this->assertFalse($deletedFromAllStagesPage->isOnDraft());
$this->assertFalse($deletedFromAllStagesPage->isPublished());
$this->assertFalse($deletedFromAllStagesPage->isOnDraftOnly());
$this->assertFalse($deletedFromAllStagesPage->isOnLiveOnly());
$this->assertFalse($deletedFromAllStagesPage->isModifiedOnDraft());
// published page, modified
$modifiedOnDraftPage = new VersionedTest\TestObject();
$modifiedOnDraftPage->write();
$modifiedOnDraftPage->copyVersionToStage('Stage', 'Live');
$modifiedOnDraftPage->Content = 'modified';
$modifiedOnDraftPage->write();
$this->assertFalse($modifiedOnDraftPage->isArchived());
$this->assertTrue($modifiedOnDraftPage->isOnDraft());
$this->assertTrue($modifiedOnDraftPage->isPublished());
$this->assertFalse($modifiedOnDraftPage->isOnDraftOnly());
$this->assertFalse($modifiedOnDraftPage->isOnLiveOnly());
$this->assertTrue($modifiedOnDraftPage->isModifiedOnDraft());
}
}

View File

@ -1,36 +0,0 @@
SilverStripe\ORM\Tests\VersionedTest\TestObject:
page1:
Title: Page 1
page2:
Title: Page 2
page3:
Title: Page 3
page2a:
Parent: =>SilverStripe\ORM\Tests\VersionedTest\TestObject.page2
Title: Page 2a
page2b:
Parent: =>SilverStripe\ORM\Tests\VersionedTest\TestObject.page2
Title: Page 2b
page3a:
Parent: =>SilverStripe\ORM\Tests\VersionedTest\TestObject.page3
Title: Page 3a
page3b:
Parent: =>SilverStripe\ORM\Tests\VersionedTest\TestObject.page3
Title: Page 3b
SilverStripe\ORM\Tests\VersionedTest\PublicStage:
public1:
Title: 'Some page'
SilverStripe\ORM\Tests\VersionedTest\PublicViaExtension:
public2:
Title: 'Another page'
SilverStripe\ORM\Tests\VersionedTest\AnotherSubclass:
subclass1:
Title: 'Subclass Page 1'
AnotherField: 'Bob'
SilverStripe\ORM\Tests\VersionedTest\SingleStage:
single:
Title: 'Singlestage Title'

View File

@ -1,14 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
class AnotherSubclass extends TestObject implements TestOnly
{
private static $table_name = 'VersionedTest_AnotherSubclass';
private static $db = array(
"AnotherField" => "Varchar"
);
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class CustomTable extends DataObject implements TestOnly
{
private static $db = [
'Title' => 'Varchar'
];
private static $table_name = 'VTCustomTable';
private static $extensions = [
Versioned::class,
];
}

View File

@ -1,17 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataExtension;
/**
* Alters stage mode of extended object to be public
*/
class PublicExtension extends DataExtension implements TestOnly
{
public function canViewNonLive($member = null)
{
return true;
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Versioned dataobject with public stage mode
*
* @mixin Versioned
*/
class PublicStage extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_PublicStage';
private static $db = array(
'Title' => 'Varchar'
);
private static $extensions = array(
Versioned::class
);
public function canView($member = null)
{
$extended = $this->extendedCan(__FUNCTION__, $member);
if ($extended !== null) {
return $extended;
}
return true;
}
public function canViewVersioned($member = null)
{
// All non-live modes are public
return true;
}
}

View File

@ -1,37 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* Public access is provided via extension rather than overriding canViewVersioned
*
* @mixin Versioned
* @mixin PublicExtension
*/
class PublicViaExtension extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_PublicViaExtension';
public function canView($member = null)
{
$extended = $this->extendedCan(__FUNCTION__, $member);
if ($extended !== null) {
return $extended;
}
return true;
}
private static $db = array(
'Title' => 'Varchar'
);
private static $extensions = array(
Versioned::class,
PublicExtension::class,
);
}

View File

@ -1,19 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class RelatedWithoutversion extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_RelatedWithoutVersion';
private static $db = array(
'Name' => 'Varchar'
);
private static $belongs_many_many = array(
'Related' => TestObject::class
);
}

View File

@ -1,23 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class SingleStage extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_SingleStage';
private static $db = array(
'Name' => 'Varchar'
);
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned("Versioned")'
);
}

View File

@ -1,13 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
class Subclass extends TestObject implements TestOnly
{
private static $table_name = 'VersionedTest_Subclass';
private static $db = array(
"ExtraField" => "Varchar",
);
}

View File

@ -1,52 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\ManyManyList;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @method TestObject Parent()
* @method HasManyList Children()
* @method ManyManyList Related()
* @mixin Versioned
*/
class TestObject extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_DataObject';
private static $db = array(
"Name" => "Varchar",
'Title' => 'Varchar',
'Content' => 'HTMLText',
);
private static $extensions = array(
Versioned::class,
);
private static $has_one = array(
'Parent' => TestObject::class,
);
private static $has_many = array(
'Children' => TestObject::class,
);
private static $many_many = array(
'Related' => RelatedWithoutversion::class,
);
public function canView($member = null)
{
$extended = $this->extendedCan(__FUNCTION__, $member);
if ($extended !== null) {
return $extended;
}
return true;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
class UnversionedWithField extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_UnversionedWithField';
private static $db = [
'Version' => 'Varchar(255)'
];
}

View File

@ -1,33 +0,0 @@
<?php
namespace SilverStripe\ORM\Tests\VersionedTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class WithIndexes extends DataObject implements TestOnly
{
private static $table_name = 'VersionedTest_WithIndexes';
private static $db = array(
'UniqA' => 'Int',
'UniqS' => 'Int',
);
private static $extensions = array(
Versioned::class
);
private static $indexes = [
'UniqS_idx' => 'unique ("UniqS")',
'UniqA_idx' => [
'type' => 'unique',
'name' => 'UniqA_idx',
'value' => '"UniqA"',
],
];
}

View File

@ -3,7 +3,7 @@
namespace SilverStripe\View\Tests;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
use Psr\SimpleCache\CacheInterface;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Director;
@ -15,12 +15,21 @@ use Symfony\Component\Cache\Simple\NullCache;
class SSViewerCacheBlockTest extends SapphireTest
{
protected $extraDataObjects = array(
SSViewerCacheBlockTest\TestModel::class,
SSViewerCacheBlockTest\VersionedModel::class
SSViewerCacheBlockTest\TestModel::class
);
protected function getExtraDataObjects()
{
$classes = parent::getExtraDataObjects();
// Add extra classes if versioning is enabled
if (class_exists(Versioned::class)) {
$classes[] = SSViewerCacheBlockTest\VersionedModel::class;
}
return $classes;
}
/**
* @var SSViewerCacheBlockTest\TestModel
*/
@ -149,6 +158,9 @@ class SSViewerCacheBlockTest extends SapphireTest
public function testVersionedCache()
{
if (!class_exists(Versioned::class)) {
$this->markTestSkipped('testVersionedCache requires Versioned extension');
}
$origReadingMode = Versioned::get_reading_mode();
// Run without caching in stage to prove data is uncached

View File

@ -4,7 +4,7 @@ namespace SilverStripe\View\Tests\SSViewerCacheBlockTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\Versioned;
use SilverStripe\Versioned\Versioned;
class VersionedModel extends DataObject implements TestOnly
{