summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2015-05-15 18:09:40 (GMT)
committerAlex Pott2015-05-15 18:09:40 (GMT)
commita451002a2c1930d25767ed2883a99963721385f4 (patch)
treed4e38587557e958c592f4d3f7241b1f39fffbafb
parent3d7cd4ed0303efe7fd89574e4dbaddb4a631e247 (diff)
Issue #2428795 by mkalkbrenner, plach, yched, catch, hchonov, webchick, tstoeckler, pjonckiere, miro_dietiker, Schnitzel, klausi: Translatable entity 'changed' timestamps are not working at all
-rw-r--r--core/lib/Drupal/Core/Entity/EntityChangedInterface.php10
-rw-r--r--core/lib/Drupal/Core/Entity/EntityChangedTrait.php31
-rw-r--r--core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php5
-rw-r--r--core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php41
-rw-r--r--core/modules/block_content/src/Entity/BlockContent.php4
-rw-r--r--core/modules/comment/src/CommentInterface.php8
-rw-r--r--core/modules/comment/src/CommentStatistics.php8
-rw-r--r--core/modules/comment/src/Entity/Comment.php3
-rw-r--r--core/modules/comment/src/Form/CommentAdminOverview.php2
-rw-r--r--core/modules/comment/src/Tests/CommentTokenReplaceTest.php2
-rw-r--r--core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php12
-rw-r--r--core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php2
-rw-r--r--core/modules/content_translation/src/Tests/ContentTranslationUITest.php81
-rw-r--r--core/modules/file/src/Entity/File.php4
-rw-r--r--core/modules/file/src/Tests/SaveTest.php4
-rw-r--r--core/modules/menu_link_content/src/Entity/MenuLinkContent.php6
-rw-r--r--core/modules/menu_link_content/src/Tests/LinksTest.php2
-rw-r--r--core/modules/node/node.module9
-rw-r--r--core/modules/node/src/Entity/Node.php3
-rw-r--r--core/modules/node/src/NodeForm.php2
-rw-r--r--core/modules/node/src/Tests/NodeLastChangedTest.php2
-rw-r--r--core/modules/node/src/Tests/NodeSaveTest.php9
-rw-r--r--core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php13
-rw-r--r--core/modules/system/src/Tests/Entity/ContentEntityChangedTest.php363
-rw-r--r--core/modules/system/tests/modules/entity_test/entity_test.module30
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php3
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php83
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php72
-rw-r--r--core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ChangedTestItem.php44
-rw-r--r--core/modules/taxonomy/src/Entity/Term.php6
-rw-r--r--core/modules/user/src/Entity/User.php6
31 files changed, 831 insertions, 39 deletions
diff --git a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
index 3013f11..5f031a3 100644
--- a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
@@ -22,11 +22,19 @@ namespace Drupal\Core\Entity;
interface EntityChangedInterface {
/**
- * Gets the timestamp of the last entity change.
+ * Gets the timestamp of the last entity change for the current translation.
*
* @return int
* The timestamp of the last entity save operation.
*/
public function getChangedTime();
+ /**
+ * Gets the timestamp of the last entity change across all translations.
+ *
+ * @return int
+ * The timestamp of the last entity save operation across all
+ * translations.
+ */
+ public function getChangedTimeAcrossTranslations();
}
diff --git a/core/lib/Drupal/Core/Entity/EntityChangedTrait.php b/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
new file mode 100644
index 0000000..5b34d1a
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Entity\EntityChangedTrait.
+ */
+
+namespace Drupal\Core\Entity;
+
+/**
+ * Provides a trait for accessing changed time.
+ */
+trait EntityChangedTrait {
+
+ /**
+ * Returns the timestamp of the last entity change across all translations.
+ *
+ * @return int
+ * The timestamp of the last entity save operation across all
+ * translations.
+ */
+ public function getChangedTimeAcrossTranslations() {
+ $changed = $this->getUntranslated()->getChangedTime();
+ foreach ($this->getTranslationLanguages(FALSE) as $language) {
+ $translation_changed = $this->getTranslation($language->getId())->getChangedTime();
+ $changed = max($translation_changed, $changed);
+ }
+ return $changed;
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index fe81a63..8fd44da 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -23,8 +23,9 @@ class EntityChangedConstraintValidator extends ConstraintValidator {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if (!$entity->isNew()) {
$saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
-
- if ($saved_entity && $saved_entity->getChangedTime() > $entity->getChangedTime()) {
+ // A change to any other translation must add a violation to the current
+ // translation because there might be untranslatable shared fields.
+ if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTime()) {
$this->context->addViolation($constraint->message);
}
}
diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php
index 81625d4..1a120f8 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/ChangedItem.php
@@ -30,7 +30,46 @@ class ChangedItem extends CreatedItem {
*/
public function preSave() {
parent::preSave();
- $this->value = REQUEST_TIME;
+
+ // Set the timestamp to request time if it is not set.
+ if (!$this->value) {
+ $this->value = REQUEST_TIME;
+ }
+ else {
+ // On an existing entity the changed timestamp will only be set to request
+ // time automatically if at least one other field value of the entity has
+ // changed. This detection doesn't run on new entities and will be turned
+ // off if the changed timestamp is set manually before save, for example
+ // during migrations or using
+ // \Drupal\content_translation\ContentTranslationMetadataWrapperInterface::setChangedTime().
+ // @todo Knowing if the current translation was modified or not is
+ // generally useful. There's a follow-up issue to reduce the nesting
+ // here and to offer an accessor for this information. See
+ // https://www.drupal.org/node/2453153
+ $entity = $this->getEntity();
+ if (!$entity->isNew()) {
+ $field_name = $this->getFieldDefinition()->getName();
+ // Clone $entity->original to avoid modifying it when calling
+ // getTranslation().
+ $original = clone $entity->original;
+ $translatable = $this->getFieldDefinition()->isTranslatable();
+ if ($translatable) {
+ $original = $original->getTranslation($entity->language()->getId());
+ }
+ if ($this->value == $original->get($field_name)->value) {
+ foreach ($entity->getFieldDefinitions() as $other_field_name => $other_field_definition) {
+ if ($other_field_name != $field_name && !$other_field_definition->isComputed() && (!$translatable || $other_field_definition->isTranslatable())) {
+ $items = $entity->get($other_field_name)->filterEmptyItems();
+ $original_items = $original->get($other_field_name)->filterEmptyItems();
+ if (!$items->equals($original_items)) {
+ $this->value = REQUEST_TIME;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php
index 046640c..82e16f5 100644
--- a/core/modules/block_content/src/Entity/BlockContent.php
+++ b/core/modules/block_content/src/Entity/BlockContent.php
@@ -8,6 +8,7 @@
namespace Drupal\block_content\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -66,6 +67,8 @@ use Drupal\block_content\BlockContentInterface;
*/
class BlockContent extends ContentEntityBase implements BlockContentInterface {
+ use EntityChangedTrait;
+
/**
* The theme the block is being created in.
*
@@ -202,6 +205,7 @@ class BlockContent extends ContentEntityBase implements BlockContentInterface {
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the custom block was last edited.'))
+ ->setTranslatable(TRUE)
->setRevisionable(TRUE);
return $fields;
diff --git a/core/modules/comment/src/CommentInterface.php b/core/modules/comment/src/CommentInterface.php
index c141706..0d558b5 100644
--- a/core/modules/comment/src/CommentInterface.php
+++ b/core/modules/comment/src/CommentInterface.php
@@ -197,14 +197,6 @@ interface CommentInterface extends ContentEntityInterface, EntityChangedInterfac
public function setCreatedTime($created);
/**
- * Returns the timestamp of when the comment was updated.
- *
- * @return int
- * The timestamp of when the comment was updated.
- */
- public function getChangedTime();
-
- /**
* Checks if the comment is published.
*
* @return bool
diff --git a/core/modules/comment/src/CommentStatistics.php b/core/modules/comment/src/CommentStatistics.php
index 588c831..c37641c 100644
--- a/core/modules/comment/src/CommentStatistics.php
+++ b/core/modules/comment/src/CommentStatistics.php
@@ -126,8 +126,10 @@ class CommentStatistics implements CommentStatisticsInterface {
}
// Default to REQUEST_TIME when entity does not have a changed property.
$last_comment_timestamp = REQUEST_TIME;
+ // @todo Make comment statistics language aware and add some tests. See
+ // https://www.drupal.org/node/2318875
if ($entity instanceof EntityChangedInterface) {
- $last_comment_timestamp = $entity->getChangedTime();
+ $last_comment_timestamp = $entity->getChangedTimeAcrossTranslations();
}
$query->values(array(
'entity_id' => $entity->id(),
@@ -243,9 +245,9 @@ class CommentStatistics implements CommentStatisticsInterface {
->fields(array(
'cid' => 0,
'comment_count' => 0,
- // Use the created date of the entity if it's set, or default to
+ // Use the changed date of the entity if it's set, or default to
// REQUEST_TIME.
- 'last_comment_timestamp' => ($entity instanceof EntityChangedInterface) ? $entity->getChangedTime() : REQUEST_TIME,
+ 'last_comment_timestamp' => ($entity instanceof EntityChangedInterface) ? $entity->getChangedTimeAcrossTranslations() : REQUEST_TIME,
'last_comment_name' => '',
'last_comment_uid' => $last_comment_uid,
))
diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php
index 2023448..bd3fbfc 100644
--- a/core/modules/comment/src/Entity/Comment.php
+++ b/core/modules/comment/src/Entity/Comment.php
@@ -10,6 +10,7 @@ namespace Drupal\comment\Entity;
use Drupal\Component\Utility\Number;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\comment\CommentInterface;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -61,6 +62,8 @@ use Drupal\user\UserInterface;
*/
class Comment extends ContentEntityBase implements CommentInterface {
+ use EntityChangedTrait;
+
/**
* The thread for which a lock was acquired.
*/
diff --git a/core/modules/comment/src/Form/CommentAdminOverview.php b/core/modules/comment/src/Form/CommentAdminOverview.php
index 0941c53..008e0bb 100644
--- a/core/modules/comment/src/Form/CommentAdminOverview.php
+++ b/core/modules/comment/src/Form/CommentAdminOverview.php
@@ -213,7 +213,7 @@ class CommentAdminOverview extends FormBase {
'#url' => $commented_entity->urlInfo(),
),
),
- 'changed' => $this->dateFormatter->format($comment->getChangedTime(), 'short'),
+ 'changed' => $this->dateFormatter->format($comment->getChangedTimeAcrossTranslations(), 'short'),
);
$comment_uri_options = $comment->urlInfo()->getOptions();
$links = array();
diff --git a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
index a5eaad8..c45c0f7 100644
--- a/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
+++ b/core/modules/comment/src/Tests/CommentTokenReplaceTest.php
@@ -61,7 +61,7 @@ class CommentTokenReplaceTest extends CommentTestBase {
$tests['[comment:url]'] = $comment->url('canonical', $url_options + array('fragment' => 'comment-' . $comment->id()));
$tests['[comment:edit-url]'] = $comment->url('edit-form', $url_options);
$tests['[comment:created:since]'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $comment->getCreatedTime(), 2, $language_interface->getId());
- $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $comment->getChangedTime(), 2, $language_interface->getId());
+ $tests['[comment:changed:since]'] = \Drupal::service('date.formatter')->formatInterval(REQUEST_TIME - $comment->getChangedTimeAcrossTranslations(), 2, $language_interface->getId());
$tests['[comment:parent:cid]'] = $comment->hasParentComment() ? $comment->getParentComment()->id() : NULL;
$tests['[comment:parent:title]'] = SafeMarkup::checkPlain($parent_comment->getSubject());
$tests['[comment:entity]'] = SafeMarkup::checkPlain($node->getTitle());
diff --git a/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php b/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php
index 4a0a65b..b0ced47 100644
--- a/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php
+++ b/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php
@@ -7,8 +7,6 @@
namespace Drupal\content_translation;
-use Drupal\Core\Entity\EntityChangedInterface;
-use Drupal\Core\Entity\EntityInterface;
use Drupal\user\UserInterface;
/**
@@ -17,7 +15,7 @@ use Drupal\user\UserInterface;
* This acts as a wrapper for an entity translation object, encapsulating the
* logic needed to retrieve translation metadata.
*/
-interface ContentTranslationMetadataWrapperInterface extends EntityChangedInterface {
+interface ContentTranslationMetadataWrapperInterface {
/**
* Retrieves the source language for this translation.
@@ -110,6 +108,14 @@ interface ContentTranslationMetadataWrapperInterface extends EntityChangedInterf
public function setCreatedTime($timestamp);
/**
+ * Returns the timestamp of the last entity change from current translation.
+ *
+ * @return int
+ * The timestamp of the last entity save operation.
+ */
+ public function getChangedTime();
+
+ /**
* Sets the translation modification timestamp.
*
* @param int $timestamp
diff --git a/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php b/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php
index 0db24c8..2b4e3cc 100644
--- a/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php
+++ b/core/modules/content_translation/src/Tests/ContentTestTranslationUITest.php
@@ -26,7 +26,7 @@ class ContentTestTranslationUITest extends ContentTranslationUITest {
*/
protected function setUp() {
// Use the entity_test_mul as this has multilingual property support.
- $this->entityTypeId = 'entity_test_mul';
+ $this->entityTypeId = 'entity_test_mul_changed';
parent::setUp();
}
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITest.php b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
index 5a7429c..8d12742 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
@@ -42,6 +42,7 @@ abstract class ContentTranslationUITest extends ContentTranslationTestBase {
$this->doTestPublishedStatus();
$this->doTestAuthoringInfo();
$this->doTestTranslationEdit();
+ $this->doTestTranslationChanged();
$this->doTestTranslationDeletion();
}
@@ -366,6 +367,19 @@ abstract class ContentTranslationUITest extends ContentTranslationTestBase {
}
/**
+ * Returns the name of the field that implements the changed timestamp.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity being tested.
+ *
+ * @return string
+ * The the field name.
+ */
+ protected function getChangedFieldName($entity) {
+ return $entity->hasField('content_translation_changed') ? 'content_translation_changed' : 'changed';
+ }
+
+ /**
* Tests edit content translation.
*/
protected function doTestTranslationEdit() {
@@ -384,4 +398,71 @@ abstract class ContentTranslationUITest extends ContentTranslationTestBase {
}
}
+ /**
+ * Tests the basic translation workflow.
+ */
+ protected function doTestTranslationChanged() {
+ $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+ $changed_field_name = $this->getChangedFieldName($entity);
+ $definition = $entity->getFieldDefinition($changed_field_name);
+ $config = $definition->getConfig($entity->bundle());
+
+ foreach ([FALSE, TRUE] as $translatable_changed_field) {
+ if ($definition->isTranslatable()) {
+ // For entities defining a translatable changed field we want to test
+ // the correct behavior of that field even if the translatability is
+ // revoked. In that case the changed timestamp should be synchronized
+ // across all translations.
+ $config->setTranslatable($translatable_changed_field);
+ $config->save();
+ }
+ elseif ($translatable_changed_field) {
+ // For entities defining a non-translatable changed field we cannot
+ // declare the field as translatable on the fly by modifying its config
+ // because the schema doesn't support this.
+ break;
+ }
+
+ foreach ($entity->getTranslationLanguages() as $language) {
+ // Ensure different timestamps.
+ sleep(1);
+
+ $langcode = $language->getId();
+
+ $edit = array(
+ $this->fieldName . '[0][value]' => $this->randomString(),
+ );
+ $edit_path = $entity->urlInfo('edit-form', array('language' => $language));
+ $this->drupalPostForm($edit_path, $edit, $this->getFormSubmitAction($entity, $langcode));
+
+ $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+ $this->assertEqual(
+ $entity->getChangedTimeAcrossTranslations(), $entity->getTranslation($langcode)->getChangedTime(),
+ format_string('Changed time for language %language is the latest change over all languages.', array('%language' => $language->getName()))
+ );
+ }
+
+ $timestamps = array();
+ foreach ($entity->getTranslationLanguages() as $language) {
+ $next_timestamp = $entity->getTranslation($language->getId())->getChangedTime();
+ if (!in_array($next_timestamp, $timestamps)) {
+ $timestamps[] = $next_timestamp;
+ }
+ }
+
+ if ($translatable_changed_field) {
+ $this->assertEqual(
+ count($timestamps), count($entity->getTranslationLanguages()),
+ 'All timestamps from all languages are different.'
+ );
+ }
+ else {
+ $this->assertEqual(
+ count($timestamps), 1,
+ 'All timestamps from all languages are identical.'
+ );
+ }
+ }
+ }
+
}
diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php
index ff38ab3..8a5bd42 100644
--- a/core/modules/file/src/Entity/File.php
+++ b/core/modules/file/src/Entity/File.php
@@ -8,10 +8,10 @@
namespace Drupal\file\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Language\LanguageInterface;
use Drupal\file\FileInterface;
use Drupal\user\UserInterface;
@@ -38,6 +38,8 @@ use Drupal\user\UserInterface;
*/
class File extends ContentEntityBase implements FileInterface {
+ use EntityChangedTrait;
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/file/src/Tests/SaveTest.php b/core/modules/file/src/Tests/SaveTest.php
index cc12515..666b08d 100644
--- a/core/modules/file/src/Tests/SaveTest.php
+++ b/core/modules/file/src/Tests/SaveTest.php
@@ -22,8 +22,6 @@ class SaveTest extends FileManagedUnitTestBase {
'filename' => 'druplicon.txt',
'uri' => 'public://druplicon.txt',
'filemime' => 'text/plain',
- 'created' => 1,
- 'changed' => 1,
'status' => FILE_STATUS_PERMANENT,
));
file_put_contents($file->getFileUri(), 'hello world');
@@ -64,8 +62,6 @@ class SaveTest extends FileManagedUnitTestBase {
'filename' => 'DRUPLICON.txt',
'uri' => 'public://DRUPLICON.txt',
'filemime' => 'text/plain',
- 'created' => 1,
- 'changed' => 1,
'status' => FILE_STATUS_PERMANENT,
));
file_put_contents($uppercase_file->getFileUri(), 'hello world');
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
index d6610a0..abbc694 100644
--- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -8,6 +8,7 @@
namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -52,6 +53,8 @@ use Drupal\menu_link_content\MenuLinkContentInterface;
*/
class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterface {
+ use EntityChangedTrait;
+
/**
* A flag for whether this entity is wrapped in a plugin instance.
*
@@ -377,7 +380,8 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
- ->setDescription(t('The time that the menu link was last edited.'));
+ ->setDescription(t('The time that the menu link was last edited.'))
+ ->setTranslatable(TRUE);
return $fields;
}
diff --git a/core/modules/menu_link_content/src/Tests/LinksTest.php b/core/modules/menu_link_content/src/Tests/LinksTest.php
index 86a484b..81d48a4 100644
--- a/core/modules/menu_link_content/src/Tests/LinksTest.php
+++ b/core/modules/menu_link_content/src/Tests/LinksTest.php
@@ -138,7 +138,7 @@ class LinksTest extends WebTestBase {
'title' => 'Test Link',
);
$link->link->options = $options;
- $link->changed->value = REQUEST_TIME - 5;
+ $link->changed->value = 0;
$link->save();
// Make sure the changed timestamp is updated.
$this->assertEqual($link->getChangedTime(), REQUEST_TIME, 'Changing a menu link sets "changed" timestamp.');
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index d33d6b2..4f8f610 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -743,8 +743,8 @@ function node_page_title(NodeInterface $node) {
* @param $nid
* The ID of a node.
* @param string $langcode
- * (optional) The language the node has been last modified in. Defaults to the
- * node language.
+ * (optional) The language to get the last changed time for. If omitted, the
+ * last changed time across all translations will be returned.
*
* @return string
* A unix timestamp indicating the last time the node was changed.
@@ -753,8 +753,9 @@ function node_page_title(NodeInterface $node) {
* for validation, which will be done by EntityChangedConstraintValidator.
*/
function node_last_changed($nid, $langcode = NULL) {
- $changed = \Drupal::entityManager()->getStorage('node')->loadUnchanged($nid)->getChangedTime();
- return $changed ? $changed : FALSE;
+ $node = \Drupal::entityManager()->getStorage('node')->loadUnchanged($nid);
+ $changed = $langcode ? $node->getTranslation($langcode)->getChangedTime() : $node->getChangedTimeAcrossTranslations();
+ return $changed ?: FALSE;
}
/**
diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index 7ae8f36..6dd1229 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -8,6 +8,7 @@
namespace Drupal\node\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -69,6 +70,8 @@ use Drupal\user\UserInterface;
*/
class Node extends ContentEntityBase implements NodeInterface {
+ use EntityChangedTrait;
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index 44fccbf..d88393d 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -289,7 +289,7 @@ class NodeForm extends ContentEntityForm {
public function validate(array $form, FormStateInterface $form_state) {
$node = parent::validate($form, $form_state);
- if ($node->id() && (node_last_changed($node->id(), $this->getFormLangcode($form_state)) > $node->getChangedTime())) {
+ if ($node->id() && (node_last_changed($node->id()) > $node->getChangedTimeAcrossTranslations())) {
$form_state->setErrorByName('changed', $this->t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.'));
}
diff --git a/core/modules/node/src/Tests/NodeLastChangedTest.php b/core/modules/node/src/Tests/NodeLastChangedTest.php
index 4444cc1..23fa4b4 100644
--- a/core/modules/node/src/Tests/NodeLastChangedTest.php
+++ b/core/modules/node/src/Tests/NodeLastChangedTest.php
@@ -38,7 +38,7 @@ class NodeLastChangedTest extends KernelTestBase {
// Test node last changed timestamp.
$changed_timestamp = node_last_changed($node->id());
- $this->assertEqual($changed_timestamp, $node->getChangedTime(), 'Expected last changed timestamp returned.');
+ $this->assertEqual($changed_timestamp, $node->getChangedTimeAcrossTranslations(), 'Expected last changed timestamp returned.');
$changed_timestamp = node_last_changed($node->id(), $node->language()->getId());
$this->assertEqual($changed_timestamp, $node->getChangedTime(), 'Expected last changed timestamp returned.');
diff --git a/core/modules/node/src/Tests/NodeSaveTest.php b/core/modules/node/src/Tests/NodeSaveTest.php
index c9dc9c0..c045e2a 100644
--- a/core/modules/node/src/Tests/NodeSaveTest.php
+++ b/core/modules/node/src/Tests/NodeSaveTest.php
@@ -120,8 +120,8 @@ class NodeSaveTest extends NodeTestBase {
entity_create('node', $edit)->save();
$node = $this->drupalGetNodeByTitle($edit['title']);
- $this->assertEqual($node->getCreatedTime(), 280299600, 'Creating a node uses user-set "created" timestamp.');
- $this->assertNotEqual($node->getChangedTime(), 979534800, 'Creating a node does not use user-set "changed" timestamp.');
+ $this->assertEqual($node->getCreatedTime(), 280299600, 'Creating a node programmatically uses programmatically set "created" timestamp.');
+ $this->assertEqual($node->getChangedTime(), 979534800, 'Creating a node programmatically uses programmatically set "changed" timestamp.');
// Update the timestamps.
$node->setCreatedTime(979534800);
@@ -130,7 +130,10 @@ class NodeSaveTest extends NodeTestBase {
$node->save();
$node = $this->drupalGetNodeByTitle($edit['title'], TRUE);
$this->assertEqual($node->getCreatedTime(), 979534800, 'Updating a node uses user-set "created" timestamp.');
- $this->assertNotEqual($node->getChangedTime(), 280299600, 'Updating a node does not use user-set "changed" timestamp.');
+ // Allowing setting changed timestamps is required, see
+ // Drupal\content_translation\ContentTranslationMetadataWrapper::setChangedTime($timestamp)
+ // for example.
+ $this->assertEqual($node->getChangedTime(), 280299600, 'Updating a node uses user-set "changed" timestamp.');
}
/**
diff --git a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
index 9f81cb5..81b67d3 100644
--- a/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
+++ b/core/modules/shortcut/src/Tests/ShortcutTranslationUITest.php
@@ -8,6 +8,7 @@
namespace Drupal\shortcut\Tests;
use Drupal\content_translation\Tests\ContentTranslationUITest;
+use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Language\Language;
/**
@@ -102,4 +103,16 @@ class ShortcutTranslationUITest extends ContentTranslationUITest {
}
}
+ /**
+ * Tests the basic translation workflow.
+ */
+ protected function doTestTranslationChanged() {
+ $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+
+ $this->assertFalse(
+ $entity instanceof EntityChangedInterface,
+ format_string('%entity is not implementing EntityChangedInterface.' , array('%entity' => $this->entityTypeId))
+ );
+ }
+
}
diff --git a/core/modules/system/src/Tests/Entity/ContentEntityChangedTest.php b/core/modules/system/src/Tests/Entity/ContentEntityChangedTest.php
new file mode 100644
index 0000000..13ec8b4
--- /dev/null
+++ b/core/modules/system/src/Tests/Entity/ContentEntityChangedTest.php
@@ -0,0 +1,363 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\system\Tests\Entity\ContentEntityChangedTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\language\Entity\ConfigurableLanguage;
+
+/**
+ * Tests basic EntityChangedInterface functionality.
+ *
+ * @group Entity
+ */
+class ContentEntityChangedTest extends EntityUnitTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['language', 'user', 'system', 'field', 'text', 'filter', 'entity_test'];
+
+ /**
+ * @inheritdoc
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // Enable an additional language.
+ ConfigurableLanguage::createFromLangcode('de')->save();
+
+ $this->installEntitySchema('entity_test_mul_changed');
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ }
+
+ /**
+ * Tests basic EntityChangedInterface functionality.
+ */
+ public function testChanged() {
+ $user1 = $this->createUser();
+ $user2 = $this->createUser();
+
+ // Create some test entities.
+ $entity = entity_create('entity_test_mul_changed', array(
+ 'name' => $this->randomString(),
+ 'user_id' => $user1->id(),
+ 'language' => 'en',
+ ));
+ $entity->save();
+
+ $this->assertTrue(
+ $entity->getChangedTime() >= REQUEST_TIME,
+ 'Changed time of original language is valid.'
+ );
+
+ // We can't assert equality here because the created time is set to the
+ // request time, while instances of ChangedTestItem use the current
+ // timestamp every time. Therefor we check if the changed timestamp is
+ // between the created time and now.
+ $this->assertTrue(
+ ($entity->getChangedTime() >= $entity->get('created')->value) &&
+ (($entity->getChangedTime() - $entity->get('created')->value) <= time() - REQUEST_TIME),
+ 'Changed and created time of original language can be assumed to be identical.'
+ );
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of original language is the same as changed time across all translations.'
+ );
+
+ $changed_en = $entity->getChangedTime();
+
+ $german = $entity->addTranslation('de');
+
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertTrue(
+ $german->getChangedTime() > $entity->getChangedTime(),
+ 'Changed time of the German translation is newer then the original language.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of the German translation is the newest time across all translations.'
+ );
+
+ $changed_de = $german->getChangedTime();
+
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $changed_de,
+ 'Changed time of the German translation did not change.'
+ );
+
+ $entity->setOwner($user2);
+
+ $entity->save();
+
+ $this->assertTrue(
+ $entity->getChangedTime() > $changed_en,
+ 'Changed time of original language did change.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $changed_de,
+ 'Changed time of the German translation did not change.'
+ );
+
+ $this->assertTrue(
+ $entity->getChangedTime() > $german->getChangedTime(),
+ 'Changed time of original language is newer then the German translation.'
+ );
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of the original language is the newest time across all translations.'
+ );
+
+ $changed_en = $entity->getChangedTime();
+
+ // Save entity without any changes.
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $changed_de,
+ 'Changed time of the German translation did not change.'
+ );
+
+ // At this point the changed time of the original language (en) is newer
+ // than the changed time of the German translation. Now test that entity
+ // queries work as expected.
+ $storage = $this->entityManager->getStorage('entity_test_mul_changed');
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en)->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time of original language.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en, '=', 'en')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time of original language by setting the original language as condition.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_de, '=', 'en')->execute();
+
+ $this->assertFalse(
+ $ids,
+ 'There\'s no original entity stored having the changed time of the German translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en)->condition('default_langcode', '1')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time of default language.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_de)->condition('default_langcode', '1')->execute();
+
+ $this->assertFalse(
+ $ids,
+ 'There\'s no entity stored using the default language having the changed time of the German translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_de)->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time of the German translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_de, '=', 'de')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time of the German translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en, '=', 'de')->execute();
+
+ $this->assertFalse(
+ $ids,
+ 'There\'s no German translation stored having the changed time of the original language.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_de, '>')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time regardless of translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en, '<')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time regardless of translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', 0, '>')->execute();
+
+ $this->assertEqual(
+ reset($ids), $entity->id(),
+ 'Entity query can access changed time regardless of translation.'
+ );
+
+ $query = $storage->getQuery();
+ $ids = $query->condition('changed', $changed_en, '>')->execute();
+
+ $this->assertFalse(
+ $ids,
+ 'Entity query can access changed time regardless of translation.'
+ );
+ }
+
+ /**
+ * Tests revisionable EntityChangedInterface functionality.
+ */
+ public function testRevisionChanged() {
+ $user1 = $this->createUser();
+ $user2 = $this->createUser();
+
+ // Create some test entities.
+ $entity = entity_create('entity_test_mulrev_changed', array(
+ 'name' => $this->randomString(),
+ 'user_id' => $user1->id(),
+ 'language' => 'en',
+ ));
+ $entity->save();
+
+ $this->assertTrue(
+ $entity->getChangedTime() >= REQUEST_TIME,
+ 'Changed time of original language is valid.'
+ );
+
+ // We can't assert equality here because the created time is set to the
+ // request time while instances of ChangedTestItem use the current
+ // timestamp every time.
+ $this->assertTrue(
+ ($entity->getChangedTime() >= $entity->get('created')->value) &&
+ (($entity->getChangedTime() - $entity->get('created')->value) <= time() - REQUEST_TIME),
+ 'Changed and created time of original language can be assumed to be identical.'
+ );
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of original language is the same as changed time across all translations.'
+ );
+
+ $changed_en = $entity->getChangedTime();
+
+ $entity->setNewRevision();
+ // Save entity without any changes but create new revision.
+ $entity->save();
+ // A new revision without any changes should not set a new changed time.
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $entity->setOwner($user2);
+ $entity->setNewRevision();
+ $entity->save();
+
+ $this->assertTrue(
+ $entity->getChangedTime() > $changed_en,
+ 'Changed time of original language has been updated by new revision.'
+ );
+
+ $changed_en = $entity->getChangedTime();
+
+ $entity->addTranslation('de');
+
+ $german = $entity->getTranslation('de');
+
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertTrue(
+ $german->getChangedTime() > $entity->getChangedTime(),
+ 'Changed time of the German translation is newer then the original language.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of the German translation is the newest time across all translations.'
+ );
+
+ $changed_de = $german->getChangedTime();
+
+ $entity->setNewRevision();
+ // Save entity without any changes but create new revision.
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $changed_de,
+ 'Changed time of the German translation did not change.'
+ );
+
+ $german->setOwner($user2);
+ $entity->setNewRevision();
+ $entity->save();
+
+ $this->assertEqual(
+ $entity->getChangedTime(), $changed_en,
+ 'Changed time of original language did not change.'
+ );
+
+ $this->assertTrue(
+ $german->getChangedTime() > $changed_de,
+ 'Changed time of the German translation did change.'
+ );
+
+ $this->assertEqual(
+ $german->getChangedTime(), $entity->getChangedTimeAcrossTranslations(),
+ 'Changed time of the German translation is the newest time across all translations.'
+ );
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 4b26cfb..dd44c1f 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -58,6 +58,7 @@ function entity_test_entity_types($filter = NULL) {
if ($filter != ENTITY_TEST_TYPES_REVISABLE) {
$types[] = 'entity_test_mul';
$types[] = 'entity_test_mul_langcode_key';
+ $types[] = 'entity_test_mul_changed';
}
if ($filter != ENTITY_TEST_TYPES_MULTILINGUAL) {
$types[] = 'entity_test_rev';
@@ -66,6 +67,7 @@ function entity_test_entity_types($filter = NULL) {
$types[] = 'entity_test_base_field_display';
}
$types[] = 'entity_test_mulrev';
+ $types[] = 'entity_test_mulrev_changed';
return array_combine($types, $types);
}
@@ -459,6 +461,20 @@ function entity_test_entity_test_mul_translation_delete(EntityInterface $transla
/**
* Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mulrev'.
*/
+function entity_test_entity_test_mul_changed_translation_insert(EntityInterface $translation) {
+ _entity_test_record_hooks('entity_test_mul_changed_translation_insert', $translation->language()->getId());
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_translation_delete().
+ */
+function entity_test_entity_test_mul_changed_translation_delete(EntityInterface $translation) {
+ _entity_test_record_hooks('entity_test_mul_changed_translation_delete', $translation->language()->getId());
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_translation_insert().
+ */
function entity_test_entity_test_mulrev_translation_insert(EntityInterface $translation) {
_entity_test_record_hooks('entity_test_mulrev_translation_insert', $translation->language()->getId());
}
@@ -471,6 +487,20 @@ function entity_test_entity_test_mulrev_translation_delete(EntityInterface $tran
}
/**
+ * Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mulrev'.
+ */
+function entity_test_entity_test_mulrev_changed_translation_insert(EntityInterface $translation) {
+ _entity_test_record_hooks('entity_test_mulrev_changed_translation_insert', $translation->language()->getId());
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_translation_delete().
+ */
+function entity_test_entity_test_mulrev_changed_translation_delete(EntityInterface $translation) {
+ _entity_test_record_hooks('entity_test_mulrev_changed_translation_delete', $translation->language()->getId());
+}
+
+/**
* Implements hook_ENTITY_TYPE_translation_insert() for 'entity_test_mul_langcode_key'.
*/
function entity_test_entity_test_mul_langcode_key_translation_insert(EntityInterface $translation) {
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
index 9f92e7a..61200f7 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
@@ -7,6 +7,7 @@
namespace Drupal\entity_test\Entity;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -32,6 +33,8 @@ use Drupal\Core\Field\BaseFieldDefinition;
*/
class EntityTestConstraints extends EntityTest implements EntityChangedInterface {
+ use EntityChangedTrait;
+
/**
* {@inheritdoc}
*/
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
new file mode 100644
index 0000000..93af073
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Entity\EntityTestMulDefaultValue.
+ */
+
+namespace Drupal\entity_test\Entity;
+
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Defines the test entity class.
+ *
+ * @ContentEntityType(
+ * id = "entity_test_mul_changed",
+ * label = @Translation("Test entity - data table"),
+ * handlers = {
+ * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
+ * "access" = "Drupal\entity_test\EntityTestAccessControlHandler",
+ * "form" = {
+ * "default" = "Drupal\entity_test\EntityTestForm",
+ * "delete" = "Drupal\entity_test\EntityTestDeleteForm"
+ * },
+ * "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ * "views_data" = "Drupal\views\EntityViewsData"
+ * },
+ * base_table = "entity_test_mul_changed",
+ * data_table = "entity_test_mul_changed_property",
+ * translatable = TRUE,
+ * entity_keys = {
+ * "id" = "id",
+ * "uuid" = "uuid",
+ * "bundle" = "type",
+ * "label" = "name",
+ * "langcode" = "langcode"
+ * },
+ * links = {
+ * "canonical" = "/entity_test_mul_changed/manage/{entity_test_mul_changed}",
+ * "edit-form" = "/entity_test_mul_changed/manage/{entity_test_mul_changed}",
+ * "delete-form" = "/entity_test/delete/entity_test_mul_changed/{entity_test_mul_changed}",
+ * },
+ * field_ui_base_route = "entity.entity_test_mul_changed.admin_form",
+ * )
+ */
+class EntityTestMulChanged extends EntityTestMul implements EntityChangedInterface {
+
+ use EntityChangedTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ $fields = parent::baseFieldDefinitions($entity_type);
+
+ $fields['changed'] = BaseFieldDefinition::create('changed_test')
+ ->setLabel(t('Changed'))
+ ->setDescription(t('The time that the entity was last edited.'))
+ ->setTranslatable(TRUE);
+
+ return $fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save() {
+ // Ensure a new timestamp.
+ sleep(1);
+ parent::save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getChangedTime() {
+ return $this->get('changed')->value;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php
new file mode 100644
index 0000000..c2f572f
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Entity\EntityTestMulRevChanged.
+ */
+
+namespace Drupal\entity_test\Entity;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+
+/**
+ * Defines the test entity class.
+ *
+ * @ContentEntityType(
+ * id = "entity_test_mulrev_changed",
+ * label = @Translation("Test entity - revisions and data table"),
+ * handlers = {
+ * "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
+ * "access" = "Drupal\entity_test\EntityTestAccessControlHandler",
+ * "form" = {
+ * "default" = "Drupal\entity_test\EntityTestForm",
+ * "delete" = "Drupal\entity_test\EntityTestDeleteForm"
+ * },
+ * "translation" = "Drupal\content_translation\ContentTranslationHandler",
+ * "views_data" = "Drupal\views\EntityViewsData"
+ * },
+ * base_table = "entity_test_mulrev_changed",
+ * data_table = "entity_test_mulrev_changed_property",
+ * revision_table = "entity_test_mulrev_changed_revision",
+ * revision_data_table = "entity_test_mulrev_changed_property_revision",
+ * translatable = TRUE,
+ * entity_keys = {
+ * "id" = "id",
+ * "uuid" = "uuid",
+ * "bundle" = "type",
+ * "revision" = "revision_id",
+ * "label" = "name",
+ * "langcode" = "langcode",
+ * },
+ * links = {
+ * "canonical" = "/entity_test_mulrev_changed/manage/{entity_test_mulrev_changed}",
+ * "delete-form" = "/entity_test/delete/entity_test_mulrev_changed/{entity_test_mulrev_changed}",
+ * "edit-form" = "/entity_test_mulrev_changed/manage/{entity_test_mulrev_changed}",
+ * "revision" = "/entity_test_mulrev_changed/{entity_test_mulrev_changed}/revision/{entity_test_mulrev_changed_revision}/view",
+ * }
+ * )
+ */
+class EntityTestMulRevChanged extends EntityTestMulChanged {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ $fields = parent::baseFieldDefinitions($entity_type);
+
+ $fields['revision_id'] = BaseFieldDefinition::create('integer')
+ ->setLabel(t('Revision ID'))
+ ->setDescription(t('The version id of the test entity.'))
+ ->setReadOnly(TRUE)
+ ->setSetting('unsigned', TRUE);
+
+ $fields['langcode']->setRevisionable(TRUE);
+ $fields['name']->setRevisionable(TRUE);
+ $fields['user_id']->setRevisionable(TRUE);
+ $fields['changed']->setRevisionable(TRUE);
+
+ return $fields;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ChangedTestItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ChangedTestItem.php
new file mode 100644
index 0000000..012134d
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ChangedTestItem.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\entity_test\Plugin\Field\FieldType\ChangedTestItem.
+ */
+
+namespace Drupal\entity_test\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\Plugin\Field\FieldType\ChangedItem;
+
+/**
+ * Defines the 'changed_test' entity field type.
+ *
+ * Wraps Drupal\Core\Field\Plugin\Field\FieldType\ChangedItem.
+ *
+ * @FieldType(
+ * id = "changed_test",
+ * label = @Translation("Last changed"),
+ * description = @Translation("An entity field containing a UNIX timestamp of when the entity has been last updated."),
+ * no_ui = TRUE,
+ * list_class = "\Drupal\Core\Field\ChangedFieldItemList"
+ * )
+ *
+ * @see \Drupal\Core\Entity\EntityChangedInterface
+ */
+class ChangedTestItem extends ChangedItem {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave() {
+ parent::preSave();
+
+ if ($this->value == REQUEST_TIME) {
+ // During a test the request time is immutable. To allow tests of the
+ // algorithm of
+ // Drupal\Core\Field\Plugin\Field\FieldType\ChangedItem::preSave() we need
+ // to set a real time value here.
+ $this->value = time();
+ }
+ }
+
+}
diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index 7ca9dd7..a88c3a4 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -8,6 +8,7 @@
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -56,6 +57,8 @@ use Drupal\taxonomy\TermInterface;
*/
class Term extends ContentEntityBase implements TermInterface {
+ use EntityChangedTrait;
+
/**
* {@inheritdoc}
*/
@@ -178,7 +181,8 @@ class Term extends ContentEntityBase implements TermInterface {
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
- ->setDescription(t('The time that the term was last edited.'));
+ ->setDescription(t('The time that the term was last edited.'))
+ ->setTranslatable(TRUE);
return $fields;
}
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index fa4db6f..05b642a 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -8,6 +8,7 @@
namespace Drupal\user\Entity;
use Drupal\Core\Entity\ContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -62,6 +63,8 @@ use Drupal\user\UserInterface;
*/
class User extends ContentEntityBase implements UserInterface {
+ use EntityChangedTrait;
+
/**
* Stores a reference for a reusable anonymous user entity.
*
@@ -502,7 +505,8 @@ class User extends ContentEntityBase implements UserInterface {
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
- ->setDescription(t('The time that the user was last edited.'));
+ ->setDescription(t('The time that the user was last edited.'))
+ ->setTranslatable(TRUE);
$fields['access'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last access'))