Problem/Motivation
When you have a node translated into two languages (p.e. English and German), both „versions" can be edited separately. But revisions are created/stored across all language versions and not independently.
This causes a situation like this:
The English editor (uid 1) creates a node and creates 3 revisions of it afterwards by editing the node 3 times.
Now the German editor (uid 2) creates a translation from this node and starts to work on it for several days and creates 5 revisions.
In the meantime the English editor also creates a new revision just to fix a small typo.
The revision table basically looks like this:
nid vid revision_uid
1 1 1
1 2 1
1 3 1
1 4 2
1 5 2
1 6 2
1 7 1 <= The typo fix by the English editor
1 8 2
1 9 2
Now the German editor decides to revert his latest 2 changes and to go back to vid 6.
In this case he not only reverts his changes in the German translation but the typo fix in the English translation as well!
This did not happen in Drupal 7 using core’s content translation!
Proposed resolution
Reverting a revision just for affected translations is technically possible in the current architecture in core by the following strategy.
- Use the latest revision of an entity as base.
- Remove the translations that should be reverted to an older revision.
- Re-add the affected translations loaded from the targeted old revision.
Therefor a new boolean field is required to indicate if a translation is modified within a revision.
Remaining tasks
Commit the existing Patch.
User interface changes
None in this patch. But based on this patch it will be possible to build a better UI to give the user more control over reverting translations and revisions. The follow up issue for that is #2465907: Node revision UI reverts multiple languages when only one language should be reverted.
API changes
Drupal\Core\Entity\ContentEntityInterface now defines 3 additional methods that are implemented by Drupal\Core\Entity\ContentEntityBase:
interface ContentEntityInterface extends \Traversable, FieldableEntityInterface, RevisionableInterface, TranslatableInterface {
/**
* Determines if the current translation of the entity has unsaved changes.
*
* If the entity is translatable only translatable fields will be checked for
* changes.
*
* @return bool
* TRUE if the current translation of the entity has changes.
*/
public function hasTranslationChanges();
/**
* Marks the current revision translation as affected.
*
* @param bool $affected
* The flag value.
*/
public function setRevisionTranslationAffected($affected);
/**
* Checks whether the current translation is affected by the current revision.
*
* @return bool
* TRUE if the entity object is affected by the current revision, FALSE
* otherwise.
*/
public function isRevisionTranslationAffected();
}
Module developers now have to define the new base field on translatable and revisionable entities, for example like
Drupal\node\Entity\Node:
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
...
$fields['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Revision translation affected'))
->setDescription(t('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
...
}
| Comment | File | Size | Author |
|---|---|---|---|
| #155 | et-node_revisions-2453153-155.patch | 39.67 KB | plach |
| #137 | et-node_revisions-2453153-137.test.patch | 3.83 KB | plach |
Comments
Comment #1
gábor hojtsyComment #2
mkalkbrennerComment #3
andypostmanager should be injected
$this->t()
could be exposed as link template
needs @inheritdoc
revisionIds()is only now used bycore/modules/quickedit/src/Tests/QuickEditLoadingTest.php:153Comment #4
mkalkbrenner1. manager should be injected
=> done. I'll upload a new patch after the following questions have been answered
2. $this->t()
3. could be exposed as link template
=> I used class NodeRevisionRevertForm as template. These things are implemented the same ways as in the existing class. Should my patch fix both or should that be a separate patch?
BTW can you point me to an example for such "link templates"?
4. needs @inheritdoc
=> done
5. RevisionIds() is only now used by core/modules/quickedit/src/Tests/QuickEditLoadingTest.php:153
=> I'm not sure if this function should be removed. What do others think about it?
Comment #5
mkalkbrennerI created a new patch that contains all suggestions from #3.
Comment #6
mkalkbrennerI added some tests to the patch. What else is needed to get this in?
Comment #7
pinoloI have tested it successfully and code looks good.
Comment #8
alexpottI would be great to have input from either @plach or @berdir
Comment #9
alexpott@berdir in IRC said "plach is probably more qualified to review that" ... assigning to @plach
Comment #10
schnitzel commentedwow, very nice idea and solution to a definitely big problem.
I tested the UI and found one small issue:

I would not show any language in the current revision, as it could imply that the current revision only has one language, which is actually not true. And as we cannot do anything with this revision I would not show anything.
Another problem I've found:
the UI implies, that for every change we create a new revision. But you can edit nodes without adding a new revision:
1. Have a node in DE and EN (Revision Nr 2)
2. Edit EN, create a new revision (revision Nr 3)
3. Edit DE, do not create a new revision
4. Edit EN, create a new revision (revision Nr 4)
5. Edit EN, create a new revision (revision Nr 5)
Now the UI shows the Revision Nr 4 as "Language: English", but it also contains some changes to German.
If the user now reverts to version Nr. 3, his german changes are not lost (because we only revert the English translation) but it's still confusing as the revision list implies there are only english changes.
But actually I don't really know how to fix that, other systems like Workbench in D7 force the creation of new revisions whenever you like to have some workflow based systems. Would there be a way in the example above to show "English, German" in the language column for Revision 4?
Comment #11
plachThe suggested approach looks sensible to me, my main concern is that in the current implementation we are changing the meaning of the
langcodecolumn in the revision table, which is meant to store the entity default language per revision. This would work only for the SQL entity storage, but the Mongo storage would save only one value for thelangcodefield, hence the current code would break.To address this and #10, we could define an additional
revision_translation_changedfield (name TBD), which would hold the entity modification timestamp per language. Translation would need to be always enabled for this field, Content Translation would take care of that by not listing it in the settings UI, as it does for its own fields.The new field would be stored in the field data and field revision tables as usual, and would allow us to track which translations were edited for each revision by simply comparing the revision translation timestamps with the revision timestamps. The drop-button in the revision list could be kept as it is, and in the confirmation form we could display a list of translations to be reverted with a checkbox for each one. I'd make them all checked by default.
Here's a very quick code review:
Language data is stored in config, hence to translate language names we should rely on config translation rather than string translation.
Why do we need all this processing in the build phase? Wouldn't it make more sense to perform it on submit?
80 chars limit exceeded
We usually have spaces around operators.
Missing blank line after introduction line.
Missing braces
Comment #12
plachBtw, it's true this functionality was available in D7 but this almost looks like a new feature to me.
Comment #13
miro_dietikerUff.. The title focusses on reverting a single language.
I would understand if one argues, this is a new D8 feature, as it was never seen in D8 before.
However, note that the revisions tab / looking at previous revisions is currently unusable in a setup with multiple languages enabled where translations happen.
The user is not aware of his current language context, but all output only represents the current language. If i compare two revisions (given that the newer added a translation into a new language) with the UI set to a different language, i see no change at all between the revisions.
Yes, revisions can not be limited to one language only. The best we can do with current design, is outline what languages are affected per revision. This, however, either needs comparision, or a per-language-per-revision timestamp as suggested by plach. We could then outline the language affected in the UI.
Alternatively, we could also just add a flag per revision per language that is 1 if a field changed in that language.
In my opinion, we should output the affected language column if we can.
As soon as you start offering this, a good UI would allow a user to filter revisions for a specific language. And similarly, allow reverting a certain language only.
The flag approach would make us most flexible since it can be used reliably for output without significant processing. It needs a lookup to the previous revision when saving though. This is permanent overhead though (for every save), since we never know if there are any previous revisions... See also #808730: Show the Revisions tab/page even when only one revision exists.
Yes, we just started to build workarounds for issues that are created because we did not manage to limit a revision to one language only. We need to be careful that this problem and related workarounds are not adding more complexity than fixing the original problem.
Comment #14
mkalkbrennerI created a new patch that covers the formal findings from plach in #11.
BTW it's funny that most of the formal issues already exist in the current version of the node module in core. I will create a separate issue for that.
I second miro_dietiker that the issue discussed here is not a "new feature". Drupal is still a CMS. And it's a fundamental requirement of an CMS that supports translations and revisions to have revisions per translation (or at least a UI that pretends it) - as it existed in prior versions of Drupal.
From my point of view there's a big mistake in the architecture regarding revisions and translations - like miro_dietiker described.
But I think it's to late in the game to apply fundamental changes to revisions in 8.0.x branch. My goal is to introduce an usable solution for 8.0.x, based on the data that already exists and without increasing the complexity too much.
I'm now looking at Schnitzel's suggestions having plach's commments in mind regarding timestamps, fields, etc.
I hope to provide an accordingly extended patch soon ...
Comment #15
mkalkbrennerComment #16
mkalkbrennerI thought about a field to store translation specific timestamps on changes.
I recognized the columns "changed" in the tables "node_field_data" and "node_field_revision". My first assumption was that this one could be simply used for our needs. But I realized that there's always the same value stored across all languages - no matter which translation has been changed really.
There's a corresponding bug report already:
#2428795: Translatable entity 'changed' timestamps are not working at all
Can anyone provide feedback on that one? Is it a bug or feature?
Comment #17
plachOne clarification: I said this «almost looks like a new feature» because fixing this requires some significant UI changes. That's why I was trying to suggest an approach minimizing the impact on the UI (and why I didn't change the category to "feature" and postponed this to 8.1.x).
These are all good points, and UX improvements are not frozen yet, so something like this would still be feasible. However It would be good to get some advice from the UX folks. What about providing a minimal fix for the bug reported here, that is preserving unaffected revision translations during reverts, and open a follow-up to implement a better UI?
Not sure about the flag approach, timestamps wouldn't require additional processing on save, I think, and determining which translations were affected for each revision should be feasible in a single iteration, while fetching revisions from the DB, so it shouldn't be a big overhead.
I'd be happy to be proved wrong, but IMHO the current approach (revision translations) is the only viable one, if we want to keep the complexity of the Entity API inside reasonable limits. Having translation revisions with the current architecture, where a single entity has multiple language variants, would mean having per language revision ids, and getting rid of per-language ids is one of the main reasons I proposed the translatable fields model in the first place. This is certainly one of the cases clarifying that picking a single translation model forces us to accept some tradeoffs, however all considered I think pros still outweigh cons. The solution proposed by @mkalkbrenner is simple and elegant and, once it's coupled with a proper UI to support it, I don't think we will have significant regressions in the user-facing functionality.
From my POV it's a tradeoff and not a mistake, but I'm not very interested in arguing about this, unless you can suggest an alternative solution that fixes this use case and keeps all the rest working, in which case I'm all ears :)
The reason why I proposed a separate field is twofold:
changedfield. I expect the revision UI to be generalized much like the translation UI and be applied to any revisionable entity type sooner or later, so we need to be prepared for that, even if Node is the only entity type currently exposing a revision UI, at least in core.changedfield might or might not be translatable (#2428795: Translatable entity 'changed' timestamps are not working at all looks like a bug to me), but we need a field that's always marked as translatable in this case.Comment #18
mkalkbrenner@plach: Thanks for your explanations. I think we are basically of the same opinion.
I now have a concept in mind how to implement the missing parts. But to avoid more redundancy in the code and database it would be nice if you can have a look at my comment #3 on #2428795: Translatable entity 'changed' timestamps are not working at all.
Comment #19
miro_dietikerWe started a first approach are pretty near to to build a revision tab for all entities in the diff module. yay!
#2452523: Offer a revisions tab for all entities
There are many relevant trade offs, as pointed out in my blogpost. The most concerning one for me is that with every single update of a language, all translations are written. This simply results in severe scaling / performance issues with platforms containing many languages. Combined with inline editing and enabled revisioning, this is going to be fun. The current cross language locking (if multiple translators add translations at the same time) is still circumventable technically.
Comment #20
gnindl commentedA customer's perspective
On osce.org we are working with about 100,000 entities in default language English and entity translations in over 50 languages. Translations are usually done by separate editors, not working in parallel, but in general after the source translation - which may also be Russian, Armenian etc. - has been completed. There is no link or common revision between translations, each translation has its own workflow, pitfalls, false friends and typos.
Having dependent revisions in Drupal 8 would force us to think to upgrade to another CMS. IMHO the demand for such a feature is very strange. I assume that people who have only worked with a single language, e. g. English, can come up with such an idea.
Guys, please don't overdo things here. Multilingualism has matured a lot in Drupal 6 & 7, just stick to it.
Comment #21
mkalkbrenner@gnindl: Thanks for your comment from a user's / customer's perspective.
My organization faces exactly the same situation, except that the number of translations is lower ;-)
The current behavior of the drupal 8 core regarding translations and revisions will be a show stopper / blocker for any migration from multilingual drupal 6 or 7 installations having multilingual editorial workflows - like the osce and my organization. That's why I created this issue to address at least one problem miro_dietiker discussed in his blog post.
For sure, scaling and performance issues have to be addressed as well, but I recommend to do that in a second step.
Comment #22
berdirAs @plach said, revisions per language would re-introduce per-language identifiers, which come with their own problems.
If you have 50 translations, then you will have different problems. All 50 translations are stored and loaded as part of the same entity. Obviously, that would be a huge amount of data.
Yes, the new translation model in core doesn't really work for that. It doesn't have to. There is AFAIK already a project that wants to support a translation model that is similar to the old translation module and uses translation sets. If that works better for your use cases, use that. Problem solved.
There is no chance that the core content translation module or content entity storage will change completely in Drupal 8.
Comment #23
mkalkbrennerOK, but what does that mean for the approach discussed here which is usable if you use only a few different languages?
I'm close to finish a new patch but I want to ensure that it makes sense to keep spending time on it.
Comment #24
gnindl commentedThat sounds scary and like a questionable design. It's like if you are just inviting a single friend to your party bringing his 50 cousins with her/him. It might be fun, but also crowded in your small apartment.
I am not aware of the code details and why this heavy loading has to be done, but why don't you introduce some lazy loading? Translation pointers which are loaded on demand? Or are you also storing 50 translations in a single Word document file?
If you want to group translations you should have translation sets. Overall I smell here an anti pattern God Object
Comment #25
cspitzlayI guess that includes any 8.x version not just 8.0.x.
All the more important that something like the above per-language reversion workaround gets in.
Comment #26
plachGreat news :)
Well, we did not get to that yet, but having a proper Entity API baked in core will allow us to implement some cool optimizations. One of them is writing to the DB only the pieces of the entity that actually changed. This would solve your concern, I guess. IIRC in your article you were also pointing out that having many translations in a single entity object may result in memory issues. This might be trickier to address, but I think we could experiment with lazy loading entity fields per language. This would require some tweak to the entity caching logic, but I think it would be feasible. OTOH having all field data in all languages readily available is very useful to provide efficient language fallback, which we could not have with the node translation model, above all when listing entities. Hence I guess this could be an optional behavior, or even an alternative entity cache backend to be used only in very specific conditions.
Yep, we certainly need to improve that, but it's definitely feasible.
Well, the current approach does not necessarily imply having dependent revisions, as @mkalkbrenner has just demonstrated with his patches. We simply did not get to make every single bit work yet. Help is more than welcome.
Actually I worked almost only on multilingual projects for many years, however TBH none of them had 50 languages installed. OTOH this scenario is definitely not the most common, so not the one the default Drupal configuration should target. I'd like to point out that the current system is fully compatible with the D6/D7 translation sets approach: reimplementing it can be done in one working day, and AFAIK the Translation project aims to do exactly that. If even with every possible optimization, the core approach did not fit your use case, node translation will still be there to serve you and anyone else needing it :)
Thanks for the suggestion, I'm sure D8MI will benefit a lot from it.
I don't think so: what we have in core is the evolution of the D7 Entity Translation project, that is being used right now in very high profile sites with many languages installed. I'm confident that most of the issues we have identified can be successfully solved.
I'm trying hard to take this comment seriously, and consider it an attempt to provide a constructive criticism, but do you realize there are literally years of work and discussions behind the current implementation?
@all:
I don't think this is the right issue to rehash the revision/translation topic in general (let alone discussing the core content translation model). It would be great if we could create a meta issue listing all the concerns raised so far about the revision/translation topic, and possibly already create the related sub-issues.
Now can we please focus on implementing what's needed to fix the issue reported in the OP, possibly punting any UI improvement to a separate major task?
Comment #27
gábor hojtsyI agree with @plach. Most components of the Drupal 8 multilingual system are not brand new at all, they evolved from existing Drupal 7 solutions. http://drupalwatchdog.com/volume-4/issue-1/drupal-babel has a comparison table of modules in Drupal 7 vs. core features we built-in. The content translation solution in Drupal 8 is especially built based on the experience we had with title module, workbench and entity_translation. I would be among the first to readily admit that not all aspects are fully covered, but (a) Drupal 8 is not yet released (b) now is your time to help out to cover your use cases. One way to help out is to help implement https://www.drupal.org/project/translation if you are interested in having a translation set option with different entities. We the entity translation method for core in part because that is more flexible in terms of configurability and especially compatibility with contributed modules (which historically had problems being compatible with both translation methods).
Comment #28
gnindl commentedThanks for the clarification, fair enough.
It wasn't clear from your posts that the patch of mkalkbrenner and other performance improvements have a chance to be accepted. That communication problem being sorted out I will have a look if I can help out.
Comment #29
mkalkbrennerI think most of us agree to keep the current architecture and to improve it step by step.
Let's focus on the original specific topic I pointed out in this issue.
I created a new patch based on the suggestions and concerns of plach and Schnitzel.
It would be nice if to get some feedback on this one.
Comment #33
mkalkbrennerI noticed the problem of some existing tests with the last patch and I'm working on it ...
Nevertheless you can already provide feedback on the feature itself provided by the patch ;-)
Comment #34
mkalkbrennerIt's good to have tests :-)
My last patch contained a bug. Here's the correct version.
Comment #37
plachThanks! I will have a look to this later today
Comment #38
fabianx commentedAnother possibility to solve this is to make revert fill in an edit form with the old revision of the entity. We use this approach (indirectly) for the CPS module (by design).
That allows to keep UI changes small and just bases reverts off an older entity. Still need a little special code to only replace those fields with data from the revision that is different and only for the loaded language, but then the language is explicit.
Just tossing in as a suggestion.
On the performance and memory concerns:
- Performance fixes in general are at the moment very viable and a lot of lazy loading could be done without any API changes, so can happen in any version of Drupal 8.
- Given the decoupled nature of Drupal 8, adding lazy loading for multiple languages within an entity is also feasible as all uses proper getter() and setter() now
Also optimizing storage writes is a feasible possibility without any API changes needed.
And given enough interests / use cases this might / will one day be implemented.
But of course if you chime in with help, it will get done faster.
Comment #39
mkalkbrennerIt turned out that there's a possible race condition caused by revision timestamps. That's why the test failed once in #35. In my environment the test fails in one of ten runs.
I already discussed that issue with cspitzlay and we have a solution. Stay tuned ...
Comment #40
plachSounds good :)
This is an interesting solution, however it's not very common to end up in an edit form when trying to revert a revision, at least in my experience. This could use some UX expert feedback, that's why I'm suggesting to provide a minimal fix here, so that the functionality is not completely broken, and discuss a more complete solution in a separate task.
As I mentioned in #2428795-9: Translatable entity 'changed' timestamps are not working at all, this field cannot be defined by the Content Translation module, because the API allows to create/update translations programmatically without needing CT to be installed. It should be provided by the Entity Manager like we do for the
default_langcodefield.We don't need two methods, we just need to make
::setChangedTime()use the new field.As a consequence, these become redundant.
This should be handled by the entity storage handler directly, we cannot rely on a form workflow.
As I was saying above, please, let's try not to rush a new UI in. A dedicated issue where we can get proper UX reviews would have way more chances to obtain a better result (and be accepted by core committers). Can't we just assume that all the affected languages need to be reverted for now? This would allow us to skip any UI change.
We normally use a "get" prefix for this kind of methods, what about
getAffectedRevisionTranslations()?Comment #41
mkalkbrennerI need some feedback on #2428795: Translatable entity 'changed' timestamps are not working at all comment #12.
Using a solution like I proposed for that bug, we don't need an additional timestamp here. In this case plach's points 1, 2, 3 and 4 from comment #40 will be obsolete.
Comment #42
plachCreated meta, feedback welcome.
Comment #43
plachThis is actually postponed on #2428795: Translatable entity 'changed' timestamps are not working at all.
Comment #44
plachComment #45
mkalkbrennerI noticed a small issue when creating screenshots for #2465907: Node revision UI reverts multiple languages when only one language should be reverted. Therefor I adjusted the patch. But this patch will not apply against head but beta9. A new patch against head is postponed until the related issues are fixed.
Comment #46
tstoecklerThe issue summary needs to be updated with the proposed resolution.
Specifically, I could not find an answer to the following question in the patch, although I must admit I didn't spend a super large amount of time on this: The normal
changedfield is already marked translatable, so why can't we use that and make sure that it's updated correctly for each translation. Semantically I don't see any difference, but again, I am probably missing something.Comment #47
mkalkbrenner@tstoeckler:
The "changed" is marked translatable but isn't translatable at the moment! Have a look at #2428795: Translatable entity 'changed' timestamps are not working at all.
Once that one is fixed I can upload a new patch here. I'm already working on it.
Comment #48
tstoecklerAhh OK thanks for that clarification, I will have a look.
Comment #49
mkalkbrennerSo here is an improved version of the patch from #45 which requires the patch #53 from #2428795: Translatable entity 'changed' timestamps are not working at all to be applied first.
(I'm doing that to enable others to test this stuff at the drupal dev days)
Screenshots are available at https://www.drupal.org/node/2465907#comment-9803885
Comment #50
mkalkbrennerComment #52
mkalkbrennerComment #53
mkalkbrennerComment #54
mkalkbrennerpostponed for #2428795: Translatable entity 'changed' timestamps are not working at all
Comment #55
mkalkbrennerComment #56
mkalkbrennerComment #57
mkalkbrennerI updated the patch to be applied on top of #2428795: Translatable entity 'changed' timestamps are not working at all. Therefore it could not be tested against 8.0.x-dev at the moment.
And I still want to polish some things, so the patch might be considered as a preview ATM ;-)
Comment #58
plach#2428795: Translatable entity 'changed' timestamps are not working at all was committed
Comment #59
plachComment #60
mkalkbrennerI need to adjust the patch to the final version of #2428795: Translatable entity 'changed' timestamps are not working at all. And ...
Comment #61
mkalkbrennerI started working on the patch again.
The first step was to adjusted the patch the deal with the changes introduced by #2428795: Translatable entity 'changed' timestamps are not working at all.
In addition I addressed the TODO that has been left there:
The second step was to remove some ugly parts of the code and to leverage the new postSave() function instead that will be introduced by #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks.
I uploaded two patches. the real one could not be tested without https://www.drupal.org/node/2478459#comment-9983133 being applied. Therefor I uploaded 2453153_61_based_on_2478459_57.patch which includes that patch in order to get the results from the test bot.
Comment #63
mkalkbrennerHopefully fixed the tests
Comment #64
plachSorry, I had to read again the whole issue and the related ones to refresh my memory and I wasn't able to do a proper review.
I'm a bit confused because I thought we agreed to provide a minimal fix here, and instead the patch seems to do way more than what's strictly required to make revision reverts work with translations. It's clear you spent a lot of time dealing with real-life issues (this is hugely helpful, thanks :), but I'm having a hard time understanding the big picture.
I thought we agreed on using the (translatable) changed timestamp to figure out which translations were affected for each revision, but I guess the fact that two revisions can have the same timestamp kind of make this approach not viable. It would be great if you could summarize what's the currently implemented solution, so we can discuss it a bit before I go back reviewing the code.
A few things that concern me are:
Comment #65
mkalkbrennerThe approach is still the same. With #2428795: Translatable entity 'changed' timestamps are not working at all we introduced the required logic to detect changes per entity translation and set the changed timestamp accordingly.
But due to the nature of timestamps it's not safe to use their values for decisions. For example in our use case here it could happen that two revisions are created within the same second.
The solution to this problem is to leverage the (previously) internal logic of ChagedItem field and to store a value different from timestamps that represents the decision that has been taken by this logic. In our case the revision ID to which the change belongs.
This use-case has been foreseen by @catch in https://www.drupal.org/node/2428795#comment-9899597 :
That's exactly what I've done in the latest patch here. I moved the logic from ChangedItem::preSave() to the public function ChangedItem::hasEntityChanges(). That's what we all agreed on at #2428795: Translatable entity 'changed' timestamps are not working at all and why we left the corresponding TODO in that patch. This modification doesn't increase the complexity of the logic itself!
For a better DX I also added ChangedItem::hasEntityChangesAcrossTranslations(). But I'm fine to remove it if you don't like it.
For performance reasons I introduced ChangedItem::hasChangedOnPreSave(), because it makes no sense to run the logic twice to get the result of the logic elsewhere during save. Again, the stored timestamp value itself is not reliable.
To store such a reliable value to complete all the data that is required to solve #2465907: Node revision UI reverts multiple languages when only one language should be reverted I introduced the small ContentTranslationRevisionPointerItem field type. Thanks to @plach's patch #57 for #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks it's possible to ask for the decision that has been taken by ChangedItem::preSave() within ContentTranslationRevisionPointerItem::postSave() using ChangedItem::hasChangedOnPreSave() in a performant way.
The patch in #63 still includes the UI part and the UI test that already existed in all earlier versions of this patch to prove that my approach works. But I'm fine to remove all the UI stuff here and move it into a separate patch for #2465907: Node revision UI reverts multiple languages when only one language should be reverted.
The remaining patch here will then only consist of the clean-up / improvement of the ChangedItem field type and the addition of the ContentTranslationRevisionPointerItem by ContentTranslation. And a new test on the API level.
I choose ContentTranslation because that's where it's ensured that a ChangedItem is added to an entity. If we want to move the revision pointer to ContentEntity we need to be consequent and require a ChangedItem at this layer as well if the entity is revisionable.
Comment #66
plach@mkalkbrenner:
Thanks for the very detailed explanation :)
I had another look to the last patch and manually tested it to get a better sense of the implementation details. The end result is great, although I have some minor doubts about the final UX, which is why I think we should move all the UI code to #2465907: Node revision UI reverts multiple languages when only one language should be reverted. It's completely fine to keep posting patches including dependencies over there, as we did here so far, if we want to ensure all tests are still green. However this will make focusing on the low-level stuff here easier.
Aside from that, I still have some doubts about the actual implementation:
Actually all revisionable entity types need to manually define revision metadata fields to work correctly, so adding a couple of further requirements does not really change the overall situation. Revision and translations cannot work properly if we don't have a changed field and a revision pointer, so we need to make them required, no point in trying to get around that. What we could to loosen these constraints could be falling back to the current core behavior (i.e. reverting all languages) if one of the two fields is not defined. In this scenario, revisionable and translatable entity types will be required to define an additional (translatable!) revision metadata field, e.g.
revision_last_affected(instead ofcontent_translation_revision). Name to be defined, but I'd prefix it withrevision_as we do that for all the other ones.In this scenario, I think we no longer need the
ContentTranslationRevisionPointerItem: revision and translation business logic are the domain of the core (Content) Entity API: they both are natively handled by all the Entity API subsystems, thus properly handling revision translations should be part of that. Specifically, properly calculating/storing the revision pointer values would be the storage handler's responsibility. Additionally, moving that logic in the storage layer may make things slightly easier to implement.Going further with this scenario, we may not even require the changed field: if I'm not mistaken, right now we are using it just track whether an entity (translation) object has changes wrt to its stored version. However it seems that the logic currently implemented in
ChangedItem::hasEntityChanges()has nothing specific to the changed item, and could easily live in the (Content) Entity object itself. In this case, the storage handler should have everything it needs to calculate and store the revision pointer value.What do you think?
Comment #67
yched commentedNot too familiar with the contents of the patch, but @plach's #66 sounds appealing. +1 to anything that lets us move behavioral logic out of specialized subclasses of 'timestamp' field type.
Coding behavior / value in the field type is an antipattern we should try to avoid.
Comment #68
plachSpoke with @catch about this during the critical issues call. The proposed solution will have an impact on the upgrade path, and I'd really like not to have to provide an upgrade for this. Since the bug is borderline critical, we agreed to make it tentatively critical, so maintainers will have a chance to discuss it in the next triage round and decide whether it actually is.
Assigning to catch because we need his feedback also on the proposed solution (see #65 and following).
Comment #69
catchOK so there are two things to consider regarding whether this is critical or not:
1. The inadvertent data-loss between translators when reverting revisions. This is similar to #2484037: Make Views bulk operations entity translation aware. The difference here is that the old revision information isn't completely deleted, but it's not possible without manual editing/comparison to get both translations to the desired state. I don't personally have a strong view on this.
2. If it is critical, then we should try to fix it before we start supporting the upgrade path officially, since the upgrade path required here will be non-trivial and could end up delaying the patch overall and hence the release. I'd really like to avoid that extra work.
So I think we need to decide now that the issue is critical and get it fixed before the upgrade path is officially supported. Or we can decide it's major - and possibly too disruptive to commit to 8.0.0 and defer to 8.1.x. But what we definitely shouldn't do is decide this is critical after supporting the upgrade path, since that's adding extra scope compared to the proposal.
#66 also appeals to me as a solution. - standardizing across revisionable/translatable entity types is fine - and better to have a couple of metadata fields required if that simplifies the handling.
Comment #70
plachThanks @catch, +100 on #69. Personally I think we should do this before we release 8.0.0. and thus we should keep this as critical, but I may be biased :)
Comment #71
mkalkbrennerTo have this issue postponed for drupal 8.1.x scares me a lot. I will repeat my own arguments again. And also some others which are spread across different issues that has been split off from this one.
@mkalkbrenner: https://www.drupal.org/node/2453153#comment-9758203
@webchick: https://www.drupal.org/node/2428795#comment-9842025
@Schnitzel: https://www.drupal.org/node/2453153#comment-9747221
@gnindl for osce.org: https://www.drupal.org/node/2453153#comment-9757787
@matsbla: https://www.drupal.org/node/2465907#comment-10013437
Comment #72
mkalkbrennerMy motivation for this issue are future migrations of various multilingual Drupal 6 and 7 sites to Drupal 8. From this point of view the decision about ContentTranslation vs. ContentEntity doesn't really matter (in our case), because we're always talking about nodes.
In my patches I choose ContentTranslation because it seemed as the easiest way to have the issue fixed for nodes.
If we now decide to fix it for all translatable entities, I'll be fine with that, even if it means additional work. I offer to help with that as long as we agree on having the feature in drupal 8.0.x.
Basically we need one field that stores the revision id to which an edit belongs. (Within my patch for ContentTranslation I named it ContentTranslationRevisionPointerItem.) The translatable ChangedItem has already be solved by #2428795: Translatable entity 'changed' timestamps are not working at all. But it makes sense to declare it as required if we go for the ContentEntity approach.
If we postpone it to drupal 8.1.x I'm really affraid of the multilingual drupal 6 websites (like ours). Without having this issue fixed we can't migrate them to drupal 8. If the support for drupal 6 ends, the only remaining way out will be a migration to drupal 7. That really sounds stupid.
Comment #73
plach@mkalkbrenner:
If you are ok with #66 too, we could have this RTBC pretty quickly, since that would mean mostly moving some code around, and then have it ready before the upgrade path is. At that point it doesn't really matter whether this is critical or not, because it would no longer be an upgrade path blocker.
Comment #74
plachTo be clear: I can make this my current top priority, but we need to agree on a way forward.
Comment #75
mkalkbrenner@plach:
As mentioned in #72 I'm fine with your approach in #66!
Even more if you help implementing it to get it RTBC soon :-)
Nevertheless it seems very attractive to me to keep this issue here as critical ;-)
Comment #76
plachSorry, somehow it wasn't completely clear to me :)
Well, I could certainly work on this, but then we'd need to find someone to review/RTBC my patches. If you have time to work on this, I think it will be faster if you work on #66 and let me review the result as soon as it's ready. If you are not planning to work on this soonish, let me know and I'll try to move it forward during the weekend.
Comment #77
mkalkbrennerMaybe I can start today. But it will speed up things if you directly point me to place where i have to declare the required fields or give me an example where corresponding fields are implemented. Just to be sure and to not lose any time ...
Comment #78
plachIt should be just matter of manually adding a
revision_last_affected(or whatever it will be called) base field definition to:BlockContent::baseFieldDefinitions()Node::baseFieldDefinitions()EntityTestMulRev::baseFieldDefinitions()EntityTestMulRevChanged::baseFieldDefinitions()Comment #79
mkalkbrennerOK. But there's a remaining question:
How do we set the values of these fields?
Could we keep my approach based on the field types (ChangedItem and a new one)?
In this case it's really just moving code from ContentTranslation to different places in core.
Or do we have to move that logic from the field items to ContentEntityBase, for example the change detection?
Comment #80
plachI think we should move
ChangedItem::hasEntityChanges()to a newContentEntityInterface::hasChanges()method (we can move it toEntityInterfacein a separate issue, as that would be BC change). And then we should move the code that's currently implemented byContentTranslationRevisionPointerIteminContentEntityStorageBase.And move the UI changes to #2465907: Node revision UI reverts multiple languages when only one language should be reverted :)
Comment #81
jhedstromCNW to split the patch.
Comment #82
jhedstromI'll give #80 a try.
Comment #83
mkalkbrenner@jhedstrom: I'm already working on it.
Comment #84
jhedstrom@mkalkbrenner thanks!
Comment #85
mkalkbrenner@catch: I assign the issue back to me while I'm working on it, ok?
Comment #86
mkalkbrennerI created a first patch. It's still work in progress and I expect a lot of tests to fail. Even the
revision_last_affectedcould not work at all because it requires #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks.Nevertheless I will upload the patch to get first results from the testbot and the patch could be considered as a draft to discuss the technical approach.
BTW all the UI has been removed and will be addressed later by #2465907: Node revision UI reverts multiple languages when only one language should be reverted.
Comment #87
mkalkbrennerAlready found a bug locally before the testbot has finished ;-)
Remember, even if the tests will pass now, the patch is not fully functional unless it is combined with https://www.drupal.org/node/2478459#comment-10010271
Comment #90
plachSorry, probabaly I didn't explain myself clearly. The
::hasChanges()method does not need to be tied to thechangedtimestamp: any entity can have changes, because it can differ from the stored version. This method should live onContentEntityInterface, and should be moved up the hierarchy tree intoEntityInterfacein a followup implementing it also for config entities (which should be easier).I'd avoid generalizing the
::*AcrossTranslations()pattern, as then we'd needed it everywhere. After all looping on the available translations is quite easy and natural.If we don't invoke the
::hasChanges()method from within a save process (or we do beforeChangedItem::preSave()is invoked), the following logic does not need thechangedfield: if any field has changes, the entity (translation) object has changes.#60 is suggesting to calculate which translations have changes for the current revision (and update the revision pointer field) in
ContentEntityStorageBaseexactly for this reason: if we overrideEntityStorageBase::save(), compute therevision_last_affectedvalue, and then invoke the parent::save()method, we should be able to have our values in place beforeChangedItemhas a chance to do any harm.Is this still true?
And thus this can go away, which lets us not depend on #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks and avoid a rewrite.
Comment #91
mkalkbrennerIt isn't! The code just contains a shortcut to skip the expensive detection if the changed timestamp is already changed.
Nevertheless I consider the changed timestamp to be a logically required field for revisionable entities.
I choose the existing EntityChangedInterface which is more obvious from a DX perspective. See the explanation below.
That's a point that needs further discussion. We're talking about two "completely" different kinds of hasChanges(), even for non-ConfigEntities. (That reminds me of the discussions regarding the ChangedItem.)
The implementation you might have in mind is about an absolute decision whether an entity has any changes compared to the stored one or not.
From a revertable translation revision only perspective you have to exclude various fields from that implementation, for example the non-translatable fields.
Again, we're mixing two use-cases here and might repeat the same error that initially caused #2428795: Translatable entity 'changed' timestamps are not working at all. There we solved it by destinguish both use-cases by providing two methods: getChangedTime() and getChangedTimeAcrossTranslations().
With this information in mind it's obvious to implement the functionality in EntityChangedTrait and to leverage the EntityChangedInterface for the use-case we're targeting here.
The detection of absolute changes instead has to be implemented deep in the entity base classes like you suggested.
BTW we have to think again carefully about naming methods, because hasChanges() has two different meanings.
Comment #92
plachI made a few "on-paper" tests to verify this:
It came quite natural to me to flag things the way Miro was suggesting in #13. Discussing with @mkalkbrenner pros and cons of the two approaches.
Comment #93
plachI had a long discussion about this with mkalkbrenner and hchonov, but no final conclusion yet, here's the log.
Comment #94
mkalkbrennerNevertheless the discussion was very important and we already agreed and some details. And we agreed on which problems need to be solved.
Comment #95
mkalkbrennerA first draft using a flag instead a revision id. Still work in progress ...
Comment #97
mkalkbrennerThat was the wrong file. This should be the draft.
Comment #99
mkalkbrennerComment #101
mkalkbrennerComment #103
mkalkbrenneradded a test
Comment #105
mkalkbrennerFirst I want to repeat the basic concept @plach, @hchonov and myself agreed on:
Every translation needs to be marked by a flag if it is edited within a revision. Therefor we store a new translatable and revisonable boolean field that's currently named 'revision_last_affected'. The value is TRUE as soon as a translation is edited.
If a new revision is created the flags of all translations are initially reset to FALSE. (see mulrev-revert-flag.pdf created by @plach)
The new field is required because the already existing translatable changed timestamp isn't sufficient to judge to which revision an edit belongs because two revisions could be created within the same second. Nevertheless the translatable changed timestamp is an important information that will later be used to build a sophisticated UI or workflow modules in contrib.
Now I want to explain my patch / my proposed implementation:
I extended the already existing ContentEntityChangedTest to verify all the different states of the new boolean flag. I assume that this test could be kept, even if we decide to modify parts of the architectural approach.
I moved the internal logic of ChangedItem to a public method defined by ContentEntityInterface::hasChanges().
I already added that as Todo in #2428795: Translatable entity 'changed' timestamps are not working at all as requested by @catch. This is also welcome by @plach (and @yched, I guess). ChangedItem itself now leverages that public method.
The logic to set the value of the new flag is nearly identical to the ChangedItem so that ContentEntityInterface::hasChanges() could be reused.
The detail that needs further discussion is the way the value of the new flag is set. @plach's current favorite approach is to set the field value in ContentEntityStorageBase::save() as a kind of hidden feature within the framework (@plach: correct me if I'm wrong.)
My approach is to provide a "simple" field type named ChangedFlagItem that selects its value on preSave() by itself.
BTW the amount of code is roughly the same in both approaches.
Beside the fact that in my opinion the field type is the more elegant and portable solution I see another advantage.
The dedicated field type could potentially be useful when someone creates the UI for reverting single translations or sophisticated workflow UIs.
In general if a field gets its value somehow magically, as a developer I would expect to find that magic implemented using the field API. From the information about the field type within an entity I'm directly pointed to ChangedFlagItem::preSave() and see how the value is detected or modified.
BTW the naming of the fields and the methods is just a draft.
Comment #106
mkalkbrennerFYI: I uploaded the UI code that has been removed from the patches here as a separate patch for #2465907: Node revision UI reverts multiple languages when only one language should be reverted.
Comment #107
mkalkbrennerWhile writing the comment #93 for #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks I thought about a different argument to use a new field type for the flag instead of setting it from within the storage classes:
I think it's a good pattern for contrib developers whenever they want to achieve something similar. They should use the Field API and not hack the storage.
Comment #108
catchDiscussed with plach, and did a very superficial review of the patch.
I think I'd keep this in the storage layer. This is specifically tracking whether something we've stored has changed internally. Revisions and translations are central to the entity system. Contrib doing special handling has access to field types and hooks, so baking this in doesn't stop them doing what they need to for other kinds of changed tracking.
This should also check untranslatable fields, otherwise it will report that things haven't changed when they have.
Comment #109
mkalkbrenner@catch
Please have a look at the irc log at #93 and especially my explanations in #91. We must not check untranslated fields!
We can discuss the name of the function but if it checks for untranslated fields it's unusable to solve our issue here.
Regarding the name of the function I would point out that we already had that same discussion with @webchick @berdir @yched and others in #2428795: Translatable entity 'changed' timestamps are not working at all.
hasChanges() follows the decision that has been taken there that all getters access the translation values.
But it would be OK for me to name it hasTranslationChanges().
As I already mentioned, my patch could be transformed to that approach very easily without the need to modify the tests or the current patch for #2465907: Node revision UI reverts multiple languages when only one language should be reverted.
Nevertheless I would welcome to hear more opinions from others.
Comment #110
catchI don't think we should have a public method that only checks translatable fields.
All we need is when saving a translation revision, to see a boolean whether it changed or not - that's something we should be able to do in a protected method somewhere without anything in the public API.
That's fine but I think anything to do with this should be split out to another issue, it's very confusing trying to determine what's dealing with setting the translation changed boolean vs. the timestamp and which should be used where.
Comment #111
mkalkbrennerBasically you're right. But that's exactly the same discussion we already had :-(
The already solved translatable changed timestamp currently carries the required logic to detect a change on a translation. The same logic is required for the flag. If we don't won't to copy the code, we have to provide it as public method because it's needed by a field and the storage (as @catch and @plach proposed).
We already did that! The separate issue for the changed timestamp was #2428795: Translatable entity 'changed' timestamps are not working at all. Here we're only discussing about the boolean flag, nothing else. But as I already mentioned the logic that is required to set the flag's value is mostly the same that we already have within the ChangedItem. And I want to leverage that code.
Comment #112
plachComment #113
plachI've just read again the logs of all our previous discussions and I think I've changed my mind on a point that might allow us to reach a consensus on the way forward. I wanted to speak with @catch about it but I wasn't able to, so we still need his approval.
What is bothering me about the current approach is introducing a public API method named
::hasChanges()inspecting only translatable fields. Additionally I was not happy withChangedItemautomatically updating its value only when translatable fields are changed.After inspecting some core use cases, I realized that there is no way to hard-code a single behavior that would work in all cases. Given that, I think now I'm fine with the current
ChangedItembehavior, since it is possible to manually set a different value for it depending on the use case. What would be horribly wrong would be hard-coding the current behavior without the possibility to override it.That said, I think this could be a way forward:
revision_translation_affectedboolean translatable field.ChangedItem::preSave()to a newContentEntityBase::hasTranslationChanges()method (and defineContentEntityInterface::hasTranslationChanges()).$entity->revision_translation_affectedinContentEntityStorageBase::save()and invokeEntityStorageBase::save()subsequently. To make sure the flag value can be manually overridden we should populate it only if its current value is NULL, which should be the initial value for all flag translations after a new revision is created.ContentEntityBase::onChange().Comment #114
catchI think #113 works for me, but I'm not sure why hasTranslationChanged() in the last bullet would be called twice? Is this just in case some other code calls it?
Comment #115
mkalkbrennerIf the changed timestamp is not explicitly set, ChangedItem::preSave() will call ContentEntityInterface::hasTranslationChanges() to judge if its value needs to be set to REQUEST_TIME automatically.
According to #113 $entity->revision_translation_affected is already set to the return value of ContentEntityInterface::hasTranslationChanges() a little bit earlier during ContentEntityStorageBase::save(). Therefor it makes sense to cache the result of ContentEntityInterface::hasTranslationChanges() during save and to reset the cache after save.
I think that's the explanation if we simply follow the bullets in #113 ;-)
If we want to avoid the caching, there's an alternative implementation that has the same positive effect from a performance perspective:
If an entity is revisionable and $entity->revision_translation_affected is a required field on revisionable entities, this field is set to the correct value in ContentEntityStorageBase::save() before preSave() is called on the other fields.
With that knowledge we can simply get the value of $entity->revision_translation_affected within ChangedItem::preSave() or call ContentEntityInterface::hasTranslationChanges() if the entity is not revisionable.
Both approaches will have the same impact on performance. Thoughts?
Comment #116
plachYep, that's what I had in mind.
The idea behind caching/invalidating on
::onChange()is not assuming a workflow: sinceContentEntityInterface::hasTranslationChanges()could be invoked in any context, it should be re-calculated as soon an entity field value changes.Comment #117
mkalkbrennerI agree, let's do it. @catch?
Comment #118
plachJust had this chat with @catch in IRC:
@mkalkbranner:
So, with the corrections above to #113, I'd say we have a plan ;)
Comment #119
mkalkbrennerYesterday @catch, @plach and myself had a very long discussion on IRC. We agreed that the flag field must not detect its value automatically if it is already set manually, for example during a migration based on the migrate module.
We discussed several approaches to achieve that but didn't find a solution that works for all use-cases.
But probably I found a solution now. Therefor I created a new patch based on #105 that solves that particular problem and adds a test for it.
In order to keep the interdiff as small as possible to be able to discuss my approach I didn't yet rename the functions and properties to the new names we agreed on.
@plach: Don't blame me for still using a new field type to solve the problem ;-) Any idea how to achieve something similar using a standard BooleanItem and the logic implemented in the storage layer?
Comment #120
mkalkbrennerDidn't upload the interdiff ...
Comment #122
mkalkbrennerI run into a cache issue with the previous patch. The
loadingCompletedflag needs to be set on entities fetched from the cache as well.Unfortunately there's no base class for the various cache implementations. Therefor the flag needs to be cached as well, which isn't a problem logically.
But the cache is set within the concrete storage implementations and not in the base class (I consider that a bug) and the only available callback like entry points before the cache is set are two hooks - hook_entity_storage_load and hook_ENTITY_TYPE_sorage_load. There are no corresponding static callbacks on content entities and EntityInterface::postLoad() is too late :-(
Therefor I had to set the flag in the concrete storage implementation
SqlContentEntityStorage::getFromStorage()where the hooks mentioned above are fired. These hooks and potentially my flag should be moved in a base class similar to the issue addressed by #2478459: FieldItemInterface methods are only invoked for SQL storage and are inconsistent with hooks.Another option will be to implement one of the hooks in system.module, but that looks like bad design, too.
Nevertheless, let's discuss my approach for this issue here first ...
Comment #123
jhedstromGiven #2507899: [policy, no patch] Require hook_update_N() for Drupal 8 core patches beginning June 29 do these need update hooks, or is the plan to get this in prior to June 29?
Comment #124
mkalkbrenner@jhedstrom: This issue here is explicitly mentioned at #2507899: [policy, no patch] Require hook_update_N() for Drupal 8 core patches beginning June 29. Nevertheless I'm available to probably finish this issue before that date. But currently the declared reviewers seem to be very busy.
Comment #125
mkalkbrennerWhile still waiting for feedback I renamed the functions and fields like discussed and moved ContentEntityInterface::hasChanges() to TranslatableInterface::hasTranslationChanges().
Comment #126
effulgentsia commentedPossibly not, even if this lands after June 29th, and even if it's not granted special exemption.
Because entity schema changes are run by update.php automatically. The code for that is update_entity_definitions(), and DbUpdateController::triggerBatch() makes it run prior to hook_update_N() functions, which is good, because it means you can also have a hook_update_N() that loads and saves entities and it will do so with the correct database schema already in place.
However:
ChangedFlagItemfield type, but update.php does not appear to clear the discovery cache prior to checking what updates are available. That's not this patch's fault. If we don't already have an issue for that, we should add one. Until that, in order to test an install of HEAD followed by applying this patch followed by running update.php, you need to clear cache between applying the patch and running update.php.revision_translation_affectedcolumn with a value of NULL for those existing entities. I did not test whether the patch then works correctly with that as the value. It would either need to be able to handle that, or else the field definition needs to be given some other default/initial value, or else we'd need a hook_update_N() that loads each entity, gives the field a value that is correct for that entity, and saves that entity. That last option, however, would have serious scalability problems for a site that has millions of entities, so if it needs to be a calculated value, perhaps better would be to calculate as part of saving a revision rather than needing to do it all during the update?Given the above, I also think it wouldn't be catastrophic if this didn't make it into 8.0.0 and ended up getting fixed in 8.1.0. So, I question whether it should remain a Critical. I'm happy for it to make it into 8.0.0 if it does, but I don't think it's worth holding up 8.0.0's release on it.
Comment #127
plachAside from the stuff below, I think this is as good as it gets with the field type approach. I'd still like to explore the no-field-type approach, though :)
I will have a try tomorrow.
Also, for this to be complete we should make node reverting actually work. Without introducing any UI changes I think it should be easy to revert all the affected translations. This would fix the worst part of this issue, and if we weren't able to complete all the other stuff in the meta before 8.0.0 is released, we would still have a functional UI, although less than optimal. However improvement would definitely be possible in 8.1.0.
This looks very fragile: I'm wondering whether we can avoid this by checking for the existence of translations, as pointed out below.
I think what we really want to know is whether the entity is translated, because all this behavior is useless on translatable but not translated entities. So what about:
If I get the comment above right, we modify
$this->originalwhen we try to instantiate a translation that was not originally stored. Would the following work?Can we rename these to
$field_nameand$definition? See above.Comment #128
plach@eff:
I'm not sure I agree with this conclusion: supporting an upgrade for this would be way worse in 8.1.0, because every 8.x website out there would be affected by the upgrade. Instead if we do this before 8.0.0 only a (way) smaller set of sites would be affected.
Ideally I would like this to be fixed before supporting the upgrade path: you are right the schema would be updated automatically, but updating data would be tricky because we cannot rely on the Entity API in update functions and thus we could provide an update function only for the default SQL storage.
It would be nice if could provide an self-healing behavior, so that field values would be fixed by just saving entities, or at least we could provide some sort of fallback behavior if data for affected translations is missing, like reverting all translations as we currently do in HEAD.
Comment #129
mkalkbrennerComment #131
mkalkbrennerI applied the appropriate changes that @plach suggested in #127.
In addition I added the same improvement to ChangedItem that went into ChangedFlagItem to detect manual sets of the field value via the API. This should 100% solve #2329253: Allow ChangedItem to skip updating the entity's "changed" timestamp when synchronizing without the need for any additional patch.
@plach: The intermediate step in #129 demonstrates why 'clone' is required. If this seems like a bug this needs to be handled as a separate issue because it is not caused here.
Comment #132
plachI'm fine with clone, I was scared by the static. What about the node UI?
Comment #133
mkalkbrenner@plach: OK, got it :-) I'm in a hurry because of the 29th of June ;-)
@plach, @effulgentsia:
Regarding the UI. We have a working patch in #2465907: Node revision UI reverts multiple languages when only one language should be reverted that fixes the issue for nodes. It only requires a minor adjustment to the patch in #131. From my point of view a UI for nodes is sufficient at the moment because that is what is required to bring back the feature that existed in Drupal 7 core.
Due to the fact that this pure UI enhancement in #2465907: Node revision UI reverts multiple languages when only one language should be reverted has no effect on the schema, we're not in trouble with the upgrade path after the 29th, right?
Comment #134
catchSo I think this would probably be fine as an 'upgrade path' for existing data - since reverting is not that common an operation, only goes back one or two revisions usually, and the data is there, just not very accessible.
If we do that, then the upgrade path here in general looks much less scary, and I'd agree with effulgentsia that this could go back to major (since we don't actually lose data here, just bury it a bit). I'd also be much happier committing it any time up to RC in that case. However, would really like to be sure about that before downgrading, since having to upgrade this to critical later and write the migration path for all the old data after all would not be nice.
Comment #135
mkalkbrenner@effulgentsia:
I totally disagree! Even if I repeat myself as you can see in the earlier comments (maybe spread over the multiple issues this one has been split into), without having this critical fixed it's impossible to migrate existing multilingual Drupal 6 an 7 sites that have editorial workflows using revisions to Drupal 8.
@effulgentsia:
The nature of the new flag is that there is no reasonable default value that could be applied to existing entities. And it could not be reliable calculated afterwards for existing entities. Therefor it's important to have the flag in core before we have to deal with an upgrade path.
So what's the problem with this issue?
@plach, @catch and myself had long disussions about this issue.
We have consesus that we need to add a additional field of a boolean type named 'revision_translation_affected' to the schema.
We have consesus how the value of 'revision_translation_affected' is detected automatically and that this detection has to be turned off if it is set manually, for example by using the migrate module.
We have consesus that the required detection logic is the same as the existing one within ChangedItem::preSave() and therefor is re-usable.
We have consesus that the re-usable detection logic should be provided by a public method named TranslatableInterface::hasTranslationChanges() that has to be implemented by ContentEntityBase.
The only remaining points of discussion are the most performant implementation of ContentEntityBase::hasTranslationChanges() and which boolean field type to use for the new 'revision_translation_affected' base field.
Performance improvements to ContentEntityBase::hasTranslationChanges() could be easily moved to a non-critical follow-up issue that doesn't affect the upgrade path.
@plach: Wether we set the value of 'revision_translation_affected' within the preSave() of the field type or from the storage, the introduction of ChangedFlagItem makes sense, because it's lightwight compared to BooleanItem which is prepared for the UI. Adjustments to it could therefor be addressed by a follow-up issue as well.
With this argumentation I recommend that we commit the patch now as it is. It's functional and has test coverage and the potentially remaining tasks are non-critical and don't affect the upgrade path. This way we avoid all the potential trouble that might araise with the upgrade path.
Comment #136
mkalkbrennerWe are probably affected by a different issue here, but only when we use the node form, not via the API. We're still investigating ...
Comment #137
plachThis patch builds on top of #131 but does not rely on new field types. As agreed during our last discussion, it implements a minimal fix for the node revision UI to cover the use case described in the OP and provides test coverage for that.
Notes:
The reason is that to improve performance for untranslated entities we now always set translations as affected. I think this is an acceptable change, but obviously totally open to discuss that.
revision_translation_affectedfield cannot be configured in the Content Translation admin UI.Comment #138
plachSome comment clean-ups
Comment #139
plachComment #141
mkalkbrennerComment #143
mkalkbrenner@plach, @catch and myself already agreed that it is necessary to allow manual setting of these fields, at least for migrations. This use-case has been described previously in #2329253: Allow ChangedItem to skip updating the entity's "changed" timestamp when synchronizing. With my field type approach in #131 that has been solved without API changes. @plach: does your approach bring back the requirement for API changes to solve #2329253: Allow ChangedItem to skip updating the entity's "changed" timestamp when synchronizing, or do you already have a nice solution in mind? We need it for both. The flag and the timestamp.
@plach: I get the your idea regarding performance and the approach makes sense from a technical point of view. But if I think of the webmaster, the user that has to deal with the UI later, I suggest to skip that shortcut. As soon as translations are added to the entity it looks confusing in the UI (we still have to develop) to get multiple changes listed, that have no changes. Even if I think of contrib modules like diff. They should leverage the new flag. But in this case the diff will be empty.
@plach: great! I missed that point.
@plach: 3 days ago I mentioned in IRC that we have a problem if we're not at the API level but create translations using the Node UI. @hchonov and myself debugged a lot to finally identify #2513094: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone break field reference to parent entity. As you can see there we already extended ContentEntityChangedTest to cover that. Therefor I decided to add this test to your patch in #138. But I expect it to fail at the moment.
In addition I changed the assertions according to my comment above regarding the performance improvement vs. usability discussion.
During my local tests it turned out that your patch in #138 in opposite to my patch in #131 seems to have a problem with translation removal on the API level. That's why I decided to upload it as #141 directly without discussing it with you upfront.
Comment #144
mkalkbrennerI fixed the exception thrown when removing a translation and creating a new revision within one step.
And I removed the minimal performance improvement in populateAffectedRevisionTranslations() for usability reasons as mentioned above.
Comment #145
mkalkbrennerReviewing my own patch of #144 ;-)
The comment is just valid for my field type approach in #131. Nevertheless #2513094: ContentEntityBase::getTranslatedField and ContentEntityBase::__clone break field reference to parent entity is an issue that has to be fixed.
Comment #146
plachInterdiffs look good to me, thanks :)
Well, if the final solution is something along the lines of the removed code, it should be a completely internal change. Maybe it would imply an API addition, but that should be fine.
I'm now happy with the patch, but can no longer RTBC it. I pinged @catch, hopefully he can review it.
Comment #147
catchLooks great to me.
Comment #148
gábor hojtsyLet's get this in then :)
Comment #149
plach;)
Comment #150
plachMarkus just created it, back to RTBC!
https://www.drupal.org/node/2513894
Comment #151
mkalkbrennerComment #152
plachDisplayed again the failing test-only patch that demonstrates that the new test coverage captures the reported bug.
Comment #153
gábor hojtsy@mkalkbrenner will certainly not be able to commit it.
Comment #154
alexpottA couple of minor nits.
We pass a null in here - also lets return $this to make the interface fluent.
Needs to be indented 2 spaces.
Comment #155
plachAddressed #154, thanks.
Comment #156
gábor hojtsyUpdates look good. I first understood the phpdoc was not updated but its on the interface lower down in the interdiff.
Comment #157
catchGetting this in sooner rather than later so that it's in before the next beta.
Committed/pushed to 8.0.x, thanks!
Comment #159
plachYay, awesome work @mkalkbrenner!
Comment #161
gábor hojtsyThanks all, especially @mkalkbrenner and @plach!
Comment #162
joseph.olstadWe have a side by side language1 / language2 interface, our client is used to pressing 'save' once for all translations and would like to revert to a revision for all translations.
For the revisions form I'm thinking of adding a form alter and a checkbox option for 'All Translations at once'
has anyone else already thought of this?