Problem/Motivation

When translating an entity through the translation overview the current entity is being prepared for translation, where all the field values are transferred from the source to the target translation, including also the 'created' and the 'uid' fields.

This leads to the same created timestamps and entity owners for each translation, except the current user edits these fields by herself. But the user might not have the right to access this section in the edit form.

Proposed resolution

Prepare the 'created' and 'uid' fields properly for the new translation.

The current state:

  public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
    /* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
    $source_translation = $entity->getTranslation($source->getId());
    $entity->addTranslation($target->getId(), $source_translation->toArray());
  }

And the new state:

  public function prepareTranslation(ContentEntityInterface $entity, LanguageInterface $source, LanguageInterface $target) {
    /* @var \Drupal\Core\Entity\ContentEntityInterface $source_translation */
    $source_translation = $entity->getTranslation($source->getId());
    $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());

    /** @var \Drupal\user\UserInterface $user */
    $user = $this->entityManager()->getStorage('user')->load($this->currentUser()->id());
    $metadata = $this->manager->getTranslationMetadata($target_translation);

    // Update the translation author to current user, as well the translation
    // creation time. 
    $metadata->setAuthor($user);
    $metadata->setCreatedTime(REQUEST_TIME);
  }

Remaining tasks

none

User interface changes

The "Authoring information" section in the translation edit form is now prepopulated with properly set 'created' and 'uid' fields, prepared for the target translation. Applies only if the fields for the current entity bundle are translatable.

API changes

The metadata setter functions for the fields author, published, created and changed now by default will skip setting the field, if it is not translatable, which might be the case, if the user decided to turn off translatability for one of these fields on a specific entity bundle. Such a field will then be left to the entity builders to update it.

In ContentTranslationHandler we check if author, published, created and changed fields exist and their storage definition is translatable, otherwise we create our own content translation fields to use for the metadata.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov’s picture

plach’s picture

Assigned: Unassigned » Gábor Hojtsy

The proposed changes make sense to me, thanks! However, it'd be good to get also Gabor's feedback about them, since the author part is a functional change.

Marking needs work for the missing test coverage.

plach’s picture

Status: Needs review » Needs work

.

hchonov’s picture

plach’s picture

@hchonov:

Gabor seems to be unavailable atm, are you willing to provide some additional test coverage?

hchonov’s picture

@plach

It's fine, I'll take care of the tests later today.

plach’s picture

+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -57,7 +57,14 @@ public static function create(ContainerInterface $container) {
+    $target_translation = $entity->addTranslation($target->getId(), $source_translation->toArray());
+
+    /** @var \Drupal\user\UserInterface $user */
+    $user = entity_load('user', \Drupal::currentUser()->id());
+
+    $metadata = $this->manager->getTranslationMetadata($target_translation);
+    $metadata->setAuthor($user);
+    $metadata->setCreatedTime(REQUEST_TIME);
...
 

Actually I think it makes sense to do this only if the fields are translatable, otherwise we'd be overriding the original author/created time.

For that we need new methods on the translation handler, as that encapsulates field definition handling.

plach’s picture

Assigned: Gábor Hojtsy » Unassigned

.

hchonov’s picture

Maybe something like this one for created and author:

  public function getCreatedFieldName() {
    return $this->translation->hasField('content_translation_created') ? 'content_translation_created' : 'created';
  }

And then check if the field is translatable in prepareTranslation before updating it's value?

plach’s picture

+++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
@@ -57,7 +57,14 @@ public static function create(ContainerInterface $container) {
+    $user = entity_load('user', \Drupal::currentUser()->id());

Let's inject the entity manager and the current user services and use them here.

hchonov’s picture

The ContentTranslationController extends from ControllerBase and thus have the getter methods for the entity manager and the current user services, which then are being get from the container.

It's just my fault I've overseen this one.

hchonov’s picture

So yesterday we decided with @plach in IRC that the setters in the ContentTranslationMedataWrapper should check if the fields are translatable before updating them, otherwise throw an exception. This should work for all the fields but for the owner field.
For all the fields but for the owner we assumme they should be named by convention and so always have the field name and can check if the field is translatable, but for the owner we check if the entity implements the EntityOwnerInterface and if so, we use it's function to update the field and do not know the field name and so are not able to check if it is translatable.

Minutes ago we discussed in IRC and agreed, that we should remove the check for EntityOwnerInterface and check if like we check e.g. the created field:

array_key_exists('created', \Drupal::entityManager()->getLastInstalledFieldStorageDefinitions($this->entityType->id()));

By doing so, we will know for sure the field name, which will be used by the ContentTranslationMetadataWrapper and can first check if it is translatable.

Further @plach suggested, that we put all the fields required by content translation in the entity keys, but for that one we have to create a new issue and later if it get's in core update the logic in content translation accordingly.

Gábor Hojtsy’s picture

However, it'd be good to get also Gabor's feedback about them, since the author part is a functional change.

Can you explain the change?

hchonov’s picture

Let's consinder the following use case. An editor creates a node. The timestamp of the creation and the owner are set automatically, if the editor want's to she has the permission to alter these values in the entity form.
Now a translator without a permission to alter the owner and created fields creates a new translation.
Both fields owner and created are translatable, but now the created timestamp for the new translation will remain as the created timestamp of the source translation, as well the owner. So this way we lose the information who created the new translation and when.

By creating a new entity these fields are set automaticaly, so why not now when creating a new translation? If you just call addTranslation, these field will be updated automaticaly, but what we do now in prepareTranslation is to manually take them over from the source translation.

I am trying to achieve a better history, so that the translator does not have to have the permission to edit these values. Actually when creating a translation the translator should not even think about setting these values, they should be updated accordingly.

hchonov’s picture

Status: Needs work » Needs review
FileSize
3.79 KB

Here just the adjusted test, which should fail.

hchonov’s picture

And here the adjusted test together with the new logic.

The last submitted patch, 15: prepareTranslation_2472621-15.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 16: prepareTranslation_2472621-16.patch, failed testing.

hchonov’s picture

While I was working on the patch I came to the conclusion, that we actually do not need exceptions if the fields are not translatable, but instead we have to create content translation fields for the author and for the creation timestamp, which are translatable. I've changed that in the new patch.

And for the owner field definition provided by content translation we need a default value set to the current user. Otherwise it happens on entity creation, that the original translation does not have any author set for the field provided by content translation.

hchonov’s picture

Status: Needs work » Needs review
plach’s picture

Status: Needs review » Needs work

I'm fine with this new approach, however for consistency we should check field storage definition translatability for all metadata fields. Moreover we still need checks + exceptions in the MW, because the fact that a field storage definition is marked as translatable, only means it supports multilingual values. We need runtime checks on the field definiton, which tells us whether the field is actually translatable for the bundle it is attached on.

  1. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -71,6 +79,7 @@ public function __construct(EntityTypeInterface $entity_type, LanguageManagerInt
    +    $this->fieldStorageDefinitions = \Drupal::entityManager()->getLastInstalledFieldStorageDefinitions($this->entityTypeId);
    

    I know we already have this code in HEAD, but as I mentioned in IRC the plan was to remove it. If it's going to stay we need to properly inject the entity manager, see ContentTranslationHandler::createInstance().

  2. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -144,13 +154,22 @@ public function getFieldDefinitions() {
    +    //Check for field named uid, only in case the entity implements the
    +    //EntityOwnerInterface. This helps to exclude cases, where the uid is
    +    //defined as field name but is not meant to be an owner field
    +    //e.g. the User entity
    

    Coding standards mandate a space after // and a trailing dot. Moreover all lines should wrap as close to column 80 as possible.

  3. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -144,13 +154,22 @@ public function getFieldDefinitions() {
    +    if (is_subclass_of($this->entityType->getClass(), '\Drupal\user\EntityOwnerInterface')) {
    +      if (array_key_exists('uid', $this->fieldStorageDefinitions) && $this->fieldStorageDefinitions['uid']->isTranslatable()) {
    +        return TRUE;
    +      }
    +    }
    +    return FALSE;
    

    Can we just return the value of the three conditions (joined via AND)? It's more compact and readable.
    Moreover we can just do $this->entityType->isSubclassOf('\Drupal\user\EntityOwnerInterface'), which is a bit shorter and readable itself.

  4. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -174,13 +193,17 @@ protected function hasChangedTime() {
    +   * Checks whether the entity type supports translatable
    +   * creation time natively.
    

    According to coding standards this should be a single line. I think we can get rid of translatable as it is now implicit.

  5. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -174,13 +193,17 @@ protected function hasChangedTime() {
    +    if (array_key_exists('created', $this->fieldStorageDefinitions) && $this->fieldStorageDefinitions['created']->isTranslatable()) {
    

    As above, let's just return the test value instead of adding the if.

  6. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -626,4 +649,15 @@ protected function entityFormTitle(EntityInterface $entity) {
    +   * @see ::baseFieldDefinitions()
    

    This is unrelated and should be removed.

  7. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -626,4 +649,15 @@ protected function entityFormTitle(EntityInterface $entity) {
    +  public static function getCurrentUserId() {
    

    I think it would be better to rename this to ::getDefaultOwnerId(), IMO the fact that we return the current user is an implementation detail and should not leak into the public API.

  8. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -626,4 +649,15 @@ protected function entityFormTitle(EntityInterface $entity) {
    +    return array(\Drupal::currentUser()->id());
    

    We should inject also the current user service, see above.

  9. +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
    @@ -57,7 +57,14 @@ public static function create(ContainerInterface $container) {
    +    $user = entity_load('user', $this->currentUser()->id());
    

    We should use the (injected) entity manager here, global functions should be avoided in OO code as they make it harder to unit test.

  10. +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
    @@ -57,7 +57,14 @@ public static function create(ContainerInterface $container) {
    +
    

    Surplus blank line, please remove it :)

  11. +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
    @@ -73,12 +73,39 @@ protected function doTestBasicTranslation() {
    +    $current_user = $this->loggedInUser;
    +    $new_user = $this->drupalCreateUser($this->getTranslatorPermissions(), 'test_translation_author');
    +    $this->drupalLogin($new_user);
    +
    ...
    +    //switch back to the previous user
    +    $this->drupalLogin($current_user);
    

    Why do we need to create a new user? ContentTranslationTestBase already provides three test users, if we wish to test that translations are created with a different user, we could log in with $this->editor at the very beginning of the test and then log in with $this->translator here. No need for all this code then.

  12. +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
    @@ -73,12 +73,39 @@ protected function doTestBasicTranslation() {
    +    //Get the entity and reset it's cache, so that the new translation gets
    +    //the updated values
    

    Space after //, missing trailing dot.

  13. +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITest.php
    @@ -73,12 +73,39 @@ protected function doTestBasicTranslation() {
    +
    

    Surplus blank line :)

hchonov’s picture

Status: Needs work » Needs review
FileSize
20.54 KB

@plach thanks for the review.

I've reworked the patch according to your review.

Just one notice - getDefaultOwnerId is a static function and as such it can not access the injected current user service, so I have to use the global function.

plach’s picture

Just one notice - getDefaultOwnerId is a static function and as such it can not access the injected current user service, so I have to use the global function.

Oh, you're right. Can you please post an interdiff (a diff between #19 and #22)? We always do that at every iteration when dealing with patches bigger then a couple of lines, it makes things easier to review.

hchonov’s picture

FileSize
21.17 KB

And here the interdiff between #19 and #22.
Thanks for the suggestion!

plach’s picture

Wow, that' s bigger than the patch, I'm afraid it won't be so useful then, but thanks anyway ;)

I'll have a look to this later.

plach’s picture

Status: Needs review » Needs work

This is starting to look really good :)

I have only a few more minor remarks and just one actual objection (see 2):

  1. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -180,7 +211,7 @@ protected function hasChangedTime() {
    +    return array_key_exists('created', $this->fieldStorageDefinitions) && $this->fieldStorageDefinitions['created']->isTranslatable();
    

    Can we add a protected helper method to avoid repeating this logic for all fields?

  2. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -533,10 +563,25 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    // It is possible, that not all the metadata fields are translatable on the
    +    // current entity bundle, so just catch the exception and continue.
    +    try {
    

    Really? I thought the point of introducing exceptions here was to make the site blow up in the user face when it's configured the wrong way, exactly to avoid silently failing.

    Well, maybe a friendlier way could be adding some checks during the validation of the field translatability settings form.

  3. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -626,4 +671,13 @@ protected function entityFormTitle(EntityInterface $entity) {
    +    return array(\Drupal::currentUser()->id());
    

    Why an array? We now use square brackets, btw.

  4. +++ b/core/modules/content_translation/src/ContentTranslationMetadataWrapper.php
    @@ -138,8 +139,24 @@ public function getChangedTime() {
    +   * Private helper method to check if a bundle field is translatable before
    +   * updating it and throw an exception if not.
    +   *
    +   * @param $field_name
    +   * @throws \Drupal\content_translation\Exception\NonTranslatableBundleFieldException
    

    Coding standards mandate a single introductory line, shorter than 80 chars, followed by an optional longer text.

    Also, we are missing @param type, @param description, and exception description.

  5. +++ b/core/modules/content_translation/src/ContentTranslationMetadataWrapper.php
    @@ -138,8 +139,24 @@ public function getChangedTime() {
    +  private function ensureBundleFieldIsTranslatableBeforeUpdating($field_name) {
    

    What about just ::checkFieldTranslatability()? It's a bit long now, and bundle field has a slightly different meaning in the Entity Field API.

    Also, no private visibility allowed, except for vary rare cases, let's make this protected.

  6. +++ b/core/modules/content_translation/src/Exception/NonTranslatableBundleFieldException.php
    @@ -0,0 +1,14 @@
    + * is called for a non translatable bundle field.
    ...
    +class NonTranslatableBundleFieldException extends \RuntimeException {}
    

    Per above, let's remove the bundle word, and just leave field. Field definitions are per-bundle, no need to specify it. Bundle fields are fields that may be attached only to certain bundles, unlike base fields (like nid) which are attached to all bundles of a certain entity type. The most common example of bundle fields are our beloved Field UI fields :)

hchonov’s picture

Status: Needs work » Needs review
FileSize
21.01 KB
11.82 KB

Fixed the minor remarks.

Really? I thought the point of introducing exceptions here was to make the site blow up in the user face when it's configured the wrong way, exactly to avoid silently failing.

The idea is that the user might turn off translatability for some of the fields on specific bundles, in these cases we still want that she can translate content without exceptions. This leaves the handling/updating of these non-translatable fields to the entity itself, which is also good, as the entity builders will update the submited values, they are just not going to be set through the content translation metadata wrapper setters funtions.

The exceptions are there in case the user writes a custom code and uses the CT MetadataWrapper setters for fields on an entity bundles, where the translatability is turned off. In these cases the user should use the entity's specific setters instead.

Status: Needs review » Needs work

The last submitted patch, 27: prepareTranslation_2472621-27.patch, failed testing.

plach’s picture

Very sorry for losing track of this, DrupalCon sent it completely out of my radar :(

I have a last small set of remarks and then we should be ready to go, I think:

  1. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -533,10 +578,25 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    // It is possible, that not all the metadata fields are translatable on the
    +    // current entity bundle, so just catch the exception and continue.
    

    Ok, now makes sense to me. We should add a brief explanation also in this comment though.

  2. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -533,10 +578,25 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    try {
    +      $metadata->setAuthor(!empty($values['uid']) ? User::load($values['uid']) : User::load(0));
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    +    try {
    +      $metadata->setPublished(!empty($values['status']));
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    +    try {
    +      $metadata->setCreatedTime(!empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME);
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    +    try {
    +      $metadata->setChangedTime(REQUEST_TIME);
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    

    I'm wondering whether it would make sense to add a $skip_untranslatable = FALSE parameter to skip the exception on fail (and the set operation of course), to make this code cleaner.

  3. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -626,4 +686,13 @@ protected function entityFormTitle(EntityInterface $entity) {
    +    return [\Drupal::currentUser()->id()];
    

    Is it mandatory to return an array of values? The method name is a bit confusing wrt the returned value atm.

  4. +++ b/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php
    @@ -69,6 +69,9 @@ public function getAuthor();
    +   * @throws \Drupal\content_translation\Exception\NonTranslatableFieldException
    +   *   If the author bundle field is not translatable.
    

    Here and below: let's remove the bundle term here too, we can use "field definition" instead.

  5. +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
    @@ -57,7 +58,23 @@ public static function create(ContainerInterface $container) {
    +    try {
    +      $metadata->setAuthor($user);
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    +    try {
    +      $metadata->setCreatedTime(REQUEST_TIME);
    +    }
    +    catch (NonTranslatableFieldException $e) {}
    

    If we add the parameter we should use it also here.

hchonov’s picture

Status: Needs work » Needs review
FileSize
23.26 KB
19.96 KB

A rework based on the last comment by @plach.

plach’s picture

Priority: Normal » Major
Status: Needs review » Needs work
FileSize
7.36 KB

Sorry, it seems the NonTranslatableFieldException class file was lost in the reroll, but the patch is green, which means we are lacking test coverage for that. Can we add a few lines in the test so that a metadata field is configured as not translatable and then test the setter with the two values for $skip_untranslatable?

Other than that this looks ready to me. However I'm attaching a diff with a few nitpick fixes and some suggested changes for comments and PHP docs, which looked a bit verbose to me. Feel free to include it in the next patch if you are ok with it.

plach’s picture

We should also update the issue summary with the implemented solution.

hchonov’s picture

Status: Needs work » Needs review
FileSize
27.13 KB
11.2 KB

@plach thanks a lot for the patience, the support and the reviews!

A reworked patch with the corrected nitpicks, the exception and a test case for it.

hchonov’s picture

Issue summary: View changes
hchonov’s picture

Issue summary: View changes
plach’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, thanks!

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: -Needs issue summary update
  1. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -534,10 +579,16 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
    +    $metadata->setAuthor(!empty($values['uid']) ? User::load($values['uid']) : User::load(0), TRUE);
    +    $metadata->setPublished(!empty($values['status']), TRUE);
    +    $metadata->setCreatedTime(!empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME, TRUE);
    +    $metadata->setChangedTime(REQUEST_TIME, TRUE);
    

    Are these set methods ever called with FALSE anywhere in the code (apart from the test)? Perhaps we don't need the flag - Martin Fowler would argue that flags are a code smell - should just have two methods. Reading back through the comments @plach wrote:

    Really? I thought the point of introducing exceptions here was to make the site blow up in the user face when it's configured the wrong way, exactly to avoid silently failing.

    Hmmm... can the user configure the site through the front end and end up with exceptions? That's not exactly user friendly.

  2. +++ b/core/modules/content_translation/src/ContentTranslationHandler.php
    @@ -634,4 +685,13 @@ protected function entityFormTitle(EntityInterface $entity) {
    +  public static function getDefaultOwnerId() {
    +    return \Drupal::currentUser()->id();
    +  }
    
    +++ b/core/modules/content_translation/src/Controller/ContentTranslationController.php
    @@ -57,7 +57,18 @@ public static function create(ContainerInterface $container) {
    +    // Update the translation author to current user, as well the translation
    +    // creation time. It is possible, that not all the metadata fields are
    +    // translatable on the current entity bundle, in that case we just avoid
    +    // overriding the original values.
    +    $metadata->setAuthor($user, TRUE);
    +    $metadata->setCreatedTime(REQUEST_TIME, TRUE);
       }
    

    Afaics one of these is setting of author ID is tested - not completely sure which one - does the other have test coverage?

  3. +++ b/core/modules/content_translation/src/ContentTranslationMetadataWrapperInterface.php
    @@ -84,10 +91,17 @@ public function isPublished();
    -  public function setPublished($published);
    +  public function setPublished($published, $skip_untranslatable = FALSE);
    
    @@ -120,9 +141,16 @@ public function getChangedTime();
    -  public function setChangedTime($timestamp);
    +  public function setChangedTime($timestamp, $skip_untranslatable = FALSE);
    

    I think these changes are missing test coverage... I can only see test coverage for setCreatedTime and setAuthor. I might be missing something though.

  4. +++ b/core/modules/content_translation/src/Tests/ContentTranslationMetadataFieldsTest.php
    @@ -0,0 +1,109 @@
    +  protected function testSkipUntranslatable() {
    

    Test methods should be public

  5. +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
    @@ -80,6 +85,39 @@ protected function doTestBasicTranslation() {
    +      $message = format_string('Author of the target translation @langcode correctly stored for translatable owner field.',
    +        array('@langcode' => $langcode));
    +      $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->translator->id(), $message);
    ...
    +      $message = format_string('Author of the target translation @target different from the author of the source translation @source for translatable owner field.',
    +        array('@target' => $langcode, '@source' => $default_langcode));
    +      $this->assertNotEqual($metadata_target_translation->getAuthor()->id(), $metadata_source_translation->getAuthor()->id(), $message);
    ...
    +      $message = format_string('Translation creation timestamp of the target translation @target is newer than the creation timestamp of the source translation @source for translatable created field.',
    +        array('@target' => $langcode, '@source' => $default_langcode));
    +      $this->assertTrue($metadata_target_translation->getCreatedTime() > $metadata_source_translation->getCreatedTime(), $message);
    

    I think the creation of the $message variable in these instances doesn't really help - might as well just have a one line assertion which includes the message creation. Also format_string should just be SafeMarkup::format().

  6. +++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
    @@ -80,6 +85,39 @@ protected function doTestBasicTranslation() {
    +      $message = format_string('Author of the entity remained untouched after translation for non translatable owner field.');
    +      $this->assertEqual($metadata_target_translation->getAuthor()->id(), $this->editor->id(), $message);
    ...
    +      $message = format_string('Creation timestamp of the entity remained untouched after translation for non translatable created field.');
    +      $this->assertEqual($metadata_target_translation->getCreatedTime(), $metadata_source_translation->getCreatedTime(), $message);
    

    There's no point to using format_string() here and the creating a $message variable is not needed either.

hchonov’s picture

Are these set methods ever called with FALSE anywhere in the code (apart from the test)? Perhaps we don't need the flag - Martin Fowler would argue that flags are a code smell - should just have two methods.

No, we do not call the methods with FALSE in the code in order to not make the site blow up in the face of the user, if a field translation for a specific bundle is turned off. But FALSE is the default value, so that if the setters are used by a contrib module, the developer should pay attention what she is doing and be aware that non translatable fields will not be updated by the content translation setters. Making two methods sounds better, but then I would suggest, let's just remove the exception and make only one method without the $skip_untranslatable parameter and this method will by default skip setting an untranslatable field? This addresses only fields that are provided by the entity's base fields definitions.
So for example:

  1. if we find the 'status' field and it is translatable, we will use this field for the metadata. If the user turns off the content translation for a specific bundle for this field, then we just skip updating it's value, but the entity builders will update it.
  2. Else we create our own content_translation_published field, which is always translatable and we use it for the metadata.

Afaics one of these is setting of author ID is tested - not completely sure which one - does the other have test coverage?

Both fields author and created get tested in ContentTranslationUITestBase::doTestBasicTranslation in both use cases (translatable and non translatable.)

I think these changes are missing test coverage... I can only see test coverage for setCreatedTime and setAuthor. I might be missing something though.

Then I would take the node entity instead of the entity_test_mul for testing, because it has all these fields and they are all translatable and test the four setters for both use cases (translatable and non translatable). Of course depending on what we decide to do with the setters and the exception.

@alexpott thanks for the remarks! I do not think the other ones need to be commented, I'll just make the changes.

plach’s picture

@alexpott:

Are these set methods ever called with FALSE anywhere in the code (apart from the test)? Perhaps we don't need the flag - Martin Fowler would argue that flags are a code smell - should just have two methods.

Personally I think having a bunch of ::setXOnlyIfTranslatable() methods wouldn't be great for DX either.

Reading back through the comments @plach wrote:

Really? I thought the point of introducing exceptions here was to make the site blow up in the user face when it's configured the wrong way, exactly to avoid silently failing.

Hmmm... can the user configure the site through the front end and end up with exceptions? That's not exactly user friendly.

It's meant to be developer friendly: by default methods will blow up with an expeption so code not taking all possible scenarios into account will break. It should be the developer responsibility to mark its code as "safe" by setting the flag to TRUE.

@hchonov:

Let's just remove the exception and make only one method without the $skip_untranslatable parameter and this method will by default skip setting an untranslatable field? This addresses only fields that are provided by the entity's base fields definitions.

I agree this could be a viable solution if @alexpott is not convinced by our explanations.

hchonov’s picture

Status: Needs work » Needs review
FileSize
23.61 KB
23.82 KB

Fixed the remarks addressed by @alexpott and as discussed with @plach and @alexpott removed the Exception and the parameter $skip_untranslatable. Now by default non translatable fields will not be set by ContetTranslation, but only by the entity builders.

plach’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs tests +language-content, +sprint
FileSize
4.67 KB
23.79 KB

Looks great, thanks!

I made just a few code style/cosmetic adjustments, nothing that should prevent me from RTBC-ing this :)

alexpott’s picture

Issue summary: View changes
Issue tags: +Needs issue summary update

We just need an issue summary update to bring it up-to-date with the latest change. The API section is out of date.

hchonov’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Updated the Issue Summary.

plach’s picture

Issue summary: View changes

Made a small clarification in the IS.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I'm pleased we're fixing this major bug with no disruptive API change. Thanks for updating the issue summary. Committed 6588593 and pushed to 8.0.x. Thanks!

  • alexpott committed 6588593 on 8.0.x
    Issue #2472621 by hchonov, plach: Translatable entity 'created' and 'uid...
Gábor Hojtsy’s picture

Issue tags: -sprint

Yay, amazing! Thanks all!

plach’s picture

Yay!

@hchonov:

Thanks for your patience and perseverance!

hchonov++

hchonov’s picture

@plach++
Thanks for your help and support. I've learned a lot during my first significant work on a core patch and I am really thankful for the patience :).

Status: Fixed » Closed (fixed)

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