From 49c6761b56fd7f5470806ebf004fbf069bfa01f3 Mon Sep 17 00:00:00 2001 From: effulgentsia Date: Fri, 12 Jan 2018 15:29:04 -0800 Subject: [PATCH] Issue #2891215 by plach, timmillwood, hchonov, amateescu, catch, gabesullice: Add a way to track whether a revision was default when originally created --- .../Drupal/Core/Entity/ContentEntityBase.php | 15 ++++ .../Core/Entity/ContentEntityStorageBase.php | 9 ++ .../Drupal/Core/Entity/ContentEntityType.php | 5 ++ .../Drupal/Core/Entity/EntityFieldManager.php | 14 +++ .../Core/Entity/RevisionableInterface.php | 8 ++ ...SqlContentEntityStorageSchemaConverter.php | 22 +++++ .../BlockContentResourceTestBase.php | 5 ++ .../Media/MediaResourceTestBase.php | 5 ++ .../Node/NodeResourceTestBase.php | 5 ++ .../src/Kernel/EntitySerializationTest.php | 4 + core/modules/system/system.install | 49 ++++++++++ .../update/entity_rev_pub_updates_8400.inc | 3 +- .../EntityUpdateAddRevisionDefaultTest.php | 90 +++++++++++++++++++ ...UpdateToRevisionableAndPublishableTest.php | 3 +- .../Entity/EntityDefinitionUpdateTest.php | 1 + .../RevisionableContentEntityBaseTest.php | 74 +++++++++++++-- 16 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 core/modules/system/tests/src/Functional/Update/EntityUpdateAddRevisionDefaultTest.php diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index d755c6f7e6b..ccb43c9b572 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -331,6 +331,21 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C return $this->isNew() || $return; } + /** + * {@inheritdoc} + */ + public function wasDefaultRevision() { + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ + $entity_type = $this->getEntityType(); + if (!$entity_type->isRevisionable()) { + return TRUE; + } + + $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default'); + $value = $this->isNew() || $this->get($revision_default_key)->value; + return $value; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php index 09ed82f66c0..c85c7252aca 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php @@ -506,6 +506,15 @@ abstract class ContentEntityStorageBase extends EntityStorageBase implements Con } $this->populateAffectedRevisionTranslations($entity); + + // Populate the "revision_default" flag. We skip this when we are resaving + // the revision because this is only allowed for default revisions, and + // these cannot be made non-default. + if ($this->entityType->isRevisionable() && $entity->isNewRevision()) { + $revision_default_key = $this->entityType->getRevisionMetadataKey('revision_default'); + $entity->set($revision_default_key, $entity->isDefaultRevision()); + } + $this->doSaveFieldItems($entity); return $return; diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php index 0e26c3bb510..5be34c9ba55 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityType.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php @@ -19,10 +19,15 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface */ public function __construct($definition) { parent::__construct($definition); + $this->handlers += [ 'storage' => 'Drupal\Core\Entity\Sql\SqlContentEntityStorage', 'view_builder' => 'Drupal\Core\Entity\EntityViewBuilder', ]; + + $this->revision_metadata_keys += [ + 'revision_default' => 'revision_default', + ]; } /** diff --git a/core/lib/Drupal/Core/Entity/EntityFieldManager.php b/core/lib/Drupal/Core/Entity/EntityFieldManager.php index 63c35e9743b..c19956491d1 100644 --- a/core/lib/Drupal/Core/Entity/EntityFieldManager.php +++ b/core/lib/Drupal/Core/Entity/EntityFieldManager.php @@ -190,8 +190,10 @@ class EntityFieldManager implements EntityFieldManagerInterface { * flagged as translatable. */ protected function buildBaseFieldDefinitions($entity_type_id) { + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); $class = $entity_type->getClass(); + /** @var string[] $keys */ $keys = array_filter($entity_type->getKeys()); // Fail with an exception for non-fieldable entity types. @@ -221,6 +223,18 @@ class EntityFieldManager implements EntityFieldManagerInterface { } // Make sure that revisionable entity types are correctly defined. + if ($entity_type->isRevisionable()) { + $field_name = $entity_type->getRevisionMetadataKey('revision_default'); + $base_field_definitions[$field_name] = BaseFieldDefinition::create('boolean') + ->setLabel($this->t('Default revision')) + ->setDescription($this->t('A flag indicating whether this was a default revision when it was saved.')) + ->setStorageRequired(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE); + } + + // Make sure that revisionable and translatable entity types are correctly + // defined. if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) { // The 'revision_translation_affected' field should always be defined. // This field has been added unconditionally in Drupal 8.4.0 and it is diff --git a/core/lib/Drupal/Core/Entity/RevisionableInterface.php b/core/lib/Drupal/Core/Entity/RevisionableInterface.php index e25bc254430..0fc6db2fec9 100644 --- a/core/lib/Drupal/Core/Entity/RevisionableInterface.php +++ b/core/lib/Drupal/Core/Entity/RevisionableInterface.php @@ -51,6 +51,14 @@ interface RevisionableInterface { */ public function isDefaultRevision($new_value = NULL); + /** + * Checks whether the entity object was a default revision when it was saved. + * + * @return bool + * TRUE if the entity object was a revision, FALSE otherwise. + */ + public function wasDefaultRevision(); + /** * Checks if this entity is the latest revision. * diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php index 60712eeb216..2d3edfc6d49 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php @@ -245,6 +245,7 @@ class SqlContentEntityStorageSchemaConverter { $original_base_table = $original_entity_type->getBaseTable(); $revision_id_key = $temporary_entity_type->getKey('revision'); + $revision_default_key = $temporary_entity_type->getRevisionMetadataKey('revision_default'); $revision_translation_affected_key = $temporary_entity_type->getKey('revision_translation_affected'); // If 'progress' is not set, then this will be the first run of the batch. @@ -288,6 +289,10 @@ class SqlContentEntityStorageSchemaConverter { // Set the revision ID to be same as the entity ID. $entity->set($revision_id_key, $entity_id); + // We had no revisions so far, so the existing data belongs to the + // default revision now. + $entity->set($revision_default_key, TRUE); + // Set the 'revision_translation_affected' flag to TRUE to match the // previous API return value: if the field was not defined the value // returned was always TRUE. @@ -380,6 +385,23 @@ class SqlContentEntityStorageSchemaConverter { } $updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field; + // Add the default revision flag field. + $field_name = $entity_type->getRevisionMetadataKey('revision_default'); + $storage_definition = BaseFieldDefinition::create('boolean') + ->setName($field_name) + ->setTargetEntityTypeId($entity_type->id()) + ->setTargetBundle(NULL) + ->setLabel(t('Default revision')) + ->setDescription(t('A flag indicating whether this was a default revision when it was saved.')) + ->setStorageRequired(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE); + + if ($update_cached_definitions) { + $this->entityDefinitionUpdateManager->installFieldStorageDefinition($field_name, $entity_type->id(), $entity_type->getProvider(), $storage_definition); + } + $updated_storage_definitions[$field_name] = $storage_definition; + // Add the 'revision_translation_affected' field if needed. if ($entity_type->isTranslatable()) { $revision_translation_affected_field = BaseFieldDefinition::create('boolean') diff --git a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php index 57668179404..f5b3fdcc156 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/BlockContent/BlockContentResourceTestBase.php @@ -122,6 +122,11 @@ abstract class BlockContentResourceTestBase extends EntityResourceTestBase { 'value' => TRUE, ], ], + 'revision_default' => [ + [ + 'value' => TRUE, + ], + ], 'default_langcode' => [ [ 'value' => TRUE, diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php index 1875d004a49..430b731be74 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Media/MediaResourceTestBase.php @@ -212,6 +212,11 @@ abstract class MediaResourceTestBase extends EntityResourceTestBase { 'value' => TRUE, ], ], + 'revision_default' => [ + [ + 'value' => TRUE, + ], + ], ]; } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php index bf0ba7a5919..fc827ace160 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php @@ -152,6 +152,11 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase { 'value' => TRUE, ], ], + 'revision_default' => [ + [ + 'value' => TRUE, + ], + ], 'default_langcode' => [ [ 'value' => TRUE, diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php index 72da3cb4ebb..65c82eb208b 100644 --- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php @@ -150,6 +150,9 @@ class EntitySerializationTest extends NormalizerTestBase { 'default_langcode' => [ ['value' => TRUE], ], + 'revision_default' => [ + ['value' => TRUE], + ], 'revision_translation_affected' => [ ['value' => TRUE], ], @@ -226,6 +229,7 @@ class EntitySerializationTest extends NormalizerTestBase { 'user_id' => '' . $this->user->id() . '' . $this->user->getEntityTypeId() . '' . $this->user->uuid() . '' . $this->user->url() . '', 'revision_id' => '' . $this->entity->getRevisionId() . '', 'default_langcode' => '1', + 'revision_default' => '1', 'revision_translation_affected' => '1', 'non_mul_field' => '', 'non_rev_field' => '', diff --git a/core/modules/system/system.install b/core/modules/system/system.install index ef6f124f511..0fd4b26bdcd 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -2056,3 +2056,52 @@ function system_update_8403() { } } } + +/** + * Add the 'revision_default' field to all relevant entity types. + */ +function system_update_8501() { + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Clear the cached entity type definitions so we get the new + // 'revision_default' revision metadata key. + \Drupal::entityTypeManager()->clearCachedDefinitions(); + + // Get a list of revisionable entity types. + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface[] $definitions */ + $definitions = array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) use ($definition_update_manager) { + if ($entity_type = $definition_update_manager->getEntityType($entity_type->id())) { + return $entity_type->isRevisionable(); + } + return FALSE; + }); + + // Install the 'revision_default' field. + foreach ($definitions as $entity_type_id => $entity_type) { + $field_name = $entity_type->getRevisionMetadataKey('revision_default'); + // Install the 'revision_default' field if needed. + if (!$definition_update_manager->getFieldStorageDefinition($field_name, $entity_type_id)) { + // Make sure the new "revision_default" revision metadata key is available + // also to code using the latest installed definition. + $installed_entity_type = $definition_update_manager->getEntityType($entity_type_id); + $revision_metadata_keys = $installed_entity_type->get('revision_metadata_keys'); + $revision_metadata_keys['revision_default'] = $field_name; + $installed_entity_type->set('revision_metadata_keys', $revision_metadata_keys); + $definition_update_manager->updateEntityType($installed_entity_type); + + $storage_definition = BaseFieldDefinition::create('boolean') + ->setLabel(t('Default revision')) + ->setDescription(t('A flag indicating whether this was a default revision when it was saved.')) + ->setStorageRequired(TRUE) + ->setTranslatable(FALSE) + ->setRevisionable(TRUE) + // We cannot tell whether existing revisions were default or not when + // they were created, but since we did not support creating non-default + // revisions in any core stable UI so far, we default to TRUE. + ->setInitialValue(TRUE); + + $definition_update_manager + ->installFieldStorageDefinition($field_name, $entity_type_id, $entity_type_id, $storage_definition); + } + } +} diff --git a/core/modules/system/tests/modules/entity_test_update/update/entity_rev_pub_updates_8400.inc b/core/modules/system/tests/modules/entity_test_update/update/entity_rev_pub_updates_8400.inc index 988505d58ac..0a4ed4fa128 100644 --- a/core/modules/system/tests/modules/entity_test_update/update/entity_rev_pub_updates_8400.inc +++ b/core/modules/system/tests/modules/entity_test_update/update/entity_rev_pub_updates_8400.inc @@ -38,7 +38,8 @@ function entity_test_update_update_8400() { $revision_metadata_keys = [ 'revision_user' => 'revision_user', 'revision_created' => 'revision_created', - 'revision_log_message' => 'revision_log_message' + 'revision_log_message' => 'revision_log_message', + 'revision_default' => 'revision_default', ]; $entity_type->set('revision_metadata_keys', $revision_metadata_keys); diff --git a/core/modules/system/tests/src/Functional/Update/EntityUpdateAddRevisionDefaultTest.php b/core/modules/system/tests/src/Functional/Update/EntityUpdateAddRevisionDefaultTest.php new file mode 100644 index 00000000000..e226540b6ae --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/EntityUpdateAddRevisionDefaultTest.php @@ -0,0 +1,90 @@ +entityManager = \Drupal::entityManager(); + $this->lastInstalledSchemaRepository = \Drupal::service('entity.last_installed_schema.repository'); + $this->state = \Drupal::state(); + } + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../fixtures/update/drupal-8.0.0-rc1-filled.standard.entity_test_update_mul_rev.php.gz', + ]; + } + + /** + * Tests the addition of the 'revision_default' base field. + * + * @see system_update_8501() + */ + public function testAddingTheRevisionDefaultField() { + // Make the entity type revisionable and translatable prior to running the + // updates. + $this->updateEntityTypeToRevisionableAndTranslatable(); + + // Check that the test entity type does not have the 'revision_default' + // field before running the updates. + $field_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update'); + $this->assertFalse(isset($field_storage_definitions['revision_default'])); + + $this->runUpdates(); + + // Check that the 'revision_default' field has been added by + // system_update_8501(). + $field_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions('entity_test_update'); + $this->assertTrue(isset($field_storage_definitions['revision_default'])); + + // Check that the correct initial value was set when the field was + // installed. + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $this->entityManager->getStorage('entity_test_update')->load(1); + $this->assertTrue($entity->wasDefaultRevision()); + } + +} diff --git a/core/modules/system/tests/src/Functional/Update/EntityUpdateToRevisionableAndPublishableTest.php b/core/modules/system/tests/src/Functional/Update/EntityUpdateToRevisionableAndPublishableTest.php index e72f9f1b55f..2312dc6f0a6 100644 --- a/core/modules/system/tests/src/Functional/Update/EntityUpdateToRevisionableAndPublishableTest.php +++ b/core/modules/system/tests/src/Functional/Update/EntityUpdateToRevisionableAndPublishableTest.php @@ -161,7 +161,8 @@ class EntityUpdateToRevisionableAndPublishableTest extends UpdatePathTestBase { $revision_metadata_keys = [ 'revision_user' => 'revision_user', 'revision_created' => 'revision_created', - 'revision_log_message' => 'revision_log_message' + 'revision_log_message' => 'revision_log_message', + 'revision_default' => 'revision_default', ]; $entity_type->set('revision_metadata_keys', $revision_metadata_keys); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index fa55935bbad..348c33cbb83 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -113,6 +113,7 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase { // The revision key is now defined, so the revision field needs to be // created. t('The %field_name field needs to be installed.', ['%field_name' => 'Revision ID']), + t('The %field_name field needs to be installed.', ['%field_name' => 'Default revision']), ], ]; $this->assertEqual($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.'); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php index 73b3254630e..74b2d6e4d85 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/RevisionableContentEntityBaseTest.php @@ -5,7 +5,6 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog; -use Drupal\KernelTests\KernelTestBase; use Drupal\user\Entity\User; use Drupal\user\UserInterface; @@ -13,7 +12,7 @@ use Drupal\user\UserInterface; * @coversDefaultClass \Drupal\Core\Entity\RevisionableContentEntityBase * @group Entity */ -class RevisionableContentEntityBaseTest extends KernelTestBase { +class RevisionableContentEntityBaseTest extends EntityKernelTestBase { /** * {@inheritdoc} @@ -25,10 +24,7 @@ class RevisionableContentEntityBaseTest extends KernelTestBase { */ protected function setUp() { parent::setUp(); - $this->installEntitySchema('entity_test_mul_revlog'); - $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); } /** @@ -89,6 +85,74 @@ class RevisionableContentEntityBaseTest extends KernelTestBase { $this->assertItemsTableCount(3, $definition); } + /** + * Tests the behavior of the "revision_default" flag. + * + * @covers \Drupal\Core\Entity\ContentEntityBase::wasDefaultRevision + */ + public function testWasDefaultRevision() { + $entity_type_id = 'entity_test_mul_revlog'; + $entity = EntityTestMulWithRevisionLog::create([ + 'type' => $entity_type_id, + ]); + + // Checks that in a new entity ::wasDefaultRevision() always matches + // ::isDefaultRevision(). + $this->assertEquals($entity->isDefaultRevision(), $entity->wasDefaultRevision()); + $entity->isDefaultRevision(FALSE); + $this->assertEquals($entity->isDefaultRevision(), $entity->wasDefaultRevision()); + + // Check that a new entity is always flagged as a default revision on save, + // regardless of its default revision status. + $entity->save(); + $this->assertTrue($entity->wasDefaultRevision()); + + // Check that a pending revision is not flagged as default. + $entity->setNewRevision(); + $entity->isDefaultRevision(FALSE); + $entity->save(); + $this->assertFalse($entity->wasDefaultRevision()); + + // Check that a default revision is flagged as such. + $entity->setNewRevision(); + $entity->isDefaultRevision(TRUE); + $entity->save(); + $this->assertTrue($entity->wasDefaultRevision()); + + // Check that a manually set value for the "revision_default" flag is + // ignored on save. + $entity->setNewRevision(); + $entity->isDefaultRevision(FALSE); + $entity->set('revision_default', TRUE); + $this->assertTrue($entity->wasDefaultRevision()); + $entity->save(); + $this->assertFalse($entity->wasDefaultRevision()); + + // Check that the default revision status was stored correctly. + $storage = $this->entityManager->getStorage($entity_type_id); + foreach ([TRUE, FALSE, TRUE, FALSE] as $index => $expected) { + /** @var \Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog $revision */ + $revision = $storage->loadRevision($index + 1); + $this->assertEquals($expected, $revision->wasDefaultRevision()); + } + + // Check that the default revision is flagged correctly. + /** @var \Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog $entity */ + $entity = $storage->loadUnchanged($entity->id()); + $this->assertTrue($entity->wasDefaultRevision()); + + // Check that the "revision_default" flag cannot be changed once set. + /** @var \Drupal\entity_test_revlog\Entity\EntityTestMulWithRevisionLog $entity2 */ + $entity2 = EntityTestMulWithRevisionLog::create([ + 'type' => $entity_type_id, + ]); + $entity2->save(); + $this->assertTrue($entity2->wasDefaultRevision()); + $entity2->isDefaultRevision(FALSE); + $entity2->save(); + $this->assertTrue($entity2->wasDefaultRevision()); + } + /** * Asserts the ammount of items on entity related tables. *