Problem/Motivation

Since #2428795: Translatable entity 'changed' timestamps are not working at all got in core there is a mixed behaviour. Previously when saving an entity through the API and the UI would have resulted always in an updated changed timestamp. Now the ChangedItem introduces some behavioural logic so that it checks if the entity changed and only then updates its value.

This means accessing Entity Edit through the UI and directly saving it without changes will not update the changed timestamp anymore.... unless the module Content Translation is enabled, which will in an Entity Builder call setChangedTime(REQUEST_TIME) for the current translation. So here we should decide if we want that the changed timestamp is always updated on UI Save and then move the logic for that out of the Content Translation module so that it is not dependent on an optional module.

Some tests in core with no Content Translation module enabled were excepting that calling Entity Edit on the UI and saving withouth changes will result in a change and because this behaviour is missing now there was a neccessary change in #2480921: Make the node entity's revision_log untranslatable in a test to force the entity to be updated.

Proposed resolution

If the entity implements the EntityChangedInterface, update its changed timestamp after the entity is validated and before the entity is saved in ContentEntityForm::submitForm.

#2534512: EntityChangedConstraint needs to compare changed time of all translations showed that the EntityChangedConstraintValidator is malfunctioning and does not allow old translations to be saved, which we've seen in the current issue after we've changed the behaviour that the changed timestamp is updated after the entity is validated. So the right functioning EntityChangedConstraintValidator is needed here, where we have also test coverage for it. That is why we are integrating it in the current patch for this issue and setting the other one as duplicate.

Remaining tasks

none

User interface changes

none

API changes

The EntityChangedInterface now defines also the method setChangedTime.
The EntityChangedTrait implements also the setChangedTime method.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

plach’s picture

hchonov’s picture

Issue summary: View changes
hchonov’s picture

Issue tags: +blocker

Tagging as blocker as #2480921: Make the node entity's revision_log untranslatable is postponed on this.

alexpott’s picture

I think pressing save in the UI should result in the time being updated. Unless we don't save the entity and reject the save and inform the user nothing has changed so not saving.

hchonov’s picture

so, we just agreed with @alexpott in IRC that we should update the timestamp on UI save.

hchonov’s picture

Status: Active » Needs review
FileSize
2.84 KB

here providing test only patch and setting a default value for the revision_log field of the Node, as otherwise it is causing a change on node save. The current test should fail.

hchonov’s picture

And here the test including the fix.

The last submitted patch, 6: 2506213_test_only-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: 2506213_including_test-7.patch, failed testing.

hchonov queued 6: 2506213_test_only-6.patch for re-testing.

Status: Needs work » Needs review

The last submitted patch, 6: 2506213_test_only-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: 2506213_including_test-7.patch, failed testing.

hchonov queued 6: 2506213_test_only-6.patch for re-testing.

Status: Needs work » Needs review

The last submitted patch, 6: 2506213_test_only-6.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 7: 2506213_including_test-7.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
3.36 KB

Unfortunatelly as I added a default value to the node revision_log field one REST test for node creation is failing, as there it was forgotten to remove the revision_log field for non admin users, so here I am adding also these chages.

hchonov’s picture

And now both the test and the fix.

The last submitted patch, 18: 2506213_test_only-18.patch, failed testing.

hchonov’s picture

Issue summary: View changes
mkalkbrenner’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/block_content/src/Entity/BlockContent.php
    @@ -221,6 +221,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/comment/src/Entity/Comment.php
    @@ -531,6 +531,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/file/src/Entity/File.php
    @@ -120,6 +120,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
    @@ -142,6 +142,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/node/src/Entity/Node.php
    @@ -228,6 +228,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
    @@ -54,4 +54,11 @@ public function getChangedTime() {
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
    @@ -80,4 +80,11 @@ public function getChangedTime() {
    +  /**
    +   * {@inheritdoc}
    +   */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/taxonomy/src/Entity/Term.php
    @@ -197,6 +197,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    
    +++ b/core/modules/user/src/Entity/User.php
    @@ -385,6 +385,14 @@ public function getChangedTime() {
       /**
        * {@inheritdoc}
        */
    +  public function setChangedTime($timestamp) {
    +    $this->set('changed', $timestamp);
    +    return $this;
    +  }
    

    If we agree on providing the function setChangedTime() it should be part of the EntityChangedTrait. BTW it seems that getChangedTime() could be moved to this trait as well now. That could be added to this patch from my point of view.

  2. +++ b/core/modules/node/src/Entity/Node.php
    @@ -486,6 +494,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    +      ->setDefaultValue('')
    

    Is this the right issue to fix that?

  3. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,74 @@
    +class NodeFormSaveChangedTimeTest extends WebTestBase {
    

    This test only covers Node and its UI. But the patch introduces a behavior for all entities implementing the EntityChangedInterface.

    Therefor I recommend to extend
    ContentEntityChangedTest first which is supposed to test the functions declared by EntityChangedInterface on the API level.

    The UI should be covered by an additional function of ContentTranslationUITestBase. This way it will cover all the entities that are already affected by this patch.

hchonov’s picture

Status: Needs work » Needs review
FileSize
12.69 KB

As suggested by @mkalkbrenner I've put setChangedTime and getChangedTime in the EntityChangedTrait.

+++ b/core/modules/node/src/Entity/Node.php
@@ -486,6 +494,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ ->setDefaultValue('')

Is this the right issue to fix that?

The problem occurs here and the new test fails because of this case. If requested so I will create another issue.

Why should we create an test exactly in ContentTranslationUITestBase? At the moment my changes have nothig to do with the content translation module.

mkalkbrenner’s picture

Why should we create an test exactly in ContentTranslationUITestBase? At the moment my changes have nothig to do with the content translation module.

It's a suggestion because the tests implemented there are automatically executed for all entity types that are affected by this patch while the test included in the patch only covers nodes!

alexpott’s picture

I gave a framework manager point of view in #4

webchick’s picture

Agreed with alexpott in #4.

Berdir’s picture

Status: Needs review » Needs work
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -579,6 +579,10 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
+    // Each ContentEntityForm will register also an entity builder method to
+    // update the changed time of the entity, but here we have to guarantee that
+    // the metadata field is updated as well, if not the entity's own changed
+    // field is being used for the metadata.
     $metadata->setChangedTime(REQUEST_TIME);

This is wrong.

Changing the changed time breaks the changed validation that we have on forms that happens *later*. This means that an old entity is always considered up to date. We *must* use the submitted, original changed time for that validation and only change it when the validation succeeded.

This is hiding the currently broken EntityChangedConstraintValidator that is comparing the saved changed date of all translations with the changed date of the current translation, which makes it impossible to save old translations.

mkalkbrenner’s picture

Changing the changed time breaks the changed validation that we have on forms that happens *later*. This means that an old entity is always considered up to date. We *must* use the submitted, original changed time for that validation and only change it when the validation succeeded.

@berdir is right.

Whenever an entity is saved via the UI it's original changed timestamp must checked if it's still the newest (or if a different user modified the entity in the meantime).
That's what the EntityChangedConstraint normally does. Therefor this issue here is also indirectly blocked by #2513094: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone break field reference to parent entity.

Nevertheless we need to have a look at Content Translation if it deals correctly with that situation.

hchonov’s picture

Status: Needs work » Needs review
FileSize
12.57 KB

So reworked the patch. Now ContentTranslationHandler::entityFormEntityBuild will update the changed timestamp on the metadata only if the entity has a metadata field.

The original entity changed field now will be updated in ContentEntityForm::submitForm after the validation has ran.

Status: Needs review » Needs work

The last submitted patch, 29: 2506213_including_test-29.patch, failed testing.

hchonov’s picture

Now including the patch from #2534512: EntityChangedConstraint needs to compare changed time of all translations to reduce the test fails as with this changes now we bring up the broken EntityChangedConstraintValidator to fail.

Status: Needs review » Needs work

The last submitted patch, 31: 2506213_including_test_including-patch-from-2534512-31.patch, failed testing.

hchonov’s picture

So the patch from #29 passes localy with applied both patches for #2534512: EntityChangedConstraint needs to compare changed time of all translations and for #2513094: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone break field reference to parent entity.

So postponing this one on the both issues. As soon as they are fixed the patch from #29 should be put for retesting and it should pass.

hchonov’s picture

mkalkbrenner’s picture

  1. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,74 @@
    +class NodeFormSaveChangedTimeTest extends WebTestBase {
    

    I still don't like that we only add test coverage for nodes. The patch modifies ContentEntityForm.

mkalkbrenner’s picture

Status: Postponed » Needs review
FileSize
2.4 KB
14.32 KB

As explained earlier I copied the test case to ContentTranslationUITestBase.
Without really changing the test we now have a great coverage of content entities in combination with content_translation. That's correct because module is also affected by this patch.

Let's see which side effects the additional test might discover.

mkalkbrenner’s picture

Status: Needs review » Postponed

Status: Postponed » Needs work

The last submitted patch, 36: 2506213_enhanced_test_coverage.patch, failed testing.

hchonov’s picture

Status: Needs work » Postponed
mkalkbrenner’s picture

Status: Postponed » Needs review
FileSize
908 bytes
15.41 KB

As @berdir suggested in https://www.drupal.org/node/2534512#comment-10156016 I merged that patch to see how the tests work here now.

Status: Needs review » Needs work

The last submitted patch, 40: 2506213_merged_2534512_2.patch, failed testing.

Status: Needs work » Needs review
hchonov’s picture

The test passes locally, so putting for retesting.

Status: Needs review » Needs work

The last submitted patch, 40: 2506213_merged_2534512_2.patch, failed testing.

hchonov’s picture

Status: Needs work » Needs review
FileSize
15.6 KB
2.35 KB

So the problem is, that ContentTranslationUITestBase::doTestChangedTimeAfterSaveWithoutChanges is checking the changed field, but not all entities, which extend ContentTranslationUITestBase are implementing the EntityChangedInterface. So we should restrict the test ContentTranslationUITestBase::doTestChangedTimeAfterSaveWithoutChanges only to entites, which are implementing the EntityChangedInterface.

hchonov’s picture

So here we proved the false implementation of the EntityChangedConstraint by integrating the patch from #2534512: EntityChangedConstraint needs to compare changed time of all translations .
Now removing the fix for the EntityChangedConstraint and RTBC-ing the other issue, so that both patches land separately in core.
The patch should fail now, and it should be put for retest as soon as #2534512: EntityChangedConstraint needs to compare changed time of all translations gets commited.

Status: Needs review » Needs work

The last submitted patch, 46: 2506213-46.patch, failed testing.

Berdir’s picture

The thing is that the other issue won't be RTBC'd without test coverage. test coverage that this issue provides, at least in the form integration tests.

As I suggested in #2534512-13: EntityChangedConstraint needs to compare changed time of all translations, if we merge that issue into this, then we have a better chance of it getting committed I think. It's a one line fix, and we can explain in the issue summary why that is needed and close the other issue as a duplicate.

PS: You want to reference to other issues with "[ #2534512 ]" (without the spaces). That way, you get things like automatic coloring. If you are using dreditor (which you should) then you can just paste the issue URL and then press Tab and it will convert it automatically. I'm always irritated that your links don't have the color I'd expect, took me a while that you are probably building them by hand.

hchonov’s picture

Title: Decide how the changed timestamp should behave on UI save » Update the changed timestamp on UI save
Issue summary: View changes
Status: Needs work » Needs review
FileSize
15.6 KB

Changed title and updated the IS.

I've set #2534512: EntityChangedConstraint needs to compare changed time of all translations as duplicated, so the patch here is fixing also the problem with the EntityChangedConstraintValidator.

mkalkbrenner’s picture

Title: Update the changed timestamp on UI save » Update content entity changed timestamp on UI save
hchonov’s picture

Category: Task » Bug report

hchonov queued 49: 2506213-49.patch for re-testing.

hchonov’s picture

Issue summary: View changes
Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

The changes proposed here make sense to me, the issue summary is up to date and @Berdir's concerns have been resolved. Let's get this in, so we can fix #2480921: Make the node entity's revision_log untranslatable too finally :)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 49: 2506213-49.patch, failed testing.

Status: Needs work » Needs review

hchonov queued 49: 2506213-49.patch for re-testing.

hchonov’s picture

Status: Needs review » Reviewed & tested by the community

Setting again as RTBC as there was some error on the testbot and the patch failed, but after retesting it is passing again.

kataku’s picture

Sorry if this is just piling on to what you already know but i was desperate for this fix, I've tested this patch on 8.0.x head and it worked for me without error :)

plach’s picture

A few nitpicks, if this happens to be rerolled:

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
    @@ -269,4 +278,18 @@ public function updateFormLangcode($entity_type_id, EntityInterface $entity, arr
    +    if ($entity->getEntityType()->isSubclassOf('Drupal\Core\Entity\EntityChangedInterface')) {
    
    +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
    @@ -531,4 +532,30 @@ protected function doTestTranslationChanged() {
    +    if ($entity->getEntityType()->isSubclassOf('Drupal\Core\Entity\EntityChangedInterface')) {
    

    We can use .class here, I think

  2. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,74 @@
    +  protected function setUp() {
    

    Missing PHP doc

hchonov’s picture

@plach:
1. unfortunately we cannot use getClass as for example for the node entity the class will be "Drupal\node\Entity\Node" and here we check if the entity is implementing the interface "Drupal\Core\Entity\EntityChangedInterface" .

2. added the mising PHP doc.

Thanks for the nitpicks :)

plach’s picture

I meant EntityChangedInterface::class :)

hchonov’s picture

Oh, clearing that nitpick then as well... :).

The tests are using a different namespace, so there either we have to put the EntityChangedInterface in the usage definition or use the whole path when calling isSubclassOf, so I think using the whole path is fine in the test.

catch’s picture

Status: Reviewed & tested by the community » Needs review

All minor but a couple of real questions.

  1. +++ b/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
    @@ -28,4 +28,26 @@ public function getChangedTimeAcrossTranslations() {
    +  public function getChangedTime() {
    

    Nice adding this here and removing from the entity classes :)

  2. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -579,7 +579,12 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    // the entity validation has ran. But if we set an metadata field for the
    

    a metadata

  3. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -579,7 +579,12 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    // translation metadata we have to update it here.
    

    Why do we have to?

  4. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,77 @@
    +    $this->assertEqual($changed_timestamp, $node->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.');
    

    Double quotes?

  5. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,77 @@
    +    debug($node->getChangedTime(), 'second time');
    

    Left over debug?

  6. +++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
    @@ -0,0 +1,77 @@
    +    sleep(2);
    

    Why sleep(2) here and sleep(1) in the other test?

hchonov’s picture

@catch:

Thanks for the remarks.

2. Corrected.

3. ContentEntityForm::submit will update the changed timestamp on submit after the entity has been validated, so that it does not break the EntityChanged constraint validator. I think it is better to have the same logic in CT for the changed metadata field even if there is no such constraint defined for the metatada field at the moment and moved now the logic in a submit function instead using an entity builder for this like in the previous case, which is being executed before the entity is saved. So I am just reproducing the logic from the Form API. I wrote a better comment in the patch as well.

4. Corrected.

5. Removed left over debugs.

6. Unintentionally. Now both are sleep(1).

plach’s picture

Status: Needs review » Reviewed & tested by the community

#64 seems to address @catch's review.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Yep. Committed/pushed to 8.0.x, thanks!

  • catch committed a6c2e2b on 8.0.x
    Issue #2506213 by hchonov, mkalkbrenner: Update content entity changed...
larowlan’s picture

Status: Fixed » Needs work
Issue tags: +Needs change record

This was an API break, so needs a change record.
Anyone with an entity implementing EntityChangedInterface would now get fatals until they add the method or the trait.

Thanks

hchonov’s picture

Status: Needs work » Needs review

@larowlan:

I just created a draft change record - EntityChangedInterface now also defines the function setChangedTime.
Should I publish it now?

larowlan’s picture

Status: Needs review » Fixed
Issue tags: -Needs change record

thanks!

published

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.