API Versioned::publishRecursive() now uses a ChangeSet

API Add IsInferred to inferred changesets
API Add SapphireTest::assertNotDOSContains
Fixes #5667
This commit is contained in:
Damian Mooyman 2016-10-12 12:19:07 +13:00
parent 84cc615e6c
commit f60fe7d4a9
No known key found for this signature in database
GPG Key ID: 78B823A10DE27D1A
10 changed files with 234 additions and 65 deletions

View File

@ -815,10 +815,10 @@ class File extends DataObject implements ShortcodeHandler, AssetContainer, Thumb
* @todo Solve this via triggered publishing / ownership in the future
*/
public function onBeforePublish() {
// Relies on Parent() returning the stage record
$parent = $this->Parent();
if($parent && $parent->exists()) {
$parent->publishRecursive();
// Publish all parents from the root up
/** @var Folder $parent */
foreach($this->getAncestors()->reverse() as $parent) {
$parent->publishSingle();
}
}

View File

@ -689,6 +689,49 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
);
}
}
/**
* Asserts that no items in a given list appear in the given dataobject list
*
* @param SS_List|array $matches The patterns to match. Each pattern is a map of key-value pairs. You can
* either pass a single pattern or an array of patterns.
* @param SS_List $dataObjectSet The {@link SS_List} to test.
*
* Examples
* --------
* Check that $members doesn't have an entry with Email = sam@example.com:
* $this->assertNotDOSContains(array('Email' => '...@example.com'), $members);
*
* Check that $members doesn't have entries with Email = sam@example.com and with
* Email = ingo@example.com:
* $this->assertNotDOSContains(array(
* array('Email' => '...@example.com'),
* array('Email' => 'i...@example.com'),
* ), $members);
*/
public function assertNotDOSContains($matches, $dataObjectSet) {
$extracted = array();
foreach($dataObjectSet as $object) {
/** @var DataObject $object */
$extracted[] = $object->toMap();
}
$matched = [];
foreach($matches as $match) {
foreach($extracted as $i => $item) {
if($this->dataObjectArrayMatch($item, $match)) {
$matched[] = $extracted[$i];
break;
}
}
// We couldn't find a match - assertion failed
$this->assertEmpty(
$matched,
"Failed asserting that the SS_List dosn't contain a set of objects. "
. "Found objects were: " . var_export($matched, true)
);
}
}
/**
* Assert that the given {@link SS_List} includes only DataObjects matching the given

View File

@ -61,6 +61,13 @@ class ReadonlyField extends FormField {
return parent::castingHelper($field);
}
public function getSchemaStateDefaults() {
$values = parent::getSchemaStateDefaults();
// Suppress `<i>('none')</i>` from appearing in react as a literal
$values['value'] = $this->dataValue();
return $values;
}
/**
* If $dontEscape is true the returned value will be plain text

View File

@ -26,6 +26,7 @@ use LogicException;
* @method Member Owner()
* @property string $Name
* @property string $State
* @property bool $IsInferred
*/
class ChangeSet extends DataObject {
@ -47,6 +48,7 @@ class ChangeSet extends DataObject {
private static $db = array(
'Name' => 'Varchar',
'State' => "Enum('open,published,reverted','open')",
'IsInferred' => 'Boolean(0)' // True if created automatically
);
private static $has_many = array(
@ -91,6 +93,7 @@ class ChangeSet extends DataObject {
* Publish this changeset, then closes it.
*
* @throws Exception
* @return bool True if successful
*/
public function publish() {
// Logical checks prior to publish
@ -114,9 +117,19 @@ class ChangeSet extends DataObject {
$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;
}
/**
@ -377,7 +390,11 @@ class ChangeSet extends DataObject {
public function getCMSFields() {
$fields = new FieldList(new TabSet('Root'));
$fields->addFieldToTab('Root.Main', TextField::create('Name', $this->fieldLabel('Name')));
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')));
}

View File

@ -245,6 +245,17 @@ class ChangeSetItem extends DataObject implements Thumbnail {
$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);

View File

@ -17,6 +17,7 @@ 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;
@ -1456,22 +1457,20 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
* @return bool
*/
public function publishRecursive() {
$owner = $this->owner;
if(!$owner->publishSingle()) {
return false;
}
// Publish owned objects
foreach ($owner->findOwned(false) as $object) {
/** @var Versioned|DataObject $object */
$object->publishRecursive();
}
// 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 live record
$owner->unlinkDisownedObjects(Versioned::DRAFT, Versioned::LIVE);
return true;
// 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();
}
/**

View File

@ -268,6 +268,7 @@ JSON;
'Created' => $changeSet->Created,
'LastEdited' => $changeSet->LastEdited,
'State' => $changeSet->State,
'IsInferred' => $changeSet->IsInferred,
'canEdit' => $changeSet->canEdit(),
'canPublish' => false,
'_embedded' => ['items' => []]

View File

@ -3,15 +3,18 @@
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\Versioning\ChangeSetItem;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\Versioning\Versioned;
/**
* @mixin Versioned
*/
class ChangeSetItemTest_Versioned extends DataObject {
private static $db = [
'Foo' => 'Int'
];
private static $extensions = [
"SilverStripe\\ORM\\Versioning\\Versioned"
Versioned::class
];
function canEdit($member = null) { return true; }
@ -24,10 +27,11 @@ class ChangeSetItemTest_Versioned extends DataObject {
class ChangeSetItemTest extends SapphireTest {
protected $extraDataObjects = [
'ChangeSetItemTest_Versioned'
ChangeSetItemTest_Versioned::class
];
function testChangeType() {
public function testChangeType() {
$this->logInWithPermission('ADMIN');
$object = new ChangeSetItemTest_Versioned(['Foo' => 1]);
$object->write();
@ -79,7 +83,8 @@ class ChangeSetItemTest extends SapphireTest {
);
}
function testGetForObject() {
public function testGetForObject() {
$this->logInWithPermission('ADMIN');
$object = new ChangeSetItemTest_Versioned(['Foo' => 1]);
$object->write();

View File

@ -10,8 +10,6 @@ use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Session;
/**
* Provides a set of targettable permissions for tested models
*
@ -59,7 +57,7 @@ class ChangeSetTest_Base extends DataObject implements TestOnly {
];
private static $has_many = [
'Mids' => 'ChangeSetTest_Mid',
'Mids' => ChangeSetTest_Mid::class,
];
private static $owns = [
@ -82,8 +80,8 @@ class ChangeSetTest_Mid extends DataObject implements TestOnly {
];
private static $has_one = [
'Base' => 'ChangeSetTest_Base',
'End' => 'ChangeSetTest_End',
'Base' => ChangeSetTest_Base::class,
'End' => ChangeSetTest_End::class,
];
private static $owns = [
@ -128,10 +126,10 @@ class ChangeSetTest extends SapphireTest {
protected static $fixture_file = 'ChangeSetTest.yml';
protected $extraDataObjects = [
'ChangeSetTest_Base',
'ChangeSetTest_Mid',
'ChangeSetTest_End',
'ChangeSetTest_EndChild',
ChangeSetTest_Base::class,
ChangeSetTest_Mid::class,
ChangeSetTest_End::class,
ChangeSetTest_EndChild::class,
];
/**
@ -191,8 +189,8 @@ class ChangeSetTest extends SapphireTest {
$cs = new ChangeSet();
$cs->write();
$cs->addObject($this->objFromFixture('ChangeSetTest_End', 'end1'));
$cs->addObject($this->objFromFixture('ChangeSetTest_EndChild', 'endchild1'));
$cs->addObject($this->objFromFixture(ChangeSetTest_End::class, 'end1'));
$cs->addObject($this->objFromFixture(ChangeSetTest_EndChild::class, 'endchild1'));
$this->assertChangeSetLooksLike($cs, [
'ChangeSetTest_End.end1' => ChangeSetItem::EXPLICITLY,
@ -206,7 +204,7 @@ class ChangeSetTest extends SapphireTest {
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
@ -226,7 +224,7 @@ class ChangeSetTest extends SapphireTest {
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
@ -235,7 +233,7 @@ class ChangeSetTest extends SapphireTest {
'ChangeSetTest_Base.base' => ChangeSetItem::EXPLICITLY
]);
$end = $this->objFromFixture('ChangeSetTest_End', 'end1');
$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end->Baz = 3;
$end->write();
@ -257,7 +255,7 @@ class ChangeSetTest extends SapphireTest {
$this->assertDOSEquals([
[
'Added' => ChangeSetItem::EXPLICITLY,
'ObjectClass' => 'ChangeSetTest_Base',
'ObjectClass' => ChangeSetTest_Base::class,
'ObjectID' => $base->ID,
'ChangeSetID' => $cs->ID
]
@ -273,7 +271,7 @@ class ChangeSetTest extends SapphireTest {
$cs = new ChangeSet();
$cs->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$cs->addObject($base);
$cs->sync();
@ -282,7 +280,7 @@ class ChangeSetTest extends SapphireTest {
]);
$this->assertTrue($cs->isSynced());
$end = $this->objFromFixture('ChangeSetTest_End', 'end1');
$end = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end->Baz = 3;
$end->write();
$this->assertFalse($cs->isSynced());
@ -301,7 +299,7 @@ class ChangeSetTest extends SapphireTest {
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
@ -333,7 +331,7 @@ class ChangeSetTest extends SapphireTest {
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
@ -362,7 +360,7 @@ class ChangeSetTest extends SapphireTest {
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
@ -381,7 +379,7 @@ class ChangeSetTest extends SapphireTest {
$this->logInWithPermission('ADMIN');
$changeSet = new ChangeSet();
$changeSet->write();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$changeSet->addObject($base);
$changeSet->sync();
$this->assertEquals(5, $changeSet->Changes()->count());
@ -398,10 +396,10 @@ class ChangeSetTest extends SapphireTest {
public function testPublish() {
$this->publishAllFixtures();
$base = $this->objFromFixture('ChangeSetTest_Base', 'base');
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
$baseID = $base->ID;
$baseBefore = $base->Version;
$end1 = $this->objFromFixture('ChangeSetTest_End', 'end1');
$end1 = $this->objFromFixture(ChangeSetTest_End::class, 'end1');
$end1ID = $end1->ID;
$end1Before = $end1->Version;
@ -436,7 +434,7 @@ class ChangeSetTest extends SapphireTest {
// Check each item has the correct before/after version applied
$baseChange = $changeset->Changes()->filter([
'ObjectClass' => 'ChangeSetTest_Base',
'ObjectClass' => ChangeSetTest_Base::class,
'ObjectID' => $baseID,
])->first();
$this->assertEquals((int)$baseBefore, (int)$baseChange->VersionBefore);
@ -444,29 +442,29 @@ class ChangeSetTest extends SapphireTest {
$this->assertEquals((int)$baseChange->VersionBefore + 1, (int)$baseChange->VersionAfter);
$this->assertEquals(
(int)$baseChange->VersionAfter,
(int)Versioned::get_versionnumber_by_stage('ChangeSetTest_Base', Versioned::LIVE, $baseID)
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Base::class, Versioned::LIVE, $baseID)
);
$end1Change = $changeset->Changes()->filter([
'ObjectClass' => 'ChangeSetTest_End',
'ObjectClass' => ChangeSetTest_End::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_End', Versioned::LIVE, $end1ID)
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_End::class, Versioned::LIVE, $end1ID)
);
$midNewChange = $changeset->Changes()->filter([
'ObjectClass' => 'ChangeSetTest_Mid',
'ObjectClass' => ChangeSetTest_Mid::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_Mid', Versioned::LIVE, $midNewID)
(int)Versioned::get_versionnumber_by_stage(ChangeSetTest_Mid::class, Versioned::LIVE, $midNewID)
);
// Test trying to re-publish is blocked
@ -477,4 +475,47 @@ class ChangeSetTest extends SapphireTest {
$changeset->publish();
}
/**
* Ensure that related objects are disassociated on live
*/
public function testUnlinkDisassociated() {
$this->publishAllFixtures();
/** @var ChangeSetTest_Base $base */
$base = $this->objFromFixture(ChangeSetTest_Base::class, 'base');
/** @var ChangeSetTest_Mid $mid1 $mid2 */
$mid1 = $this->objFromFixture(ChangeSetTest_Mid::class, 'mid1');
$mid2 = $this->objFromFixture(ChangeSetTest_Mid::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_Base.base' => ChangeSetItem::EXPLICITLY
]);
$changeset->publish();
// mid1 on live exists, but has BaseID set to zero
$mid1Live = Versioned::get_by_stage(ChangeSetTest_Mid::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_Mid::class, Versioned::LIVE)
->byID($mid2->ID);
$this->assertNotNull($mid2Live);
$this->assertEquals($mid2->ID, $mid2Live->ID);
$this->assertEquals($base->ID, $mid2Live->BaseID);
}
}

View File

@ -1,12 +1,15 @@
<?php
use SilverStripe\Dev\Debug;
use SilverStripe\ORM\DataList;
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 SilverStripe\Dev\TestOnly;
/**
* Tests ownership API of versioned DataObjects
*/
@ -268,7 +271,7 @@ class VersionedOwnershipTest extends SapphireTest {
// Check state of objects before publish
$oldLiveBanners = [
['Title' => 'Related Many 1'],
['Title' => 'Related Many 2'], // Will be deleted
['Title' => 'Related Many 2'], // Will be unlinked (but not deleted)
// `Related Many 3` isn't published
];
$newBanners = [
@ -284,6 +287,8 @@ class VersionedOwnershipTest extends SapphireTest {
$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
@ -299,6 +304,46 @@ class VersionedOwnershipTest extends SapphireTest {
$banner2Live = Versioned::get_by_stage('VersionedOwnershipTest_RelatedMany', 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()
);
}
/**
@ -595,7 +640,7 @@ class VersionedOwnershipTest extends SapphireTest {
*/
class VersionedOwnershipTest_Object extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -637,7 +682,7 @@ class VersionedOwnershipTest_Subclass extends VersionedOwnershipTest_Object impl
*/
class VersionedOwnershipTest_Related extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -669,7 +714,7 @@ class VersionedOwnershipTest_Related extends DataObject implements TestOnly {
*/
class VersionedOwnershipTest_RelatedMany extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -691,7 +736,7 @@ class VersionedOwnershipTest_RelatedMany extends DataObject implements TestOnly
class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -714,7 +759,7 @@ class VersionedOwnershipTest_Attachment extends DataObject implements TestOnly {
*/
class VersionedOwnershipTest_Page extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -749,7 +794,7 @@ class VersionedOwnershipTest_Page extends DataObject implements TestOnly {
*/
class VersionedOwnershipTest_Banner extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -773,7 +818,7 @@ class VersionedOwnershipTest_Banner extends DataObject implements TestOnly {
*/
class VersionedOwnershipTest_CustomRelation extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(
@ -803,7 +848,7 @@ class VersionedOwnershipTest_CustomRelation extends DataObject implements TestOn
*/
class VersionedOwnershipTest_Image extends DataObject implements TestOnly {
private static $extensions = array(
'SilverStripe\\ORM\\Versioning\\Versioned',
Versioned::class,
);
private static $db = array(