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.
Comment | File | Size | Author |
---|---|---|---|
#42 | prepareTranslation_2472621-42.patch | 23.79 KB | plach |
#42 | prepareTranslation_2472621-42.interdiff.txt | 4.67 KB | plach |
#41 | 34-41-interdiff.txt | 23.82 KB | hchonov |
#41 | prepareTranslation_2472621-41.patch | 23.61 KB | hchonov |
#34 | 31-34-interdiff.txt | 11.2 KB | hchonov |
Comments
Comment #1
hchonovComment #2
plachThe 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.
Comment #3
plach.
Comment #4
hchonovComment #5
plach@hchonov:
Gabor seems to be unavailable atm, are you willing to provide some additional test coverage?
Comment #6
hchonov@plach
It's fine, I'll take care of the tests later today.
Comment #7
plachActually 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.
Comment #8
plach.
Comment #9
hchonovMaybe something like this one for created and author:
And then check if the field is translatable in prepareTranslation before updating it's value?
Comment #10
plachLet's inject the entity manager and the current user services and use them here.
Comment #11
hchonovThe 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.
Comment #12
hchonovSo 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:
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.
Comment #13
Gábor HojtsyCan you explain the change?
Comment #14
hchonovLet'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.
Comment #15
hchonovHere just the adjusted test, which should fail.
Comment #16
hchonovAnd here the adjusted test together with the new logic.
Comment #19
hchonovWhile 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.
Comment #20
hchonovComment #21
plachI'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.
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()
.Coding standards mandate a space after
//
and a trailing dot. Moreover all lines should wrap as close to column 80 as possible.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.According to coding standards this should be a single line. I think we can get rid of
translatable
as it is now implicit.As above, let's just return the test value instead of adding the if.
This is unrelated and should be removed.
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.We should inject also the current user service, see above.
We should use the (injected) entity manager here, global functions should be avoided in OO code as they make it harder to unit test.
Surplus blank line, please remove it :)
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.Space after
//
, missing trailing dot.Surplus blank line :)
Comment #22
hchonov@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.
Comment #23
plachOh, 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.
Comment #24
hchonovAnd here the interdiff between #19 and #22.
Thanks for the suggestion!
Comment #25
plachWow, 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.
Comment #26
plachThis is starting to look really good :)
I have only a few more minor remarks and just one actual objection (see 2):
Can we add a protected helper method to avoid repeating this logic for all fields?
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.
Why an array? We now use square brackets, btw.
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.
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.
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 :)Comment #27
hchonovFixed the minor remarks.
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.
Comment #30
plachVery 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:
Ok, now makes sense to me. We should add a brief explanation also in this comment though.
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.Is it mandatory to return an array of values? The method name is a bit confusing wrt the returned value atm.
Here and below: let's remove the bundle term here too, we can use "field definition" instead.
If we add the parameter we should use it also here.
Comment #31
hchonovA rework based on the last comment by @plach.
Comment #32
plachSorry, 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.
Comment #33
plachWe should also update the issue summary with the implemented solution.
Comment #34
hchonov@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.
Comment #35
hchonovComment #36
hchonovComment #37
plachAwesome, thanks!
Comment #38
alexpottAre 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:
Hmmm... can the user configure the site through the front end and end up with exceptions? That's not exactly user friendly.
Afaics one of these is setting of author ID is tested - not completely sure which one - does the other have test coverage?
I think these changes are missing test coverage... I can only see test coverage for setCreatedTime and setAuthor. I might be missing something though.
Test methods should be public
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().
There's no point to using format_string() here and the creating a $message variable is not needed either.
Comment #39
hchonovNo, 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:
Both fields author and created get tested in ContentTranslationUITestBase::doTestBasicTranslation in both use cases (translatable and non translatable.)
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.
Comment #40
plach@alexpott:
Personally I think having a bunch of
::setXOnlyIfTranslatable()
methods wouldn't be great for DX either.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:
I agree this could be a viable solution if @alexpott is not convinced by our explanations.
Comment #41
hchonovFixed 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.
Comment #42
plachLooks great, thanks!
I made just a few code style/cosmetic adjustments, nothing that should prevent me from RTBC-ing this :)
Comment #43
alexpottWe just need an issue summary update to bring it up-to-date with the latest change. The API section is out of date.
Comment #44
hchonovUpdated the Issue Summary.
Comment #45
plachMade a small clarification in the IS.
Comment #46
alexpottI'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!
Comment #48
Gábor HojtsyYay, amazing! Thanks all!
Comment #49
plachYay!
@hchonov:
Thanks for your patience and perseverance!
hchonov++
Comment #50
hchonov@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 :).