BUG Prevent Versioned::doRollbackTo from creating incorrect versions on subclasses of Versioned DataObjects

Document correct configuration of Versioned DataObjects
Fixes #4936
This commit is contained in:
Damian Mooyman 2016-01-22 14:45:05 +13:00
parent 88f8acff5a
commit bf8bf5e4d5
5 changed files with 83 additions and 37 deletions

View File

@ -27,6 +27,11 @@ The extension is automatically applied to `SiteTree` class. For more information
[Extending](../extending) and the [Configuration](../configuration) documentation. [Extending](../extending) and the [Configuration](../configuration) documentation.
</div> </div>
<div class="warning" markdown="1">
Versioning only works if you are adding the extension to the base class. That is, the first subclass
of `DataObject`. Adding this extension to children of the base class will have unpredictable behaviour.
</div>
## Database Structure ## Database Structure
Depending on how many stages you configured, two or more new tables will be created for your records. In the above, this Depending on how many stages you configured, two or more new tables will be created for your records. In the above, this

View File

@ -652,26 +652,31 @@ class Versioned extends DataExtension implements TemplateGlobalProvider {
// Get ID field // Get ID field
$id = $manipulation[$table]['id'] ? $manipulation[$table]['id'] : $manipulation[$table]['fields']['ID']; $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(!$id) {
user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
}
if($this->migratingVersion) { if($this->migratingVersion) {
$manipulation[$table]['fields']['Version'] = $this->migratingVersion; $manipulation[$table]['fields']['Version'] = $this->migratingVersion;
} }
// If we haven't got a version #, then we're creating a new version. $version = isset($manipulation[$table]['fields']['Version'])
// Otherwise, we're just copying a version to another table ? $manipulation[$table]['fields']['Version']
if(empty($manipulation[$table]['fields']['Version'])) { : null;
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, $table, $id); $this->augmentWriteVersioned($manipulation, $table, $id);
} }
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version // For base classes of versioned data objects
if($manipulation[$table]['fields']['Version'] < 0 || $this->_nextWriteWithoutVersion) { if(!$this->hasVersionField($table)) {
unset($manipulation[$table]['fields']['Version']); unset($manipulation[$table]['fields']['Version']);
} }
// For base classes of versioned data objects
if(!$this->hasVersionField($table)) unset($manipulation[$table]['fields']['Version']);
// Grab a version number - it should be the same across all tables. // Grab a version number - it should be the same across all tables.
if(isset($manipulation[$table]['fields']['Version'])) { if(isset($manipulation[$table]['fields']['Version'])) {
$thisVersion = $manipulation[$table]['fields']['Version']; $thisVersion = $manipulation[$table]['fields']['Version'];

View File

@ -13,6 +13,7 @@ class DataObjectLazyLoadingTest extends SapphireTest {
// These are all defined in DataObjectTest.php and VersionedTest.php // These are all defined in DataObjectTest.php and VersionedTest.php
protected $extraDataObjects = array( protected $extraDataObjects = array(
// From DataObjectTest
'DataObjectTest_Team', 'DataObjectTest_Team',
'DataObjectTest_Fixture', 'DataObjectTest_Fixture',
'DataObjectTest_SubTeam', 'DataObjectTest_SubTeam',
@ -24,8 +25,24 @@ class DataObjectLazyLoadingTest extends SapphireTest {
'DataObjectTest_TeamComment', 'DataObjectTest_TeamComment',
'DataObjectTest_EquipmentCompany', 'DataObjectTest_EquipmentCompany',
'DataObjectTest_SubEquipmentCompany', 'DataObjectTest_SubEquipmentCompany',
'DataObjectTest\NamespacedClass',
'DataObjectTest\RelationClass',
'DataObjectTest_ExtendedTeamComment',
'DataObjectTest_Company',
'DataObjectTest_Staff',
'DataObjectTest_CEO',
'DataObjectTest_Fan',
'DataObjectTest_Play',
'DataObjectTest_Ploy',
'DataObjectTest_Bogey',
// From VersionedTest
'VersionedTest_DataObject', 'VersionedTest_DataObject',
'VersionedTest_Subclass', 'VersionedTest_Subclass',
'VersionedTest_AnotherSubclass',
'VersionedTest_RelatedWithoutVersion',
'VersionedTest_SingleStage',
'VersionedTest_WithIndexes',
// From DataObjectLazyLoadingTest
'VersionedLazy_DataObject', 'VersionedLazy_DataObject',
'VersionedLazySub_DataObject', 'VersionedLazySub_DataObject',
); );

View File

@ -154,13 +154,13 @@ class VersionedTest extends SapphireTest {
"\"VersionedTest_DataObject\".\"ID\" ASC"); "\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 has gone // Check that page 3 has gone
$this->assertNotNull($remainingPages); $this->assertNotNull($remainingPages);
$this->assertEquals(array("Page 1", "Page 2"), $remainingPages->column('Title')); $this->assertEquals(array("Page 1", "Page 2", "Subclass Page 1"), $remainingPages->column('Title'));
// Get all including deleted // Get all including deleted
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0", $allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"); "\"VersionedTest_DataObject\".\"ID\" ASC");
// Check that page 3 is still there // Check that page 3 is still there
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title')); $this->assertEquals(array("Page 1", "Page 2", "Page 3", "Subclass Page 1"), $allPages->column('Title'));
// Check that the returned pages have the correct IDs // Check that the returned pages have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID')); $this->assertEquals($allPageIDs, $allPages->column('ID'));
@ -169,7 +169,7 @@ class VersionedTest extends SapphireTest {
Versioned::reading_stage("Live"); Versioned::reading_stage("Live");
$allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0", $allPages = Versioned::get_including_deleted("VersionedTest_DataObject", "\"ParentID\" = 0",
"\"VersionedTest_DataObject\".\"ID\" ASC"); "\"VersionedTest_DataObject\".\"ID\" ASC");
$this->assertEquals(array("Page 1", "Page 2", "Page 3"), $allPages->column('Title')); $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 // Check that the returned pages still have the correct IDs
$this->assertEquals($allPageIDs, $allPages->column('ID')); $this->assertEquals($allPageIDs, $allPages->column('ID'));
@ -208,7 +208,7 @@ class VersionedTest extends SapphireTest {
} }
public function testRollbackTo() { public function testRollbackTo() {
$page1 = $this->objFromFixture('VersionedTest_DataObject', 'page1'); $page1 = $this->objFromFixture('VersionedTest_AnotherSubclass', 'subclass1');
$page1->Content = 'orig'; $page1->Content = 'orig';
$page1->write(); $page1->write();
$page1->publish('Stage', 'Live'); $page1->publish('Stage', 'Live');
@ -225,6 +225,17 @@ class VersionedTest extends SapphireTest {
$this->assertTrue($page1->Version > $changedVersion, 'Create a new higher version number'); $this->assertTrue($page1->Version > $changedVersion, 'Create a new higher version number');
$this->assertEquals('orig', $page1->Content, 'Copies the content from the old version'); $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() { public function testDeleteFromStage() {
@ -318,6 +329,7 @@ class VersionedTest extends SapphireTest {
$noversion = new DataObject(); $noversion = new DataObject();
$versioned = new VersionedTest_DataObject(); $versioned = new VersionedTest_DataObject();
$versionedSub = new VersionedTest_Subclass(); $versionedSub = new VersionedTest_Subclass();
$versionedAno = new VersionedTest_AnotherSubclass();
$versionField = new VersionedTest_UnversionedWithField(); $versionField = new VersionedTest_UnversionedWithField();
$this->assertFalse( $this->assertFalse(
@ -329,8 +341,14 @@ class VersionedTest extends SapphireTest {
'The versioned ext adds an Int version field.' 'The versioned ext adds an Int version field.'
); );
$this->assertEquals( $this->assertEquals(
'Int', $versionedSub->hasOwnTableDatabaseField('Version'), null,
'Sub-classes of a versioned model have a Version field.' $versionedSub->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model don\'t have a Version field.'
);
$this->assertEquals(
null,
$versionedAno->hasOwnTableDatabaseField('Version'),
'Sub-classes of a versioned model don\'t have a Version field.'
); );
$this->assertEquals( $this->assertEquals(
'Varchar', $versionField->hasOwnTableDatabaseField('Version'), 'Varchar', $versionField->hasOwnTableDatabaseField('Version'),
@ -818,10 +836,6 @@ class VersionedTest_Subclass extends VersionedTest_DataObject implements TestOnl
private static $db = array( private static $db = array(
"ExtraField" => "Varchar", "ExtraField" => "Varchar",
); );
private static $extensions = array(
"Versioned('Stage', 'Live')"
);
} }
/** /**

View File

@ -1,19 +1,24 @@
VersionedTest_DataObject: VersionedTest_DataObject:
page1: page1:
Title: Page 1 Title: Page 1
page2: page2:
Title: Page 2 Title: Page 2
page3: page3:
Title: Page 3 Title: Page 3
page2a: page2a:
Parent: =>VersionedTest_DataObject.page2 Parent: =>VersionedTest_DataObject.page2
Title: Page 2a Title: Page 2a
page2b: page2b:
Parent: =>VersionedTest_DataObject.page2 Parent: =>VersionedTest_DataObject.page2
Title: Page 2b Title: Page 2b
page3a: page3a:
Parent: =>VersionedTest_DataObject.page3 Parent: =>VersionedTest_DataObject.page3
Title: Page 3a Title: Page 3a
page3b: page3b:
Parent: =>VersionedTest_DataObject.page3 Parent: =>VersionedTest_DataObject.page3
Title: Page 3b Title: Page 3b
VersionedTest_AnotherSubclass:
subclass1:
Title: 'Subclass Page 1'
AnotherField: 'Bob'