Problem/Motivation
@matsbla wrote at #2860097-162: Ensure that content translations can be moderated independently:
I tested patch from #157 and to me it seems like after I apply it I'm not any longer able to delete drafts for languages that didn't yet have any published translations. I tested both using the "delete" operation in Translation tab, the "delete" button in the draft form, and the "delete" button in the local task bar. Every time I get the status message like: [French] translation of the Article [blablabla] has been deleted. But when I go back to the translation tab again the draft is still there.
Maybe it should be possible to discard a draft without deleting the last published version of the language. Right now it doesn't seem like we do have that choice.
@plach indicates this is a known limitation, which is a problem only if you enable Content Moderation. It's due to a limitation in the low-level Entity/Field API infrastructure: in the storage a translation removed in a pending revision is indistinguishable from a new translation added in a pending revision.
This is not introduced by #2860097, it is exposed by #2860097!
Proposed resolution
- Fix fully in 8.6
- By introducing the necessary infrastructure in Entity/Field API (see #2945956: Allow removing translations in pending revisions).
- Fix cosmetically in 8.5 (during beta)
- By hiding the delete links when the latest translation-affecting revision is a pending revision. This should be an acceptable compromise, since a pending revision is normally not accessible to regular users anyway.
Remaining tasks
- Validate the proposed solution
Write a patch- Reviews
User interface changes



API changes
None.
Data model changes
None.
Comments
Comment #2
wim leersPlease credit @matsbla & @plach.
Comment #3
timmillwoodComment #4
matsbla commentedI tested again now, it looks like even though I after deleting the translation see it in the translation overview page I'm not able to view it anymore. When I click to view the translation I just see the original language. If I use the operation link to try delete the language a second time I'm in fact sent to a page to delete the original language (the whole node).
I've also made another observation that might be a little bit related to this. If I add a draft for a new translation to a node and look at it I'm also being sent to view the original language. I can see the draft inside the "Latest version" tab, but when I click "View" I see original language, and inside "Revision" tab I see revision for original language. It seems like the draft just inherit whatever it doesn't have from the original language. Maybe it would make sense to not display "view" tab and "revision" tab as the draft for a new translation doesn't have those thing yet. Just like hiding "Delete" button these things could be hidden too.
Still I think there should be a "Discard" button to be able to trash a draft you do not want to keep.
Comment #5
plachAgreed this would be ideal, but it's just nearly impossible to implement properly without a deleted flag for each revision translation. In fact right now when you create a new revision as a draft, the default revision has no entry for that translation. So right now in the storage a new draft translation and a removed translation are basically indistinguishable.
Comment #6
plachComment #7
plachHere's an attempt to fix this while preserving the ability to delete translations: this patch is hiding the delete link only when dealing with a pending revision, which means that translations can be removed only in default revisions. This should be an acceptable compromise, since a pending revision is normally not accessible to regular users anyway.
Comment #8
matsbla commentedI tested the patch, however I'm still able to see the delete link when dealing with a pending revision.
When I click delete it deletes the published version of the translation, but keep the draft revision.
After the published version is deleted I still see a "view" tab. When I click on that I see the node it it's original language, which to me seem a little bit strange.
Comment #9
plachDo you mean the delete tab?
Comment #10
plachPosting some additional test coverage. I have lingering failure I was not able to troubleshoot yet. @Wim Leers said he would look into it.
Comment #11
plachUpdated IS
Comment #13
plachStill a few rough edges to smooth out, but this should be close to complete. Adding screenshots of the proposed solution.
@matsbla, it would be great if you could test this again.
Comment #14
plach@matsbla:
This is "by design": it's how core works right now, not something this patch is introducing, I believe. It should be the regular behavior when a translation does not exist yet.
Comment #15
matsbla commentedOkay I get it now, I was looking at the edit form, there is still a delete button there. Earlier when I click on it I delete the published version of the translation, but in last patch I come to an "Access denied" page.
I'm a little bit confused about the solution for this issue. The issue is named "Draft revisions of translations can not be deleted", but after I apply this patch I'm not able to delete/discard a draft. With this approach to me it seems like I need to first publish a draft and first then I'm able to delete the translation. But I can't delete a draft and still keep current published version.
I understand that it is not introduced here, but is this really how it is intended to work? I find it quite confusing that I see another language at the "view" button, I would find it more logical if I'm not able to click a "view" button at all if there is no published translation to view. Anyway I just mentioned the detail as it seemed related earlier when I did the initial tests.
Comment #16
wim leersYou won’t believe this. When you test manually, it works fine. In the test, it fails, because Dynamic Page Cache (DPC) is caching it. But why is DPC caching it only in the test, not in manual testing? Well, in manual testing, you do that in the
Standardinstall profile. And guess whatstandard_install()does?This line is what tells Drupal to use the admin theme for node editing. Including the translations/revisions overviews.
To make those use the admin theme,
\Drupal\node\EventSubscriber\NodeAdminRouteSubscriber::alterRoutes()sets the_admin_route=TRUEflag.As a precaution for not-quite-as-vetted code that often makes it into admin backends (since you need elevated privileges to access the admin backend), it was decided that DPC could go into Drupal core if it didn’t attempt to cache responses for admin routes.
But here’s the thing:
use_admin_themeinnode.settingsis OFF BY DEFAULT, but TURNED ON BY STANDARD AND THEREFORE ON BY DEFAULT IN MOST CASES. So the tests were running without that route being marked an admin one, and hence DPC was running, and hence it was being cached.I’ve long cursed that stupid flag and it’s weird dissonance between default and “real default”
But of course it should also work when
use_admin_themeis not enabled. Which is what this test is testing. And sadly, that is a bug in core.So there are two possible solutions:
(f.e.
\Drupal\Tests\shortcut\Functional\ShortcutLinksTest::testShortcutQuickLink) does this too.when deleting a revision
Both versions attached.
Comment #17
plach@matsbla:
Yep, I forgot about that button, it will need to be hidden as well.
So the goal here is to implement something we can ship in 8.5. I agree the solution is suboptimal, but it always lets you make a translation unavailable to end users, which is the main goal after all. You can delete the draft by going to the revision list page, once you do that, you can delete the translation in a default revision, if you wish to do so.
Comment #18
plachThis one should be ready for review.
Comment #19
plachComment #20
plachForgot to add a PHP doc. Attached also a test only patch.
Comment #22
plachSmall clean-up.
Comment #23
wim leersI don't know enough about the Entity/Field API and Content Translation implications and architecture to RTBC this patch, but I can review it. Especially for the many cacheability-related things.
👍 This is a temporary work-around, and hence there's both a
@todoand an@internal, great.Nit:
list($foo, )looks weird.The values in
$entityare affecting the entity result. So we need this line before the quoted line:In theory,
isEnabled()should return a cacheable boolean (one with cacheability metadata). But that's out of scope here.The access result here depends on a
LanguageContentSettingsconfig entity. We can construct its cache tag manually for now:$result->addCacheTags(['language_content_settings:' . $entity_type_id . '.' . $entity->bundle()]);The result still depends on
$entity. But because this is overwriting the access result, that cacheability is lost.Fixable by doing
But the service this is calling is going to be removed, so does this need a
@todotoo?Woah! This is tricky.
This can be much simpler:
Same here:
This re-runs the same access check. Can be
$delete_access->isAllowed().Nit: inheritdoc
Nit:
string[]Why not always the latter?
Comment #24
plach@Wim Leers:
Great review, thanks!
This should address #23.
13: Because depending on the permissions, CT exposes different alternative routes in the UI: if a user has no edit permissions, a stripped down edit form or a translation deletion form is presented.
Comment #25
plachAnother minor clean-up.
Comment #26
timmillwoodGenerally looks fine, only a couple of minor nit picks.
I also wonder, would uploading a test-only patch help explain / prove the issue?
Should this be parameterized?
This method seems a bit longer than it needs to be, but makes it nice and easy to read.
Maybe open an issue postponed on #2945956: Allow removing translations in pending revisions to remove this logic, then replace the comment with a single line
@todo Remove in https://www.drupal.org/node/#######.Comment #27
plach1: Not sure what you are asking :)
3: I think #2945956: Allow removing translations in pending revisions is the place to remove those lines, reworded comments to clarify that.
Comment #28
plachAddressed #26.1: I didn't realize there was a typo.
Comment #29
timmillwoodThanks @plach, looks good to me!
Comment #30
effulgentsia commentedI'm afraid this patch makes more changes than I'm personally comfortable committing during the 8.5 RC phase. I'm leaving this as RTBC in case another committer is more comfortable with it.
From what I can tell, it would be ok to commit this or something like it into a patch release. It's not ideal for 8.5.0 to ship with Delete links that appear to work (all the way up to the final status message of "[French] translation of the Article [blablabla] has been deleted."), but don't actually end in a state where it's been deleted. However, as far as I can tell, there's no data loss or corruption or integrity violation: just a status message that wasn't accurate, so I think it's not that bad to fix this in 8.5.1 instead.
For me, it would be easier to commit this (between 8.5.0 and 8.5.1) if the following were split into separate issues:
We need to be really careful with changes to
ContentEntityStorageBasein patch releases (or during RC), since it's very prone to unintended consequences. So, would it be possible to split this into its own issue for more eyes on it?I don't understand yet why it's desirable to change this from checking submitted/rebuilding to checking user input. Could this be split into its own issue for clarity?
If someone else wants to commit this without needing the above split out, that's fine too.
Comment #31
plachSplit the CESB changes out to #2949619: Removed revision translations may reappear when creating a new revision and made it critical, since I realized that that bug introduces data integrity issues. This is now postponed on that one.
I also reverted the unrelated changes to the message display, that were meaning to prevent the message from appearing in some edge cases (e.g. when uploading a file via AJAX or coming back from a node preview). I will create a separate issue for that.
I think it would be way better to commit this before 8.5.0 because, even if technically this bug does not introduce data integrity issues, allowing to delete translations in pending revisions will bring the DB in a state from which the CT UI can no longer work properly (and a future update adding a flag for deleted translations cannot deal with). For sure the fix included in this patch would not restore the UI functionality in those cases.
Comment #32
plachThe blocker was committed, here's a reroll. Back to RTBC.
Comment #33
plachComment #34
plach@effulgentsia suggested a fix in Slack
Comment #35
effulgentsia commentedI committed #2949710: Pending revisions may become unavailable when untranslatable fields affect all translations which #34 conflicts with. Here's a reroll.
Comment #36
effulgentsia commentedWith #2949710: Pending revisions may become unavailable when untranslatable fields affect all translations committed, this now needs cache dependencies added to the access result. Here's a patch that does it one way. Not sure if there's a better way.
Comment #37
gábor hojtsyI think showing the warning on the edit form is strange. I went to edit, its not a warning about editing but about a tab the system is not showing. Why would I care, I am not here to delete? If we put up warnings about things you did not want to do, how do we expect people to read them (when they are actually useful)? :)
I think showing on the translation page may be more accurate, although you may not be going there to delete either, if you don't find the delete button in the tabs, I think you'll go there next.
Comment #38
effulgentsia commentedIs this true with the current UI in HEAD? If so, what are the steps that surface the db being in a non-functional state?
I'm tentatively raising this issue to Critical for this. Even if HEAD's current UI is masking a fundamental db problem that occurs when you delete a translation with a pending revision, if it's true that the db still gets into a state that will create known future data loss problems for us, then we probably do need to get this into 8.5.0 or figure out what to do if we don't.
Comment #39
effulgentsia commentedNW for #37. Unless a consensus emerges to commit #36 anyway, in which case set this back to NR or RTBC as appropriate.
Comment #40
effulgentsia commentedFWIW, I reviewed #36 (and earlier iterations), and would feel comfortable committing it (to both 8.6.x and 8.5.x) once #36's interdiff is reviewed by someone and #37 is addressed. I'm heading to bed now though, and won't be up when the 8.5.0 commit freeze starts in a couple hours, so if another committer in a more convenient timezone feels comfortable committing it once those things are addressed, please do! Otherwise, I'll check in with the release managers tomorrow to see if it's eligible for commit during the freeze.
Comment #41
plachThis addresses #37 and reworks the test to leverage the new
ContentTranslationPendingRevisionTestBaseclass the was recently committed.The interdiff form #36 looks correct to me.
Comment #42
gábor hojtsyUI looks good to me now. As good as it can get in this issue. :)
Comment #43
wim leersI specifically reviewed the #36 interdiff because it touches on cacheability.
This is just adding every
Workflowconfig entity that uses thecontent_moderationWorkflowTypeplugin as a dependency.That's not what I see happen in
::isPendingRevisionSupportEnabled(): that is iterating over all of them, yes, but only to find theWorkflowentities that also happen to affect the given entity type + bundle. IOW:isPendingRevisionSupportEnabled()is trying to find whichWorkflowentity is the one that enables support for pending revisions. So in principle, we should only be adding that oneWorkflowconfig entity as a dependency here, not all of them.But it looks like the idea here is that
WorkflowA could stop handling this entity type+bundle, andWorkflowB could start doing so. Or maybe multiple could do so at the same time? Can multipleWorkflows apply to the same entity type+bundle?If that is the case, then it makes some sense to depend on all
Workflowconfig entities.Although I think that the cacheability here could be much tighter/more optimized than it is right now, this would definitely work correctly. I think it may be adding dependencies on too many things, but that will just result in unnecessary invalidations, not in incorrect behavior.
Optimizing this can be deferred to a follow-up and is likely to be rather pointless since all of this logic is supposed to be superseded in #2940575: Document the scope and purpose of pending revisions anyway.
(The coupling to the
content_moderationmodule here is questionable, but I'm sure that has been raised before. It's also not this issue that introduces it, so out of scope here. And fortunately,\Drupal\content_translation\ContentTranslationManager::isPendingRevisionSupportEnabled()is marked@internal👍.)Comment #44
wim leersThe class is
@internal, can we could also have marked the service private. But that's sadly something we don't really do at all in Drupal core yet, so there's no point holding a critical issue up for that.I'd say that we should add documentation indicating this can and should be improved. But as said in the previous comment:
isPendingRevisionSupportEnabled()is guaranteed to change anyway (and is marked internal). Plus this entire class is also marked as internal. So I feel fine with this.\Drupal\Tests\content_translation\Functional\ContentTranslationPendingRevisionTestBase— looks excellent 👍 Far less repetition, nice.I've reviewed everything since the previous RTBC in #29, after reading all bits of discussion. This is ready
again. The two primary concerns for un-RTBC'ing were @effulgentsia's in #38: the
ContentEntityStorageBasechanges should be handled in another issue (they were: #2949619: Removed revision translations may reappear when creating a new revision) and the message/message checking logic (message improved, shown in a more relevant place, and with none of the previously questionable logic). Those have definitely been addressed. 👏Back to RTBC!
Comment #45
gábor hojtsyComment #48
gábor hojtsyThanks everyone. This was really close to not making it to 8.5.0.
Comment #49
plachYay, thanks to all the people that spent (lunch ;) time on this!
Comment #50
wim leers🎉
Unpostponed #2945956: Allow removing translations in pending revisions.
Comment #52
yonailo commentedThis patch does not seem to be applied to the 9.x branch, why ?