mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Split out SilverStripe\ORM\Versioned into new module
This commit is contained in:
parent
a9e1ce48df
commit
ac3a9c9e6e
@ -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"
|
||||
|
92
.upgrade.yml
92
.upgrade.yml
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
Name: versioning
|
||||
---
|
||||
SilverStripe\Forms\GridField\GridFieldDetailForm:
|
||||
extensions:
|
||||
- SilverStripe\ORM\Versioning\VersionedGridFieldDetailForm
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
@ -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,
|
||||
];
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
@ -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',
|
||||
];
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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\')',
|
||||
|
@ -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'))
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
);
|
||||
}
|
@ -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 |
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
];
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
];
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
SilverStripe\ORM\Tests\VersionableExtensionsTest\TestObject:
|
||||
object:
|
||||
Title: "Test"
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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()';
|
||||
}
|
||||
}
|
@ -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']
|
||||
];
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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'
|
@ -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'
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)',
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -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'
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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'
|
@ -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"
|
||||
);
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
@ -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")'
|
||||
);
|
||||
}
|
@ -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",
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)'
|
||||
];
|
||||
}
|
@ -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"',
|
||||
],
|
||||
];
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user