Problem/Motivation

There is currently no way in views to create a select list of states to filter content by. Additionally a relationship to an @internal entity type is required to make the filter work at all.

Proposed resolution

Provide a views filter plugin for the computed "moderation_state" field, which allows you to select a state form a dropdown list as well as preform the necessary joins for filtering.

Remaining tasks

  1. Write the patch.
  2. Make it work with multilingual.
  3. Review.
  4. Commit.

User interface changes

Additional options for users to filter views with.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
#257 interdiff-257.txt988 bytesamateescu
#257 2862041-257.patch63.26 KBamateescu
#256 2862041-256.patch63.29 KBtimmillwood
#256 interdiff-2862041-256.txt1.07 KBtimmillwood
#251 interdiff-251.txt14.68 KBamateescu
#251 2862041-251.patch63.11 KBamateescu
#251 2862041-251-test-only.patch63.11 KBamateescu
#249 2862041-249.patch55.07 KBtimmillwood
#249 interdiff-2862041-249.txt5.08 KBtimmillwood
#248 Screenshot-2018-1-11 Content (Content) drupal 8 5 x(1).png157.52 KBlarowlan
#248 Screenshot-2018-1-11 Content (Content) drupal 8 5 x.png169.02 KBlarowlan
#245 interdiff-245.txt1.25 KBamateescu
#245 2862041-245.patch54.6 KBamateescu
#243 interdiff-243.txt1.18 KBamateescu
#243 2862041-243.patch54.26 KBamateescu
#241 2862041-241.patch53.92 KBManuel Garcia
#236 Peek 2018-01-02 10-00.gif2.77 MBManuel Garcia
#229 interdiff-229.txt849 bytesamateescu
#229 2862041-229.patch53.87 KBamateescu
#227 interdiff.txt1.1 KBamateescu
#227 2862041-227.patch53.91 KBamateescu
#222 Content__Content____Site-Install.png107.17 KBSam152
#220 interdiff.txt10.76 KBamateescu
#220 2862041-220.patch54.06 KBamateescu
#218 interdiff.txt2.11 KBamateescu
#218 2862041-218.patch47.8 KBamateescu
#217 interdiff.txt11.53 KBamateescu
#217 2862041-217.patch48.76 KBamateescu
#211 interdiff.txt6.41 KBamateescu
#211 2862041-211.patch47.96 KBamateescu
#210 interdiff.txt12.69 KBamateescu
#210 2862041-210.patch43.69 KBamateescu
#205 2862041-205.patch46.49 KBjofitz
#205 interdiff-203-205.txt702 bytesjofitz
#203 actually-2862041-201.patch46.49 KBSam152
#201 2862041-201.patch8.08 KBSam152
#201 interdiff.txt1.23 KBSam152
#200 2862041-200.patch45.55 KBSam152
#200 interdiff.txt6.85 KBSam152
#194 2862041-194.patch39.23 KBSam152
#194 2862041-interdiff.txt20.84 KBSam152
#189 Screen Shot 2017-09-10 at 4.41.14 pm.png79.28 KBSam152
#189 Screen Shot 2017-09-10 at 4.34.44 pm.png91.46 KBSam152
#189 2862041-189.patch43.19 KBSam152
#189 interdiff.txt6.65 KBSam152
#186 Screenshot_20170910_054743.png35.35 KBamateescu
#186 interdiff.txt6.78 KBamateescu
#186 2862041-186.patch42.52 KBamateescu
#158 2862041-158.patch39.26 KBSam152
#158 interdiff.txt794 bytesSam152
#155 2862041-155.patch39.26 KBSam152
#155 2862041-155_TEST_ONLY.patch38.82 KBSam152
#155 interdiff.txt4.4 KBSam152
#152 2862041-152.patch38.47 KBSam152
#152 2862041-152_TEST_ONLY.patch38.24 KBSam152
#152 interdiff.txt4.78 KBSam152
#149 2862041-149.patch44.41 KBtimmillwood
#149 2862041-149.txt2.92 KBtimmillwood
#147 2862041-147.patch41.41 KBSam152
#147 interdiff.txt788 bytesSam152
#146 2862041-146.patch41.49 KBSam152
#146 interdiff.txt7.83 KBSam152
#143 three_nodes.png101.51 KBxjm
#143 frontpage_empty.png39.5 KBxjm
#143 no_valid_values.png13.45 KBxjm
#143 filters_unknown.png40.14 KBxjm
#143 filter_config_no_relevant_bundles.png79.36 KBxjm
#135 2862041-131.patch35.23 KBamateescu
#133 interdiff-2862041-132-133.txt669 byteshamrant
#133 2862041-133.patch36.65 KBhamrant
#132 2862041-132.patch36.61 KBhamrant
#132 interdiff-2862041-131-132.txt2.98 KBhamrant
#131 2862041-131.patch35.23 KBSam152
#131 interdiff.txt4.65 KBSam152
#127 2862041-127.patch37.07 KBSam152
#123 2862041-123.patch37.72 KBtimmillwood
#123 interdiff-2862041-123.txt1.12 KBtimmillwood
#120 2862041-120.patch37.13 KBtimmillwood
#120 interdiff-2862041-120.txt584 bytestimmillwood
#116 2862041-116.patch37.13 KBtimmillwood
#116 interdiff-2862041-116.txt1.34 KBtimmillwood
#114 2862041-108.patch36.58 KBtimmillwood
#109 2862041-108-TESTS--64-FILTER_FAIL.patch36.14 KBSam152
#109 interdiff_FAIL.txt4.48 KBSam152
#108 2862041-108.patch36.58 KBtimmillwood
#108 interdiff-2862041-108.txt2.85 KBtimmillwood
#106 2862041-106.patch36.56 KBtimmillwood
#106 interdiff-2862041-106.txt3.61 KBtimmillwood
#101 interdiff-2862041.txt1.76 KBdawehner
#101 2862041-101.patch35.06 KBdawehner
#98 interdiff.txt972 bytesSam152
#98 2862041-98.patch35.38 KBSam152
#96 interdiff-2862041.txt3.83 KBdawehner
#96 2862041-96.patch34.73 KBdawehner
#90 interdiff.txt1.98 KBamateescu
#90 2862041-90.patch35.13 KBamateescu
#81 2862041-80.patch34.55 KBtimmillwood
#79 2862041-79.patch74.03 KBtimmillwood
#79 interdiff-2862041-79.txt1.04 KBtimmillwood
#77 2862041-77.patch34.44 KBtimmillwood
#77 interdiff-2862041-77.txt1.19 KBtimmillwood
#68 Screen Shot 2017-08-14 at 9.23.56 PM.png96.34 KBwebchick
#66 Screen Shot 2017-08-14 at 12.07.34 PM.png72.1 KBSam152
#66 Screen Shot 2017-08-14 at 12.07.11 PM.png122.36 KBSam152
#66 Screen Shot 2017-08-14 at 12.06.50 PM.png55.45 KBSam152
#64 provide_useful_views-2862041-64.patch34.01 KBjibran
#64 interdiff-c1296d.txt1.55 KBjibran
#62 provide_useful_views-2862041-62.patch33.97 KBjibran
#62 interdiff-d8f8b7.txt2.02 KBjibran
#59 2862041-59.patch33.22 KBSam152
#59 interdiff.txt1.66 KBSam152
#49 2862041-49.patch33.02 KBSam152
#49 interdiff.txt2.8 KBSam152
#46 2862041-46.patch33.01 KBamateescu
#36 2862041-states-views-filter-36.patch33.02 KBSam152
#36 interdiff.txt2.69 KBSam152
#8 content_moderation-provide-checklist-workflow-states-views-filter-2862041-8.patch2.15 KBmgalalm
#12 2862041-states-views-filter-12.patch17.27 KBSam152
#16 interdiff.txt7.75 KBSam152
#16 2862041-states-views-filter-16.patch23.9 KBSam152
#17 2862041-states-views-filter-17.patch23.92 KBSam152
#17 interdiff.txt740 bytesSam152
#20 interdiff.txt1.03 KBSam152
#20 2862041-states-views-filter-20.patch24.08 KBSam152
#24 interdiff.txt13.39 KBSam152
#24 2862041-states-views-filter-24.patch32.15 KBSam152
#31 2862041-states-views-filter-30.interdiff.txt609 bytesplach
#31 2862041-states-views-filter-30.patch32.75 KBplach
#31 2862041-states-views-filter-30.combined.patch44.71 KBplach
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

timmillwood created an issue. See original summary.

plach’s picture

Assigned: Unassigned » plach
Priority: Normal » Major
Issue tags: +WI critical

This was defined as MUST-HAVE by @webchik in #2755073-10: WI: Content Moderation module roadmap:

So it seems like another must-have (but not commit-blocking) is to get that widget to be a drop-down instead, since I don't think we want this to be the content author experience.

Working on this.

Sam152’s picture

plach’s picture

Yes, it seems so. I closed that since this is linked in the meta issues, I will summarize any relevant info here.

Sam152’s picture

I spoke about this briefly in IRC, but I think it would be great to provide good views support for the computed content moderation state field on the entities under moderation, without the need to add a relationship to the ContentModerationState entity.

If we can do that here for filtering, #2852067: Add support for rendering computed fields to the "field" views field handler solves being able to view the state.

After we have filtering and support for viewing the computed field on the moderated entity, I think we should remove the views relationship to the ContentModerationState entirely, honoring it's @internal status by putting it out of sight of users.

Sam152’s picture

plach’s picture

Assigned: plach » Unassigned

I'm not going to get to this one before Monday morning.

mgalalm’s picture

Please see attached patch for an attempt to add this feature

handkerchief’s picture

Please integrate this "feature" in the next drupal core 8.3.x update. 8.4.x is far away isn't? It's a really basic and required funcionality.

timmillwood’s picture

Status: Active » Needs work
Issue tags: +Needs tests

@handkerchief - This won't get into 8.3.x, as it is WI critical it should get into 8.4.x. The 8.4.0 alpha release is only 2 weeks away, with the final release early october.

@mgalalm

+++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php
@@ -179,4 +179,25 @@ protected function realSave() {
+  public static function getStates() {
+    $workflow_ids = \Drupal::entityQuery('workflow')
+      ->execute();
+    $storage = \Drupal::entityTypeManager()->getStorage('workflow');
+    $states = [];
+    foreach ($storage->loadMultiple(array_keys($workflow_ids)) as $workflow) {
+      $workflow_states = $workflow->getStates();
+      foreach ($workflow_states as $state) {
+        $states[$state->id()] = $state->label();
+      }
+    }
+    return $states;
+  }

This shouldn't go in ContentModerationState. It has nothing to do with the ContentModerationState entity.

Also, we need tests for this.

Sam152’s picture

Assigned: Unassigned » Sam152

Going to have a look at this. As per #6 going to try and ensure the filter is applied to the computed field on the moderated entity, not the ContentModerationState entity, to ensure no relationship is necessary in the views configuration.

Sam152’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
17.27 KB

Patch attached allows filtering on the computed field, directly on the entity being moderated, no relationships required.

Sam152’s picture

Assigned: Sam152 » Unassigned
plach’s picture

Having a look at this, however the IS needs some love :)

Sam152’s picture

Status: Needs review » Needs work

@plach pointed out this doesn't seem to work with a view of node revisions. NW for fixing that + tests.

Sam152’s picture

Status: Needs work » Needs review
FileSize
7.75 KB
23.9 KB

I wasn't able to reproduce the bug, but adding a test case.

Sam152’s picture

The field map is required in the assert to make this fail properly.

plach’s picture

Status: Needs review » Needs work

Code is looking great and on manual testing things look very nice too, awesome work!

I was able to find a problem with content revision views and multilingual: apparently this view works perfectly on a monolingual site but adding a language breaks the query.

Aside from that, I have only a few very minor remarks:

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,92 @@
    + * Filter based on the moderation state of an entity.
    

    We should use a third person verb to start the doc block, what about "Provides a filter..."?

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,92 @@
    +   * Create an instance of ModerationStateFilter.
    

    Creates

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,92 @@
    +    $this->query->addRelationship('cms', $join, 'content_moderation_state_field_revision');
    

    Views normally uses more verbose aliases, what about replacing cms with content_moderation_state for consistency?

  4. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,173 @@
    +   * Test the list of states in the filter plugin.
    

    Tests

  5. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,173 @@
    +    // Adding nodes to the editorial workflow will enable all of the editorial
    +    // states.
    

    "nodes" is a bit misleading, what about "a content type"?

  6. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,173 @@
    +   * @param array $states
    +   *   The states which should appear in the filter.
    ...
    +   * @param array $filters
    +   *   An array of filters to apply to the view.
    

    Shouldn't these be string[]?

  7. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,173 @@
    +   * @param array $nodes
    +   *   Nodes to assert are in the views result.
    

    Shouldn't this be \Drupal\node\NodeInterface[]?

Sam152’s picture

Issue summary: View changes
Sam152’s picture

Status: Needs work » Needs review
FileSize
1.03 KB
24.08 KB

Thanks for the review, will address the feedback. This shows us the failure, but I suspect we'll also need to add another part of the 'extra' section to ensure langcode becomes part of the join criteria, unless that's auto-magically handled.

FWIW, the fail boils down to:

Integrity constraint violation: 1052 Column 'langcode' in field list is ambiguous

Can't see any obvious fixes for it yet.

Sam152’s picture

Status: Needs review » Needs work
plach’s picture

I'll try to have a look to that failure during the weekend if you don't beat me to it, tomorrow I'll be AFK.

Sam152’s picture

Sam152’s picture

Status: Needs work » Needs review
Issue tags: -Needs issue summary update
FileSize
13.39 KB
32.15 KB

This patch addresses #18 and adds multilingual support + tests.

It will go green as soon as the blocker is resolved.

Status: Needs review » Needs work

The last submitted patch, 24: 2862041-states-views-filter-24.patch, failed testing. View results

tacituseu’s picture

Sam152’s picture

Thanks, @tacituseu, I've created a test and patch for the blocker. @plach, if have time to look at the progress on it, that'd be great. Looks like you're already familiar with the issue.

plach’s picture

Title: Provide useful Views filters for Content Moderation State fields. » [PP-1] Provide useful Views filters for Content Moderation State fields.
Status: Needs work » Postponed
dawehner’s picture

The test coverage looks great!

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,98 @@
    +  /**
    +   * Creates an instance of ModerationStateFilter.
    +   */
    +  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
    ...
    +    $this->entityTypeManager = $entity_type_manager;
    

    Nitpick: let's document the parameters and provide class documentation for them

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,98 @@
    +      if (in_array($this->getEntityType(), $workflow->getTypePlugin()->getEntityTypes())) {
    

    Let's use string comparison, you never know :)

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,98 @@
    +      'table' => 'content_moderation_state_field_revision',
    +      'field' => 'content_entity_revision_id',
    

    This information could be fetched from the content moderation state entity key definition, right?

  4. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,98 @@
    +        'field' => 'langcode',
    

    Is there a reason you couldn't get the entity key in the content_moderation_state entity?

plach’s picture

Here's a patch combined with #2896381-9: "TranslationLanguageRenderer" uses the wrong table for the "langcode" column with entity revision views to check whether we are done here. This includes a one-line fix to a test module to fix a couple failures.

Setting to NR for the bot but this is still postponed.

plach’s picture

Missed #30, setting to NW then.

plach’s picture

Status: Needs review » Needs work
Sam152’s picture

Looks like your work in the blocker is going to work for us here. I've +1d the approach in that issue, but having written the test, if it would be great if @dawehner could RTBC.

#30 is good feedback, happy to look at it. Re: #30.3 and #30.4, do we usually get that information from entity definitions when we are working with only a single specific entity type? If those fields ever changed, the tests would inform us right?

amateescu’s picture

Re: #30.3 and #30.4, do we usually get that information from entity definitions when we are working with only a single specific entity type? If those fields ever changed, the tests would inform us right?

If they were ever changed in core, yes, but contrib and custom code are free to rename things to whatever they want, so we should always get the table and field names from the entity definition.

Sam152’s picture

Status: Needs work » Needs review
FileSize
2.69 KB
33.02 KB

Addressing the feedback in #30. Not combined with the fix, so will go read.

plach’s picture

Looks RTBC to me, I'll move this back to postponed as soon as the bot is done testing.

Status: Needs review » Needs work

The last submitted patch, 36: 2862041-states-views-filter-36.patch, failed testing. View results

plach’s picture

Status: Needs work » Postponed
Anybody’s picture

Perfect, confirming RTBC by my manual tests. As soon as the testbot tests succeed this should become part of the module. It's really essential for building useful moderation views.

Thank you all so much.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

timmillwood’s picture

Version: 8.5.x-dev » 8.4.x-dev

This is a "should have" to get Content Moderation stable, so moving back to 8.4.x

hamrant’s picture

@Sam152 thanks! patch #36 works great!

DuneBL’s picture

I think it needs a reroll for 8.5 (one FAILED)

patching file core/modules/content_moderation/config/schema/content_moderation.schema.yml
Hunk #1 succeeded at 16 with fuzz 2 (offset 10 lines).
patching file core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
patching file core/modules/content_moderation/src/ViewsData.php
Hunk #1 succeeded at 203 (offset 2 lines).
Hunk #2 succeeded at 223 (offset 2 lines).
patching file core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter.yml
patching file core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_entity_test.yml
patching file core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter_revision_table.yml
patching file core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml
Hunk #1 FAILED at 8.
1 out of 1 hunk FAILED -- saving rejects to file core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml.rej
patching file core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
patching file core/modules/system/tests/modules/entity_test/src/Entity/EntityTestNoBundle.php
DuneBL’s picture

Just to be clear: I was talking of the patch #36
And, after applying it on 8.5, I got the hideous The handler for this item is broken or missing. The following details are available:

amateescu’s picture

Sam152’s picture

Title: [PP-1] Provide useful Views filters for Content Moderation State fields. » Provide useful Views filters for Content Moderation State fields.
Status: Postponed » Needs review

Status: Needs review » Needs work

The last submitted patch, 46: 2862041-46.patch, failed testing. View results

Sam152’s picture

Status: Needs work » Needs review
FileSize
2.8 KB
33.02 KB

Removing calls to removed methods.

DuneBL’s picture

Many thanks for the reroll.
I have applied #49 on 8.5 and it apply more cleanly.
Here is the only problem

patching file core/modules/content_moderation/config/schema/content_moderation.schema.yml
patching file core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml
Hunk #1 FAILED at 8.
1 out of 1 hunk FAILED -- saving rejects to file core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml.rej

Unfortunately, the problem remains (after cache clear): when I tried to add the moderation state field in the view UI, I got the following message:

The handler for this item is broken or missing.

jeqq’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @Sam152! Tested #49, it works great with 8.4.x.

jibran’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +VDC

Just some minor nits.

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,106 @@
    +      if (in_array($this->getEntityType(), $workflow->getTypePlugin()->getEntityTypes(), TRUE)) {
    +        $states += $workflow->getTypePlugin()->getStates();
    

    Let's store plugin in a local variable and not call the method twice.

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,106 @@
    +      'operator' => '=',
    

    This is not needed.

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,106 @@
    +    $join = Views::pluginManager('join')->createInstance('standard', $configuration);
    

    We have seen some nasty bugs related to join configurations and it's so easy to write the views data wrong for joins. Can we please have a \Drupal\Tests\views\Kernel\Plugin\JoinTest kind of tests for this, just to make sure that in future whenever someone changes this they have to update the test as well.

jibran’s picture

Issue tags: +Needs screenshots

Can we please also have some screenshots of the filters and resultant queries?

DuneBL’s picture

@Sam152: At the beginning of this issue, I can read

2-Make it work with multilingual.

Is it because my site is multilingual that #49 doesn't solve the issue or is it because it is 8.5?

Sam152’s picture

Issue summary: View changes

Issue summary just needs an update. The patch works with multilingual.

Sam152’s picture

Re: #52.3, I don't know what this would look like, we don't want to retest the abstraction we're using?

jibran’s picture

Status: Needs review » Needs work

I had a quick chat with @dawehner and he agreed with #52.3.

dawehner: do you think we need specific test for #52.3 in https://www.drupal.org/node/2862041#comment-12217439? If not then can you please reply to the issue?
jibran: I would love to see that

Sam152’s picture

Like I said in #56, what does this actually look like? Can you add it to the patch?

Sam152’s picture

Status: Needs work » Needs review
Related issues: +#2901690: Implement ModerationStateFilter::calculateDependencies.
FileSize
1.66 KB
33.22 KB

Addressing #52.(1|2) and filing a follow-up.

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

@jibran, this patch does not add a new views join plugin, so I also don't understand #52.3. Can you open a followup and explain in more detail what you meant?

Otherwise, the patch looks really good!

jibran’s picture

Status: Reviewed & tested by the community » Needs review

I'm working on a patch. I'll post soon.

jibran’s picture

Added asserts for join config.
Here is resultant query:

SELECT node_field_data.langcode AS node_field_data_langcode, node_field_data.nid AS nid, node_field_revision_content_revision_tracker.vid AS node_field_revision_content_revision_tracker_vid
FROM 
{node_field_data} node_field_data
LEFT JOIN {content_revision_tracker} content_revision_tracker ON node_field_data.nid = content_revision_tracker.entity_id AND (content_revision_tracker.entity_type = :views_join_condition_0 AND content_revision_tracker.langcode = node_field_data.langcode)
INNER JOIN {node_field_revision} node_field_revision_content_revision_tracker ON content_revision_tracker.revision_id = node_field_revision_content_revision_tracker.vid AND (content_revision_tracker.entity_type = :views_join_condition_2 AND node_field_revision_content_revision_tracker.langcode = content_revision_tracker.langcode)
LEFT JOIN {content_moderation_state_field_revision} content_moderation_state ON node_field_revision_content_revision_tracker.vid = content_moderation_state.content_entity_revision_id AND (content_moderation_state.content_entity_type_id = :views_join_condition_4 AND content_moderation_state.langcode = node_field_revision_content_revision_tracker.langcode)
WHERE content_moderation_state.moderation_state IN (:db_condition_placeholder_6)
amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Still looks good apart from these two nitpicks, which could be fixed on commit :)

  1. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -102,6 +102,7 @@ public function testStateFilterViewsRelationship() {
     
    +
         // The latest revision for both nodes is a draft status.
    

    Extra new line here.

  2. +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -227,10 +228,20 @@ protected function assertPluginStates($states) {
         $view->execute();
    +    $query = $view->getQuery();
    ...
    +    $this->assertEquals('langcode', $configuration['extra'][1]['left_field']);
         $expected_result = [];
    

    And here we could have some new lines to delimit these assertions.

jibran’s picture

Here we go.

Sam152’s picture

Looks good. RTBC++

Sam152’s picture

Adding screenshots to IS as requested.

jibran’s picture

Thanks, Sam.

webchick’s picture

Status: Reviewed & tested by the community » Needs work
FileSize
96.34 KB

I'll try and go back to find it, but the original must-have was that the Content view (or, failing that, a sub-tab) have a moderation state drop-down when Content Moderation module (or Workflow or whatever makes most sense) is installed. Because without that, people within the content approval flow get hard-blocked and can't actually "do" anything, because there's no way (without engaging the services of a Views Master™) to get a list of content in a Draft state:

Only published and unpublished

The preview screenshot at https://www.drupal.org/files/issues/Screen%20Shot%202017-08-14%20at%2012... looks great; we just need to either apply that to the default Content view, or if that's too hard, add a "Moderate" or something sub-tab to that view that exposes the filter.

We can go over this on tomorrow's UX call if you'd like.

Sam152’s picture

That would be great, but does it have to block this issue? This patch introduces the technical capability to add the filter. The details of integrating is mostly a UX conversation, whereas this has been a technical one so far.

webchick’s picture

We can do it as a follow-up, sure. But either way, it's a must-have and stands in the way of the module being marked stable, so whatever's easiest for you all.

webchick’s picture

Status: Needs work » Needs review

Bumping back to needs review to reflect this.

Sam152’s picture

I'm inclined to go for a follow-up, just based on narrower scoped issues being easier to review and commit.

webchick’s picture

Status: Needs review » Reviewed & tested by the community

Ok cool, moving this one back to RTBC, will create a new WI critical in a sec w/ the contents of #68.

webchick’s picture

Sam152’s picture

Based on the comment you made in #2755073: WI: Content Moderation module roadmap, I should clarify. This issue doesn't add a view or change any existing views configuration. It adds the ability for a site builder to add a views filter based on the "moderation state" field, which wasn't possible previously.

timmillwood’s picture

Issue tags: +Workflow Initiative
timmillwood’s picture

FileSize
1.19 KB
34.44 KB

This issue didn't work with "NULL" or "NOT NULL" options, borrowing ideas from InOperator, this patch fixes that.

gambry’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,134 @@
    +  protected function opEmpty() {
    ...
    +  protected function opSimple() {
    

    Missing DocBlocks.

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,134 @@
    +    $this->ensureMyTable();
    ...
    +    $this->query->addWhere($this->options['group'], 'content_moderation_state.moderation_state', NULL, $operator);
    

    I'm not sure how ensureMyTable() is useful in here if we don't use $this->tableAlias in the addWhere().
    It's a shame we are overriding parent method only because 'content_moderation_state.moderation_state' is hardcoded.

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,134 @@
    +    if ($this->operator == 'empty') {
    

    This should probably be ===.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
1.04 KB
74.03 KB

Thanks for the review, this patch addresses those three items.

I agree with #78.2, it is a shame we just can't use the parent methods, but Content Moderation is pretty special with it's ContentModerationState entity.

Status: Needs review » Needs work

The last submitted patch, 79: 2862041-79.patch, failed testing. View results

timmillwood’s picture

Status: Needs work » Needs review
FileSize
34.55 KB

I messed up the patch, here's a new one, interdiff was still fine.

plach’s picture

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -103,9 +103,11 @@ public function query() {
-    $this->ensureMyTable();

I think @gambry was actually suggesting to use the table alias returned by ::ensureMyTable() in the where condition instead of hardcoding content_moderation_state. Unless it's assuming the wrong table obviously :)

timmillwood’s picture

@plach - ::ensureMyTable() returns a Node table, not a ContentModerationState table.

plach’s picture

Ok, then ignore me :)

plach’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC, actually

timmillwood’s picture

Been talking to @amateescu and he reminded me of #2817835-37: When enabling moderation apply a relative state where he suggested using COALESCE(). Here's some initial R&D around how we could use that to handle filtering entities which were created before moderation was enabled, and thus don't have a content_moderation_state entity to query against.

diff --git a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
index ea2c1d5694..2516ba18d1 100644
--- a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -114,7 +114,7 @@ protected function opEmpty() {
       $operator = "IS NOT NULL";
     }
 
-    $this->query->addWhere($this->options['group'], 'content_moderation_state.moderation_state', NULL, $operator);
+    $this->query->addWhereExpression($this->options['group'], "COALESCE(content_moderation_state.moderation_state, IF({$this->tableAlias}.status = 0, 'draft', 'published')) {$operator}");
   }
 
   /**
@@ -124,8 +124,8 @@ protected function opSimple() {
     if (empty($this->value)) {
       return;
     }
-
-    $this->query->addWhere($this->options['group'], 'content_moderation_state.moderation_state', $this->value, $this->operator);
+    $value = join("', '", $this->value);
+    $this->query->addWhereExpression($this->options['group'], "COALESCE(content_moderation_state.moderation_state, IF({$this->tableAlias}.status = 0, 'draft', 'published')) {$this->operator} ('{$value}')");
   }
 
   /**

We still need to handle entity types that don't have moderation enabled, and non-publishable entity types (such as block content).

timmillwood’s picture

Even with #86 I still support the RTBC.

If COALESCE() is something we want to do we can add it in #2902187: Provide a way for users to moderate content or another issue.

webchick’s picture

After discussing #2902187: Provide a way for users to moderate content with Tim earlier today, I don't think we need to explore the COALESCE option. With the tweaks suggested at #2902187-54: Provide a way for users to moderate content over there, the view makes sense to do what it's currently doing, and only showing things in a "needs moderation" state.

tim.plunkett’s picture

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,139 @@
    +        $states += $workflow_type->getStates();
    

    If this is a non-associative array, the += will fail. Could result in a pretty nasty bug?
    Should use NestedArray::mergeDeep().
    IMO this needs test coverage.

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,139 @@
    +    $this->valueOptions = array_map(function($state) {
    

    array_map(function (StateInterface $state) { (missing space and typehint)

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,139 @@
    +   * Add the where query for NULL and NOT NULL operators.
    ...
    +   * Add the where query for "in" and "not in" operators.
    

    {@inheritdoc}

  4. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,139 @@
    +    // @todo calculate dependencies in https://www.drupal.org/node/2901690.
    

    That is quite the punt, but okay

amateescu’s picture

@tim.plunkett, thanks for reviewing! :)

Re #89:

  1. \Drupal\workflows\WorkflowTypeInterface::getStates() always returns an associative array, I expanded its documentation because it wasn't clear in this regard. And that code is pretty well tested in \Drupal\Tests\content_moderation\Kernel\ViewsModerationStateFilterTest::testStateFilterStatesList()
  2. Fixed.
  3. Fixed as well.
  4. :/
xjm’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,140 @@
+  public function calculateDependencies() {
...
+    return parent::calculateDependencies();
+  }

Hmmm I don't think we should scope that to a followup. Wrong dependency info == data integrity issues.

We are also missing dedicated test coverage for the new view. There should be test coverage for all the twists and turns this issue has gone down in terms of what to display. This is not the kind of test coverage that should be scoped to followps.

Also let's not mark this issue RTBC with a combined patch with others that haven't landed yet, and please provide the no-test patches so it's easier to read what's part of this issue. Thanks!

Sam152’s picture

I think #91 was meant to be on #2902187: Provide a way for users to moderate content?

In this instance, when states disappear because of a deleted workflow, views handles this gracefully and the states simply disappear from the list. The impact is some stale state keys in configuration until the filter is resaved. Is there any chance of it being split up like this to keep things moving? Given this is already kinda big, I thought a follow-up would make more sense.

dawehner’s picture

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,140 @@
+    $this->query->addRelationship('content_moderation_state', $join, 'content_moderation_state_field_revision');
...
+  protected function opEmpty() {
...
+    $this->query->addWhere($this->options['group'], 'content_moderation_state.moderation_state', NULL, $operator);
...
+  protected function opSimple() {
...
+    $this->query->addWhere($this->options['group'], 'content_moderation_state.moderation_state', $this->value, $this->operator);

Is there a reason we don't assign $this->tableAlias , given that this is what this relationship represents? Once done we could get rid of opEmpty and opSimple

larowlan’s picture

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,140 @@
+    // @todo calculate dependencies in https://www.drupal.org/node/2901690.

I'm with @tim.plunkett on this - that's a big gamble. I think without that, backporting this to 8.4 would be off the table. So does that reduce the urgency here?

amateescu’s picture

@dawehner, that was asked before in #82 and replied to in #83 ;)

dawehner’s picture

@amateescu
At least #82/#83 is not what I would have intended.

Note: I'm uploading the patch to see whether it fails ...

Status: Needs review » Needs work

The last submitted patch, 96: 2862041-96.patch, failed testing. View results

Sam152’s picture

Status: Needs work » Needs review
FileSize
35.38 KB
972 bytes

Updating the @todo with a comment as to why we shouldn't actually implement calculateDependencies.

@see #2901690: Implement ModerationStateFilter::calculateDependencies. for some further discussion on this.

timmillwood’s picture

Assigned: Unassigned » timmillwood
+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,117 @@
+    if (!isset($this->tableAlias)) {
...
+        'left_table' => $this->tableAlias,

The condition is if table alias is not set, but then we use table alias for the left_table. Something tells me this is why the test is failing.

timmillwood’s picture

Assigned: timmillwood » Unassigned
diff --git a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
index 091210d86a..b07ef0553c 100644
--- a/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -84,7 +84,7 @@ public function ensureMyTable() {
       $configuration = [
         'table' => $entity_type->getRevisionDataTable(),
         'field' => 'content_entity_revision_id',
-        'left_table' => $this->tableAlias,
+        'left_table' => $left_entity_type->getRevisionDataTable(),
         'left_field' => $left_entity_type->getKey('revision'),
         'extra' => [
           [
@@ -100,7 +100,7 @@ public function ensureMyTable() {
         ];
       }
       $join = Views::pluginManager('join')->createInstance('standard', $configuration);
-      $this->tableAlias = $this->query->addRelationship('content_moderation_state', $join, 'content_moderation_state_field_revision');
+      $this->tableAlias = $this->query->addRelationship($entity_type->id(), $join, $entity_type->getRevisionDataTable());
     }
 
     return $this->tableAlias;

These changes give the same failing tests as #96. In one test the table is missing on content_moderation_state.langcode = .langcode and on another the table is missing on revision_id = content_moderation_state.content_entity_revision_id.

dawehner’s picture

Here is a fix for my experiment.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

Tested locally, much clearer, thanks @dawehner.

That comment makes sense based on #2901690: Implement ModerationStateFilter::calculateDependencies., hope it's ok for @larowlan.

amateescu’s picture

The changes from #96 and #101 seem to make sense, there's just one problem though: the patch that fixed "NULL" or "NOT NULL" handling in #77 did not add any tests for the fix, so we don't know if the latest changes from @dawehner have any impact on them..

jibran’s picture

Changes look good. Thanks @dawehner for jumping in.

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -74,6 +74,8 @@ public function getValueOptions() {
+      $node_table_alias = $this->query->ensureTable($this->table, $this->relationship);

@@ -84,7 +86,7 @@ public function ensureMyTable() {
+        'left_table' => $node_table_alias,

Let's change the name to $table_alias. Can be fixed on commit.

amateescu’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

I think #103 is pretty serious though, we have no idea if "NULL" or "NOT NULL" handling is still working after the latest changes, since they are not tested.

timmillwood’s picture

Status: Needs work » Needs review
FileSize
3.61 KB
36.56 KB

I have been doing a bit of manual testing, and the NULL and NOT NULL look to be pretty stable, here's an initial patch which adds a NOT NULL filter to the main test view, also fixes #104.

Status: Needs review » Needs work

The last submitted patch, 106: 2862041-106.patch, failed testing. View results

timmillwood’s picture

Status: Needs work » Needs review
FileSize
2.85 KB
36.58 KB

- Fixing the views config
- Adding a non moderated content type and node to test it isn't listed in any views.
- Adding a test for a view with no options selected in the exposed filters.

Sam152’s picture

I ran the tests in #108 against the filter in #64, the latest patch before the NULL handling was added. Fail patch attached should prove they work as described.

Sam152’s picture

Status: Needs review » Needs work

The last submitted patch, 109: 2862041-108-TESTS--64-FILTER_FAIL.patch, failed testing. View results

Sam152’s picture

Status: Needs work » Reviewed & tested by the community

Fail expected, RTBC for #108.

Sam152’s picture

Issue tags: -Needs tests
timmillwood’s picture

Awesome, thanks for testing @Sam152!

Reuploading #108 so it's 100% clear this is the patch to be committed.

gambry’s picture

Is there a reason why the views schema configuration lives in content_moderation.schema.yml instead of content_moderation.views.schema.yml?

I can't find any documentation/best practice about it but I can see all core modules have the views config schema definitions inside *.views.schema.yml .
If there isn't any and views schema should be in *.views.schema.yml for consistency, it can be done in a follow-up as there is already a pre-existing views.filter.latest_revision to be moved too.

timmillwood’s picture

Good point @gambry, lets move all views schema to content_moderation.views.schema.yml, tested all content_moderation tests locally, so going straight o RTBC.

xjm’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests
  1. +++ b/core/modules/content_moderation/config/schema/content_moderation.views.schema.yml
    @@ -0,0 +1,11 @@
    +  label: 'Moderation State Filter'
    

    Core UI labels should be sentence case rather than title case unless it's a proper noun (like the name of a module).

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,123 @@
    +    // Do not depend on the workflows which provide states that are utilised
    +    // in the filter. States cannot be depended on directly and can also be
    +    // common across multiple workflows. States and workflows with existing
    +    // content cannot be deleted, so the impact of not depending on them here is
    +    // mitigated.
    +    return parent::calculateDependencies();
    

    Hmmm. So I think this is saying that we won't have (e.g.) access bypasses due to a deleted state because no content would be in the state anymore if the filter were no longer functional.

    I'm still not entirely sure about this; "mitigating" the impact of potential data integrity issues is still not a reason to introduce them. We also get into trouble a lot when we make assumptions like this for core, but then contrib does something with the data that bypasses core expectations.

    At a minimum, we should have thorough functional test coverage for what we expect to happen in different scenarios where relevant workflows, states, provider modules, etc. are not available.

    I'd also want to see and review that the view is properly cleaned up (without noisy diffs in config), that it can be re-saved afterward, that it is still functional, etc.

xjm’s picture

For others, here's the discussion from the other issue. From @amateescu:

I don't think there's any reasonable way to implement this. The only way for a view to really depend on a workflow state is if we implement states as config entities.

Perhaps supporting all of these edge-cases is going a bit beyond what is necasary? The impact of a state disappearing when it's being used in a filter value seems to be fairly minimal, it simply no longer appears in the list. The only fallout as far as I can see is you have a state in your views config object which is stale until the filter is resaved.

I would go even further and say they are not even edge-cases, they are no-cases :) The view shouldn't care about states at all, they should only care about the workflow config entities that contain those states and their workflow type dependencies.

The reason is this: let's say we have a view with a moderation_state filter configured with a state, so it will show (or exclude) content with that state. Now, a workflow state can not be removed from a workflow when there is existing content with that state. In order to remove a state, the user also has to remove all the content using that state, at which point the view filter will have nothing to filter by because there's no more content with that state.

So your analysis above is correct, the impact of a state disappearing when it's being used in a filter value is zero :)

If we expand the reasoning for not depending on states, I think the outcome is the same: a workflow can only be deleted when there's no content using it, and, when that happens, the view doesn't show anything different based on the workflow existing or not. So.. yep, we don't need to depend on workflows at all.

And from @Sam152:

I think it could be argued that implementing something like this would mean the views filter configuration would not contain references to defunct states. Ie, a state with no content could be added to the filter, then deleted and the views filter configuration would still contain that state, even though it doesn't exist.

Given your analysis in #7 confirming what I suspected in #3, is this worth supporting? I would agree, no.

amateescu’s picture

@xjm, please see #2901690-7: Implement ModerationStateFilter::calculateDependencies. and #2901690-9: Implement ModerationStateFilter::calculateDependencies. for the reasoning behind that code comment. I'll repeat a short version of it here as well:

Workflow states can not be deleted while there is existing content using them. When that content is gone and the state can be deleted, a views filter that excludes entities with that workflow state will have nothing left to exclude because there are no entities with the given state anymore.

There is no impact to mitigate because there is no impact at all. The code comment shouldn't say anything about mitigation actually :)

timmillwood’s picture

Status: Needs work » Needs review
FileSize
584 bytes
37.13 KB

Although it could be done on commit, here's a patch fixing #117.1.

I agree with there being no need to implement calculateDependencies, and tried, but can't think of a better comment to explain this. We put a lot of work into making sure that states and workflows cannot be deleted via the UI or via config import when there is content uses the workflows or states, so this is a non-issue.

Moving to "Needs review" for another check by @xjm prior to commit.

Sam152’s picture

I think this part is still outstanding:

At a minimum, we should have thorough functional test coverage for what we expect to happen in different scenarios where relevant workflows, states, provider modules, etc. are not available.

I'd also want to see and review that the view is properly cleaned up (without noisy diffs in config), that it can be re-saved afterward, that it is still functional, etc.

But if we can agree in principle it'll be empty, maybe that isn't a blocker.

amateescu’s picture

The "where provider modules are not available" part got me thinking that we should do this: #2904101: Do not allow modules that provide a workflow type plugin with existing data to be uninstalled

timmillwood’s picture

Issue tags: -Needs tests
FileSize
1.12 KB
37.72 KB

Here is a patch updating the test to make sure that if a workflow or state is removed the states are removed from the filter. Workflows and States which are in use cannot be deleted therefore testing the entities returned by a view isn't possible.

Sam152’s picture

I think the testing in #123 is sufficient for to at least unblock this issue and further work on the follow-up view.

The primary concern that was blocking this issue, in my understanding was that a missing implementation of calculateDependencies would cause serious problems. If that has now been debunked, in my eyes it'd be best to get this in now with a follow-up (which I'll happily allocate time to) to cover more detailed testing that dives into:

  • Functional testing around resaving the filter from the UI.
  • The associated views configuration before/after resaving with deleted states.
  • Proof the result-set is always unchanged based on @amateescu's analysis.

My reasoning for addressing this in a follow-up is to unblock folks working on #2902187: Provide a way for users to moderate content quickly. I'm hoping the above doesn't need to block that.

@xjm, if that approach is too much of the cart before the horse, it'd be great if you could chime in so we can prioritise work accordingly.

Sam152’s picture

Status: Needs review » Reviewed & tested by the community
hamrant’s picture

It would be cool to add to the filter settings the ability to select the workflow type. For example, I have 2 workflow types and both is used in different node types. In this case views filter will show all moderation states from both workflow types instead states of workflow type for current entity bundle.

Sam152’s picture

Rerolling.

Sam152’s picture

Version: 8.4.x-dev » 8.5.x-dev

I think this only gets into 8.4.x is the follow-up view gets in, so 8.5 is more correct?

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 127: 2862041-127.patch, failed testing. View results

Sam152’s picture

Sam152’s picture

Status: Needs work » Needs review
FileSize
4.65 KB
35.23 KB
  1. +++ b/core/modules/content_moderation/src/ViewsData.php
    @@ -79,7 +79,7 @@ public function getViewsData() {
    -        'filter' => ['id' => 'moderation_state_filter'],
    +        'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
    
    @@ -99,7 +99,7 @@ public function getViewsData() {
    -        'filter' => ['id' => 'moderation_state_filter'],
    +        'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
    

    This was assumed for views with a relationship, but now that we've removed the relationship seems like we have to put it back manually.

  2. +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter.yml
    @@ -254,16 +214,7 @@ display:
    -      relationships:
    -        latest_revision__node:
    -          id: latest_revision__node
    -          table: content_revision_tracker
    -          field: latest_revision__node
    -          relationship: none
    -          group_type: group
    -          admin_label: 'Content latest revision'
    -          required: true
    -          plugin_id: standard
    
    +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
    @@ -117,16 +117,6 @@ public function testStateFilterViewsRelationship() {
    -    // The latest revision for both nodes is a draft status.
    -    $this->assertNodesWithFilters([$node, $second_node], [
    -      'latest_revision_state' => 'draft',
    -    ]);
    -
    -    // Only node three has a published latest revision.
    -    $this->assertNodesWithFilters([$third_node], [
    -      'latest_revision_state' => 'published',
    -    ]);
    

    Since #2865579: Rewrite the 'Latest revision' views filter and remove the revision_tracker table we no longer have the latest_revision relationship, so no need to test it here.

hamrant’s picture

According to #126 comment I have updated last patch. In this update was provided ability to select workflow types in ExtraOptionsForm and value options now related to this option, if nothing is selected it shows all the options.

hamrant’s picture

Minor fix

The last submitted patch, 132: 2862041-132.patch, failed testing. View results

amateescu’s picture

Title: Provide useful Views filters for Content Moderation State fields. » Provide useful Views filters for Content Moderation State fields
Status: Needs review » Reviewed & tested by the community
FileSize
35.23 KB

I fully agree with #124 and the fixes from #131 makes sense, so back to RTBC!

@hamrant, I discussed #126 with @Sam152 and we both think that this extra feature is outside the 80% use case of this filter and that it should be discussed/added in a followup. We need to keep in mind that time is very important here because we still very much want this issue and the one that it blocks to be committed before 8.4.0-beta2.

Re-uploading the patch from #131 to be clear which one we think it's ready to go.

xjm’s picture

Assigned: Unassigned » xjm

Thanks all! Assigning to myself for review both of the work on the issue here since and to test the kinds of edgecase scenarios I'm concerned about.

hamrant’s picture

@amateescu, ok, thanks for checking

timmillwood’s picture

@xjm - How are the edge case tests going? Is there anything we can do to help drive this forward?

xjm’s picture

Okay, so one thing that came up in the course of my testing.

I was originally expecting that the state filter would be supplied by Workflows rather than by Content Moderation (since Workflows supplies the concepts of workflows, states, and transitions). Is there a reason that the plugin is supplied by Content Moderation instead? I didn't see anything about this when cmd-f "workflows" on the issue, but I haven't read all 130 comments.

amateescu’s picture

That's because the filter is using a join on the ContentModerationState entity type, which is an internal implementation of Content Moderation. That's also why the filter is called content_moderation_state and not workflow_state.

Sam152’s picture

I can echo #140. Workflows has no knowledge of the thing-being-workflowed, implementations may choose to store states and use transitions in any fashion they like, the job of workflows is to allow the creation and configuration of the constraints, not to dictate how a state is stored or used. One interesting use-case I've seen is someone using it to define the submission workflow of a multi-step form, the state "storage" in that instance is the associated $form_state.

That's why workflows has no knowledge or association with entities at all. content_moderation introduces this association.

xjm’s picture

the job of workflows is to allow the creation and configuration of the constraints

But a Views filter is exactly a constraint?

Edit: I should clarify that the bit about not having any sort of table to join on with Workflows by itself is a reason, although it's a developer's reason rather than a sitebuilder's reason. However, that didn't necessarily need to be the case. :)

xjm’s picture

One weirdness happens when there are no relevant moderation states for whatever entity type is the base table of the view. For example:

Scenario A

  1. Enable Content Moderation. Don't apply the Editorial workflow to any content types.
  2. Edit the Frontpage view at /admin/structure/views/view/frontpage.
  3. Clicl the "Add" button next to "Filter criteria".
  4. Type "state" in the search field. Check "Moderation state" for "Content" and click "Apply (all displays)".
  5. You will see this form:

    If I didn't know the underlying reason for this, I would be pretty confused right about now, because there are no options to configure for the filter. Well, that's as a Views maintainer. If I didn't know Views that well, I might not even notice. Since "Select all" isn't something I want, I might just click "Apply (all displays)" again. Since it's a checkbox rather than a radio button, there's no validation to fail, and Views happily adds the filter.
  6. Then I get this:

    Unknown!

    So, okay, this part is a Views bug. Views shouldn't let you submit the form with no options checked when it's a filter and operand that could allow multiple values. However, Views does at least prevent me from saving the View with this handy message:

The above can be a bit confusing, since it's essentially impossible for me to configure the filter correctly until I apply a workflow to some bundle of the entity type of the view's base table (nodes in this case). (And we wouldn't want to have the Views UI to be confusing. Edit: This is a joke with the punchline being that the Views UI is confusing.) However, the risk is minimal because Views does eventually prevent you from saving the View that way.

Scenario B

  1. Install Standard and enable Content Moderation. Don't apply the Editorial workflow to any content type yet.
  2. Create an article node 1. (This article will have no moderation state.)
  3. Edit the "Archived" state to be a published state. (You need to do this before step 5, or your Archived article will not show up in your view because it won't have gotten published by the transition, and you will spend a bunch of time thinking there are hardcoded assumptions about the default "Published" state and getting confused when you can't find that in the patch.)
  4. Apply the Editorial workflow to articles.
  5. Create an article node 2. Publish it. Save it again and this time set it to Archived.
  6. Create an article node 3. Leave it as a draft.
  7. Create an article node 4. Publish it.
  8. Visit the frontpage at /node. You should see nodes 1, 2, and 3.
  9. Edit the frontpage view.
  10. Add the moderation state filter again. This time, check "Expose this filter". Check the boxes next to Draft, Published, and Archived (but don't check "Limit list to selected items").
  11. Save the filter and the view.
  12. Visit the frontpage. This is the first time something unexpected happens: the frontpage is empty.

    It looks like the filter is defaulting to "Draft" rather than the more logical "- Any -". Okay, that's also a familiar Views configuration problem. If I instead select "- Any -" and click "Apply", my three published articles (which has no moderation state) re-emerge.

    If I go back and edit the views configuration to uncheck the selected options, then it still displays all of them but switches to providing sensible defaults. A little-known "feature" here is that if you check some options but do not check the "Limit list" checkbox, the first one as displayed in the Views UI will be the default value.

    Note that "Draft" will never show anything here, because the frontpage view, as per the best practice, has the published content filter. (This is kind of a sitebuilder UX gotcha; to truly use this exposed filter, I'd need to remove the "published content" filter. And that in turn is a security gotcha, because if I did that my view would start showing unpublished drafts to the world, so I'd have to replace it with a different access restriction on the view itself, presumably the "view unpublished content" permission. But these are both things that happen with views filters all the time.)

  13. Now for the actually buggy part: Edit the Editorial workflow and add a state called "Future" that is an unpublished state. Give it transitions from every other state.
  14. Visit the /node frontpage again. Expected result: the "Future" state shows up in my selector. Actual result: No "Future". Edit:
    Oh. I think this might be a cache invalidation issue, because it goes away as soon as I create a node.

Aside: I accidentally deleted my Frontpage view about 10 times already while testing this. That's #2773205: Come up with a design for highly destructive operations in confirm forms.

Another aside: Adding a state named "9000" is enough to result in permanent fatal errors. I think I saw an issue for that as well, for any config entity, but did not find it yet because it's late.

Going to bed now, more tomorrow.

Edit: just labeling the two separate scenarios so that I can refer to them in further review.

Sam152’s picture

#2886567: Adding a workflow state or transition with an integer ID results in unrecoverable fatal errors is the ID issue.

Thanks for the really detailed analysis. I think you're correct in saying we might be missing some cache tags to invalidate the filter when the associated workflows are updated.

Sam152’s picture

Assigned: xjm » Sam152

Workin' on it.

Sam152’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
7.83 KB
41.49 KB

Initial crack at this. We need to be able to tag things with all workflows of a certain type. Adding a new workflow to an existing entity type with a views filter needs to invalidate it, so it's not enough to add the existing workflows providing states to the filter as dependencies.

As far as why we need to override ::invalidateTagsOnSave, I'm not sure. It seems in the context of config entities, the ::getCacheTagsToInvalidate method is intentionally ignored. @see \Drupal\block\Entity\Block::postSave or \Drupal\shortcut\Entity\Shortcut::postSave for an example and \Drupal\Core\Config\Entity\ConfigEntityBase::invalidateTagsOnSave for where the behavior is ignored.

There was some funkyness where adding a new state to an existing workflow in the test didn't update the filter, but I couldn't reproduce in a real-world environment.

Sam152’s picture

Removing some left over junk.

Status: Needs review » Needs work

The last submitted patch, 147: 2862041-147.patch, failed testing. View results

timmillwood’s picture

Wim Leers’s picture

I attempted to review #146 + #147 + #149, but I kept wondering what it was trying to achieve. It seems the answer can be found in #144:

I think you're correct in saying we might be missing some cache tags to invalidate the filter when the associated workflows are updated.

Okay, so now I roughly know what it's trying to do, I can review it:

  1. +++ b/core/modules/workflows/src/Entity/Workflow.php
    @@ -174,4 +175,24 @@ public function onDependencyRemoval(array $dependencies) {
    +      'workflow_type:' . $this->type,
    

    Where did this cache tag come from? Is this a new one you're generating here? Because workflow types are plugins, not entities. They don't have per-workflow-type cache tags. There only is the generic workflow_type_plugins cache tag, which covers the list of workflow type plugins that are installed.

    It's strange for the Workflow entity type to be defining a cache tag that seems to be associated with @WorkflowType plugins. If we have a cache tag like this, I think something like 'workflows_of_type:' . $this->type would make more sense? (This reminds me of #2145751: Introduce ENTITY_TYPE_list:BUNDLE cache tag and add it to single bundle listing btw — it's similar, but not the same.)

    I'm missing documentation for this.

  2. +++ b/core/modules/workflows/src/Entity/Workflow.php
    @@ -174,4 +175,24 @@ public function onDependencyRemoval(array $dependencies) {
    +    // The config system handles invalidating the typical return value of
    +    // ::getCacheTagsToInvalidate, but since we add an extra tag, we have to
    +    // call this manually.
    +    Cache::invalidateTags($this->getCacheTagsToInvalidate());
    

    This comment is inaccurate if you read \Drupal\Core\Config\Entity\ConfigEntityBase::invalidateTagsOnSave().

    You mentioned your reasoning in #146, but note that the docblock of \Drupal\Core\Config\Entity\ConfigEntityBase::invalidateTagsOnSave() explicitly explains why it does what it does.

  3. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -72,6 +73,14 @@ public function getValueOptions() {
    +    $form['#cache']['tags'][] = 'workflow_type:content_moderation';
    

    So this is really getting at the heart of it (I think).

    I think you're trying to make sure that the options presented in the form are always up-to-date, to ensure they reflect any changes in Workflow entities that use the content_moderation @WorkflowType plugin.

    But then why introduce a new cache tag? Why not just reuse the Workflow entity type's list cache tag? You can get that by calling \Drupal\Core\Entity\EntityType::getListCacheTags().

    Yes, that would mean that even changes to Workflow entities that use another @WorkflowType plugin than content_moderation would cause the rendered exposed filter to be invalidated. But changing workflows is a sufficiently rare operation that that should be okay. And you could still optimize it later if the need arises.

    Finally: I'd think that you'd not override getValueForm(), but instead override getCacheTags(). Then it'll also end up in here:

    +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter.yml
    @@ -226,3 +226,20 @@ display:
    +    cache_metadata:
    +      max-age: -1
    +      contexts:
    +        - 'languages:language_content'
    +        - 'languages:language_interface'
    +        - url
    +        - 'user.node_grants:view'
    +        - user.permissions
    +      tags: {  }
    

So my recommendations:

  1. use the existing Workflow entity type list cache tags, don't introduce a new one
  2. override ModerationStateFilter::getCacheTags(), not ModerationStateFilter::getValueForm()
  3. better docs
Sam152’s picture

Status: Needs review » Needs work

All three suggestions sound fantastic, thanks @Wim. I assumed it'd be a deal-breaker to invalidate more often than absolutely necessary, but your reasoning makes perfect sense.

Sam152’s picture

Status: Needs work » Needs review
FileSize
4.78 KB
38.24 KB
38.47 KB

Interdiff is from #135 because of the new approach. I've also uploaded a test only patch just for the bug reported in #143 and the test associated with that bug.

Wim Leers’s picture

Glad you liked my suggestions :)

I like how #152's interdiff looks!

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -48,6 +49,13 @@ public static function create(ContainerInterface $container, array $configuratio
    +  public function getCacheTags() {
    +    return Cache::mergeTags(parent::getCacheTags(), $this->entityTypeManager->getDefinition('workflow')->getListCacheTags());
    +  }
    

    👏

    Note: although only the Node entity type currently uses it, it's safer to also do the same for getCacheContexts(): EntityType::getListCacheContexts() is also a thing.

    Note that getListCacheContexts() will just return [] for the Workflow entity type, but it's the semantically correct thing to do.

  2. +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter.yml
    @@ -226,3 +226,20 @@ display:
    +      tags: {  }
    

    Hm, did you re-export this? I expected this to list the Workflow entity type's list cache tag.

  3. +++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,83 @@
    +    $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example_a');
    +    $workflow->save();
    

    Nit: you can chain these.

  4. +++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php
    @@ -0,0 +1,83 @@
    +    $new_workflow = Workflow::create([
    +      'type' => 'content_moderation',
    +      'id' => 'new_workflow',
    +    ]);
    +    $new_workflow->getTypePlugin()->addState('bar', 'Bar');
    +    $new_workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example_b');
    +    $new_workflow->save();
    

    And these.

The last submitted patch, 152: 2862041-152_TEST_ONLY.patch, failed testing. View results

Sam152’s picture

Thanks for the review.

1. Beauty, didn't know entities could do this.
2. Good catch, re-exported.
3. The return value of addEntityTypeAndBundle is the @WorkflowType not the entity.
4. Same here, but removed these anyway.

This patch is also a minor test refactor to address some missing coverage I mentioned in #146. Previously I was manipulating the workflow entity in the test thread and visiting the pages in a sub-request. For some reason, the sub-request was loading the old non-updated entity, meaning adding a state wouldn't propagate onto the page. I've had a debugger in there for a few hours now, right into cache-backends, but couldn't figure this out. The rules around manipulating stuff in the test thread during functional testing and the sub-request have always been a bit janky for my liking, so I've simply performed the actions from the UI. I think this is far more widespread practice anyway, so hoping this is alright.

Another test only patch highlighting only the issue picked up in #143. I think this was the only bug with the filter specifically. A lot of issues I suspect would exist when using the InOperator with a list of taxonomy terms for example.

Hope this addresses all of the critical concerns with the patch so we can get #2902187: Provide a way for users to moderate content done and into 8.4.x. 🤞

Wim Leers’s picture

Issue tags: +D8 cacheability
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_state_filter.yml
@@ -225,7 +225,8 @@ display:
-      tags: {  }
+      tags:
+        - 'config:workflow_list'

@@ -242,4 +243,6 @@ display:
-      tags: {  }
+      tags:
+        - 'config:workflow_list'

It's good to see things work as expected :)

I'm hereby RTBC'ing the cacheability aspects of this patch. RTBC'ing the entire patch should be done by somebody else.

Status: Needs review » Needs work

The last submitted patch, 155: 2862041-155.patch, failed testing. View results

Sam152’s picture

Status: Needs work » Needs review
FileSize
794 bytes
39.26 KB

The last submitted patch, 155: 2862041-155_TEST_ONLY.patch, failed testing. View results

Wim Leers’s picture

Pesky semi-colons! 😭 😀

timmillwood’s picture

When a workflow is not set to any entity types the filter just shows "select all" as shown in the screenshot of #143.5, what can we do to address this?

xjm’s picture

Thanks @Sam152, testing the latest patch now.

xjm’s picture

@timmillwood, a simple thing I was thinking we could do would be to add help text on that form explaining where the values come from. E.g.: "The Editorial and Marketing workflows apply to content, which has the following states available." or "No workflows are currently available for this content. They can be configured on the workflows administration page." etc.

xjm’s picture

Posted #2906892: Only one workflow can be applied to a given bundle, but there is no indication in the UI (this came up because multiple workflows was one of the edgecases I was going to test; thankfully it appears that's not something we have to worry about).

Sam152’s picture

Assigned: Sam152 » Unassigned

Awesome, thank you for retesting!

Sam152’s picture

Assigned: Unassigned » xjm

I think this is waiting on xjm for sign-off and RTBC.

xjm’s picture

  1. I think we at least need a followup for #143 scenario "A" (edit: the issue with there not being valid options for the field). It might be okay to descope it to a followup because Views does prevent the invalid Views configuration from being saved and it's not worse than a lot of other places in the Views UI where we present dynamic values as options.
     
  2. A summary of the general problem space I'm testing (which I should have given two weeks ago, sorry for that.) The situation currently is that what's presented as allowed values (for the filter, in an exposed filter, etc.) changes depending on changes to the workflow configuration. You may not be able to delete a state when it has content in it, but you can add or remove which bundles the workflow applies to, etc., and stale/invalid references will sit around in the View until someone edits the filter, at which point they magically disappear.

    I was concerned that we might run into fatal data integrity issues for the view with scenarios where we change workflow configuration or remove states. However, I tested lots of permutations of this (more than just what's documented in #143; I fell asleep before writing them all out) and in all cases it was silently updated to the new state list.
     

  3. One thing that wasn't exactly right was that the state names lingered in config exports. I think that's something that happens elsewhere in Views, though. The closest analogue I can think of is entity references e.g. taxonomy terms, and I tested and confirmed we already have the same problem there. (Filed #2907378: Cached view is not invalidated and dependencies not updated when removing a target entity for an ER filter for that.) Edit: But a more general followup for cleaning up views handler allowed values settings might be in order.

     

  4. A difference is that taxonomy terms are stored as entities and @timmillwood explained that states are just stored on the given workflow. Tim suggested a filter based on allowed select field values instead.

    I confirmed that If you configure a Views filter on an allowed value for a list field, and then remove the allowed value for the list field, the View is not cleaned up. So, that's still an existing problem in Views and can be out of scope here, but we should reference related/followup issues.

    However, that scenario isn't entirely parallel: an allowed value can't be removed from a field when the field has that value, and removing the field entirely does disable the view (no longer deleting it since #2468045: When deleting a content type field, users do not realize the related View also is deleted). While you cannot delete a state when it is in use, you can disconnect the workflow from any content type that has it without changing the allowed states.
     

  5. The above point made me wonder if we should prevent removing workflows from a bundle when a state from that workflow is in use in the same way that deleting the state is disallowed. That would be out of scope for this issue, but it would set expectations for this issue and reduce the need for creative ideas on how to break it.
     

  6. Above, @amateescu says we shouldn't say anything in the inline comment about "mitigations". If this is a design behavior, I'd agree. And if it's a design behavior, we do need tests for it.

Some more scenarios I haven't tested yet:

  • "Not in" filters
  • What might happen when there are expectations that the filter will behave as an access restriction. (This is important; see https://www.drupal.org/PSA-2017-002.)
  • Actual deployments with content in workflows and views.

Edit: Fixing goofy list whitespace.

xjm’s picture

Hm, also debating whether we need #2859702: Add a Views filter for access to transition to a given moderation state first to avoid access bypass scenarios. (Thinking of the PSA again here.)

amateescu’s picture

Above, @amateescu says we shouldn't say anything in the inline comment about "mitigations". If this is a design behavior, I'd agree. And if it's a design behavior, we do need tests for it.

My comment you are referring to (#119) has this explanation:

Workflow states can not be deleted while there is existing content using them. When that content is gone and the state can be deleted, a views filter that excludes entities with that workflow state will have nothing left to exclude because there are no entities with the given state anymore.

So I will assume that by 'design behavior', you are referring to that paragraph. A test scenario for this would look like:

- create an "editorial" content moderation workflow with two states, published, draft and archived; assign this workflow to the "article" node type
- create one article node with the "published" state
- create a different article node with the "draft" state
- create a different article node with the "archived" state
- create a view of node revisions, add the moderation_state filter and configure it to display display only nodes with the "published" and "draft" states
- check that the view shows all the three nodes above
- try to delete the 'archived' state -> you can't because there is a node using it (this is already tested in \Drupal\Tests\content_moderation\Functional\ModerationFormTest::testWorkflowInUse())
- delete the third node and then delete the 'archived' state
- check that the view does not show the third node anymore

The last step is what this whole discussion is about and I might be missing something, but isn't it obvious that the third node will not be displayed in the view because you just deleted it?


While you cannot delete a state when it is in use, you can disconnect the workflow from any content type that has it without changing the allowed states.

I think this is an entirely different topic. The filter we're adding here is intentionally not tied to the workflows that provide the allowed values (states). Btw, allowing the filter to be tied to specific workflows was actually discussed and decided against, see the second paragraph from #135.

If the user wants to filter the view by some specific bundles in addition to moderation states, there is a dedicated filter for that.


What might happen when there are expectations that the filter will behave as an access restriction. (This is important; see https://www.drupal.org/PSA-2017-002.)

The PSA was about something else: views that do not have a views access plugin configured.

But if we go along with the line of thought, the access restriction expectation from this filter would be "only display entities with the given moderation states", because that's the only thing it can do.. and it does that just fine :)

xjm’s picture

isn't it obvious that the third node will not be displayed in the view because you just deleted it?

lol yes, but we should have coverage to ensure that nothing breaks when you remove the state, and for what we expect to happen to the view (that the view is not updated).

The PSA was about something else: views that do not have a views access plugin configured.

I helped write the PSA BTW so I know what is in it. We don't have any such for Content Moderation views to my knowledge. Again, ref. #2859702: Add a Views filter for access to transition to a given moderation state.

xjm’s picture

Meant to say that I think #119 should further update the inline comment that's already there, but also

Workflow states can not be deleted while there is existing content using them

But the states' workflows can be removed from the bundles that include the entities, even though that's what supplies the allowed values in the View config, and the allowed values changes when the workflow is enabled or disabled for the bundle, which can happen even while content is in their states. That is what needs additional test coverage.

xjm’s picture

Also, some of the access bypass concerns I want to look into are out of scope here (e.g. changing which states are published and the mismatch of expectations between "content in this state is published" vs. the actual "content is published when it gets to this state") but might be blocking. But it was my next action item for me, not actionable yet for anyone else.

xjm’s picture

The filter we're adding here is intentionally not tied to the workflows that provide the allowed values (states)

But it is tied to it because the allowed values presented in the Views UI depend which workflows are attached to the entity type for the base table, which you can change. When you change which workflows are attached to which entity types, the allowed values of states presented in the views UI change, but the view has not been updated.

xjm’s picture

Steps to reproduce:

  1. Add Editorial to articles.
  2. Edit the frontpage view. Add a moderation state filter. You'll see Draft, Published, Archive as allowed values. Select Archive and save. (Let's say archive is a published state.).
  3. Add a new workflow called Albatross with "Bird" and "Mariner" states. Attach it to pages.
  4. Edit the frontpage view. Edit the moderation state filter. You'll see Draft, Published, Archive, Bird, Mariner as allowed values.
  5. Remove the Editorial workflow from articles. Back to the view. Your only options are Bird and Mariner. Until you actually save this filter, though, your view still has Archive stored in the config. When you save it, though, it's impossible to save Archive to the config anymore.
xjm’s picture

Status: Needs review » Needs work

Okay, there actually is a data integrity issue. If you do #175, it's impossible to ever save your view again without removing and fixing the filter. You'll get the validation error from my scenario A earlier.

So we do need the View to react somehow when a workflow is detached from an entity type entirely (if it's the last bundle), or "heal" the view somehow, or disallow removing it the workflow, or change the validation, or something. (Edit: added possibilities).

Sam152’s picture

Re: cleaning up the view when workflows change, I wrote a bunch of test cases kind of in this realm in #2901690: Implement ModerationStateFilter::calculateDependencies.. Turns out it was really complicated with a bunch of edge-cases right below the surface. States being common across workflows made this tricky.

When you save it, though, it's impossible to save Archive to the config anymore.

That is getting way too far into edge-cases IMO. If you've removed the archived state from content, this seems like reasonable or even expected behaviour. I'm pretty sure even though it exists in the config, it doesn't show up in the list, so resaving the filter doesn't actually change anything but the stored key.

I'm going to try reproduce and fix the bug of the view not being resavable when a filter is removed, that doesn't seem right. Is anything else in #168 in scope for this issue? I agree that we should look into stopping users from removing moderation from a bundle-in-moderation-with-data.

amateescu’s picture

amateescu’s picture

And here's another issue for the access bypass problem hinted at in #173.

Sam152’s picture

What was the validation error in #176? I couldn't reproduce this.

timmillwood’s picture

Status: Needs work » Needs review

I've been testing #175 and #176. I have been able to replicate the issue, and have also been able to replicate this with a select field, as stated in #168.4. So this is not an issue specific to Workflows.

In #176 this issue was marked "Needs work" because it was not possible to save the view after removing a workflow from an entity type. As the same issue of not being able to save a view also happens when removing an allow value from a list field, I think we can push this back to "needs review" and open a new issue to fix views InOperator filters in general.

timmillwood’s picture

xjm’s picture

Thanks @timmillwood, I thought I had tested with the select list version and not encountered the error but I apparently hadn't actually tried to save the view yet (which is also I did not notice it at first with this until #176).

The reason I think this is different the select list case is that with a field's allowed values, you configure the list once in a single screen (and while you can edit it, there's a message on the screen claiming you can't so most people may not even try). If you up and deleted the field that the filter depended on, the view would get disabled.

Re:

When you save it, though, it's impossible to save Archive to the config anymore.

That is getting way too far into edge-cases IMO. If you've removed the archived state from content, this seems like reasonable or even expected behaviour.

Sorry, I wasn't saying you should be able to save it when it did not exist; I was saying that it meant the simple act of editing and re-saving the filter would result in what was stored in configuration changing, even though the user didn't change anything. This results in noise in configuration exports/deployments, a mismatch between the user's actions and the results, etc. I.e. data integrity issues.

I've seen it stated several times that states can be "shared across workflows" and that for this reason we don't want to depend on the workflow, and I've read all the past discussion, but I am still not convinced that's the right choice. There are just so many edgecases that we have to handle that the configuration system would handle for us if we declared a dependency. (Comparing it with the select list case, that's the equivalent of saying that we don't want to add a dependency on the field because the possible values of the field could be found in other select list fields' allowed values.) And really, they're not actually even the same state when they are "shared"; I've just created a state on one workflow called foo that's published and becomes the default revision, and a state called foo on another workflow that is neither of those things.

xjm’s picture

So my proposal would be that the filter be for State in Workflow X. Then, if Workflow X is changed, the view can react.

tacituseu’s picture

@xjm: The shared states idea was raised in 8.3.x rewrite issue (#2779647-27: Add a workflow component, ui module, and implement it in content moderation) and it was decided against it.
It was sort of the status quo before that, when it was closer to Workbench and there wasn't a support for multiple workflows but you kinda sorta could checkbox out a subset of the states per bundle.
In current implementation there can be multiple workflows that have the same ID/label (key in .yml) for their states, but that doesn't make them common in any meaningful way as they are independent (their settings/properties aren't shared).
If you need to mix and match them in some filter settings the sensible thing would be to prefix each state with the workflow name it belongs to, but then you would have to filter out duplicates if you want to shove them into one select list.

amateescu’s picture

So #183 and #185 finally made me realize that I was so wrong with my "states can be shared across workflows" argument. Sorry, @xjm!

As proposed in #184, we need to differentiate between states across workflows. Also, at this point I think it's actually useful to include the changes from #132.

Here's a starting point for the above, tests will need to be fixed and the logic for updating the view when something changes in a workflow needs to be written, but this should get the ball rolling in the right direction :)

The filter configuration screen looks like this now:

Status: Needs review » Needs work

The last submitted patch, 186: 2862041-186.patch, failed testing. View results

Sam152’s picture

While it's true that the same state ID could possibly have a different meaning across workflows and thus should be filtered different, that doesn't seem like the case for draft and published, which are on all CM workflows and contain settings that cannot be changed.

In any case, the actual likelihood you'd have two different workflows attached to two different bundles of the same entity type and want a view that gracefully created one select list for two workflows, sharing common states etc, seems pretty outside of the 80% as well, so I'm totally fine with the direction in #186.

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -63,23 +69,55 @@ public function getCacheContexts() {
+    $form['limit_workflows'] = [
+      '#title' => $this->t('Limit the allowed workflows to the following list'),
+      '#type' => 'checkboxes',
+      '#options' => $types,
+      '#default_value' => $this->options['limit_workflows'],
+    ];
...
+        foreach ($workflow_type->getStates() as $state_id => $state) {
+          $this->valueOptions[$workflow->label()][implode('-', [$workflow->id(), $state_id])] = $state->label();
+        }

I don't think we really need this. Surely if the user wanted to limit the workflow/states, they could simply select the relevant values.

I'm just thinking off the top of my head here, but is there any way we could create a new views field for each workflow. That way in the fields list, you'd select Moderation state (editorial) or Moderation state (foobar). It would add complexity in views data, but probably drastically simplify the implementation of the filter and I think it's possibly a clearer in terms of UX? Ie, a user could never start mixing states across different workflows in the same field, which I think given the paradigm of shared labels/IDs, but different semantics/meanings would be a win. Haven't tried this, but just a thought.

Sam152’s picture

Here is an interdiff on top of #158 prototyping what I proposed in #188. I'm quite pleased how simple this ended up being, and it could probably be cleaned up a bit further as well.

If we're happy with this approach, happy to fix up and add additional testing as well as implement calculateDependencies and onDependencyRemoval. Both of these method will be trivial if we're dealing with one workflow.

Status: Needs review » Needs work

The last submitted patch, 189: 2862041-189.patch, failed testing. View results

amateescu’s picture

While my patch from #186 was surely fun to write and discover hidden gems in Views, I agree that the approach in #189 is way simpler.

I don't have a preference either way, tbh, so let's ask a product manager if they think that having a single filter able to query across multiple workflows is useful or not :)

Ping @webchick?

Sam152’s picture

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -126,13 +164,57 @@ public function ensureMyTable() {
+      $and = new Condition('AND');
+      $and
+        ->condition("$this->tableAlias.workflow", $workflow_id, '=')
+        ->condition("$this->tableAlias.$this->realField", $state_id, $operator);

With the approach in #186 we were treating states across workflows as individual items in the same list right? So this change just shifts the way the user enables additional workflows and splits those workflows into multiple lists. Sounds like the two options are pretty close in terms of outcomes, just a difference in terms of site building and front-end. So my guess would be this is acceptable from the product and technical angle, but would love to hear from @webchick and @xjm.

Sam152’s picture

Sam152’s picture

Status: Needs work » Needs review
FileSize
20.84 KB
39.23 KB

Fixing tests, some clean up and implementing calculateDependencies.

Looking at #2468045: When deleting a content type field, users do not realize the related View also is deleted, there is a DependentWithRemovalPluginInterface, but I'm not sure if that's only for display plugins, not the filter ones. In any case, for the view to gracefully clean-up when the config is updated/deleted, we can hopefully:

  • Implement the interface, ensure the filter is removed when the config is deleted.
  • When the workflow is updated and an entity type is removed, do something tricky and possibly call $view->onDependencyRemoval() to indicate the dependency it has is not actually a dependency anymore (hence removing it from the view).

Still need feedback on the general approach, but if anyone has time to look at the above, my time is a bit stretched this week.

Sam152’s picture

@xjm FWIW, the behaviour is the standard views "broken/missing handler" when the entity type is removed from the workflow. I'm not sure if this is better or worse than the stale state list, but it leaves the config in-tact. It even tells the user what it was and gives them a chance to rectify the situation.

Given I don't think there is much else out there which tries to manually clean-up views config on entity_update (not a dependency being removed), perhaps we can scope that for a follow-up and discuss it there, given the current behaviour seems quite acceptable. I half suspect we'd arrive at the conclusion that it isn't worth doing.

xjm’s picture

Blah, ugh, gah, I totally misremembered what was in the scope of #2212081: Remove views optional handler handling. The view is still merrily returning results with the broken handler... it should not be. That's (another) major views bug there.

In any case, for the view to gracefully clean-up when the config is updated/deleted

We don't want to do this; in fact, we want to do not-this. Views filters can be used to restrict the data that is shown to the user. There's a reason we disable the view in #2468045: When deleting a content type field, users do not realize the related View also is deleted rather than trying to remove the configuration from the view: doing otherwise could result in access bypasses/information disclosure/etc. because we don't know how the user has used the field in their view.

I'll have a think and chat with some folks about it.

Sam152’s picture

Great, thanks for that additional context.

xjm’s picture

So we need to add a test case that does something like this:

  1. Apply editorial to a content type.
  2. Set up the view to filter to "Archived" state.
  3. Create nodes in the "Published" state.
  4. Verify that the view has no results.
  5. Remove the workflow from the content type.
  6. Verify that the view returns no results.
  7. Ensure that the view can be edited and saved.

This will currently fail in step 6 for @Sam152's version, because views is just ignoring the broken handler rather than disabling the output of the view. I thought that we'd made views with broken handlers return no results back before release. That feels like a views bug to me that would be a separate issue but changing that now would be extremely disruptive (sites that had views with broken handlers would update and then have site content disappear from views that were previously working).

The test will pass step 6 but fail in step 7 for @amateescu's patch, with:
11-Sep-2017 12:24:57 Europe/Berlin] TypeError: Argument 1 passed to Drupal\Core\Form\OptGroup::flattenOptions() must be of the type array, null given, called in /Applications/MAMP/htdocs/d8git/core/modules/views/src/Plugin/views/filter/InOperator.php on line 330 in /Applications/MAMP/htdocs/d8git/core/lib/Drupal/Core/Form/OptGroup.php on line 23

The earlier version of the patch will also pass step 6 and fail step 7.

So let's at least add that test scenario. We'll need to update its default views depending on the approach but I do want to make sure we have coverage for it. (It's basically what I was asking for test coverage for earlier on the issue.) We can hold off on making any one approach work until we get more feedback. I'll also look into it more.

xjm’s picture

Posted #2907954: Views with broken handlers still return results even if the results might be incorrect to discuss the broken handler thing. Whatever we do here we should make sure that that doesn't happen.

I wonder if our config API should supply an onDependencyChange() or something. We can fix this issue in whatever way works without getting into that since it's also out of scope, but it could be a followup where we refactor whatever hotfix we use here to handle it with an added API.

Sam152’s picture

Totally missed what you were after in terms of testing previously, my bad! Here is the test case you've described in #198.

Not sure what a good solution is yet.

Sam152’s picture

Here is a solution.

Status: Needs review » Needs work

The last submitted patch, 201: 2862041-201.patch, failed testing. View results

Sam152’s picture

Status: Needs work » Needs review
FileSize
46.49 KB

Patch for #201 but for real this time.

The last submitted patch, 200: 2862041-200.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

jofitz’s picture

Coding standards correction.

webchick’s picture

Ok, I got actually pinged today in #ux on Slack :D, so here's a summary of that conversation.

The use case: a site has two Workflows: Editorial and Translation. Both have a state called "Needs review." But that state means different things depending on context. Let's say for Editorial, it goes to Mary for review. For Translation, it goes to Maria.

We were asked to decide between the approach in #186 (optgroups):

optgroups

...and #189 (a filter per workflow / state pairing):

filter per

My initial impressions:

  1. Optgroups seem like semantically the correct element here to use.
  2. It's also way easier to read and scan.
  3. Because the moderation view we aim to ship with Drupal core is going to display a) only state labels, and b) for all content, regardless of workflow, sites that have this edge case are going to hit it pretty fast with core out of the box. And they're far more likely to fix it by just relabeling the states ("Needs review" vs. "Needs translator review") rather than farting around in Views. In other words, I really don't think this is something we need to bend over backwards to solve. There are much clearer and simpler workarounds.

Additionally, @CatherineOmega chimed in that for the non-technical users in her organization, optgroups would make more sense to them as well, so that's a second data point. (Adding her to the creditors list here, since she helped with the brainstorming.)

So unless @yoroy or @Bojhan or someone with more UX chops than us chimes in differently, that seems like the way to go. Hope that helps.

tim.plunkett’s picture

Architecturally both approaches seem reasonable, and have different downsides.
I think deferring to UX is a good choice.

Review of #186:

  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,220 @@
    +    $this->entityTypeManager = $entity_type_manager;
    ...
    +      $container->get('entity_type.manager')
    ...
    +    return Cache::mergeTags(parent::getCacheTags(), $this->entityTypeManager->getDefinition('workflow')->getListCacheTags());
    ...
    +    return Cache::mergeContexts(parent::getCacheContexts(), $this->entityTypeManager->getDefinition('workflow')->getListCacheContexts());
    ...
    +    foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
    ...
    +    $query = $this->entityTypeManager->getStorage('workflow')->getQuery();
    ...
    +      $left_entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
    +      $entity_type = $this->entityTypeManager->getDefinition('content_moderation_state');
    ...
    +      $join = Views::pluginManager('join')->createInstance('standard', $configuration);
    ...
    +      $workflows = Workflow::loadMultiple(array_keys($workflow_ids));
    

    This has a strange hybrid approach to DI.

    Nothing on ETM is being consulted directly, it's still used as a pseudo service locator.

    With the one exception (getStorage($this->getEntityType()), every other call is known in advance. I believe it'd would be preferable to inject the needed classes then.

    This goes to for the static helper methods like ::loadMultiple

  2. +++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
    @@ -228,7 +228,7 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
    -        '#size' => count($options) > 8 ? 8 : count($options),
    +        '#size' => count($options, COUNT_RECURSIVE) > 8 ? 8 : count($options, COUNT_RECURSIVE),
    

    This seems like it deserves dedicated test coverage, and maybe a comment?

amateescu’s picture

Assigned: xjm » amateescu
Status: Needs review » Needs work

Thanks @webchick and @tim.plunkett, I'm going to work on finishing up the approach in #186.

amateescu’s picture

Status: Needs work » Needs review
FileSize
43.69 KB
12.69 KB

First, let's fix the tests and the review from #208.

Edit: the interdiff is to #186.

amateescu’s picture

Assigned: amateescu » Unassigned
FileSize
47.96 KB
6.41 KB

This patch implements onDependencyRemoval() so that the view is disabled and not deleted when the last workflow dependency is deleted, but I can't figure out why the last assertion doesn't work and the view is not actually disabled.

Unassigning because I'm done for today so someone else can pick it up from here.

Sam152’s picture

+++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php
@@ -242,6 +250,83 @@ public function testStateFilterStatesList() {
+    // Delete one of the workflows and check that view is not deleted and that
+    // it can be saved.
+    $translation_workflow->delete();
+    $this->assertWorkflowDependencies([$editorial_workflow], $view);
+    $this->assertTrue($view->storage->status());
+    $view->save();

Couple of issues with this. The first one is, a workflow can be removed from an entity type/bundle without being deleted. So we can't really rely on onDependencyRemoval to control anything that needs to happen to the view based on a workflow being applicable or not.

The second issue is, the test case was around asserting the actual nodes visible in the view before and after moderation is added/removed from a bundle. We need to assert once $translation_workflow is disabled, all nodes that were in that workflow (filtered out by a state) still don't appear in the view if they didn't before. I think that's going to be pretty difficult to detect and handle.

I don't want to distract from continuing on with this, but I can see this style of filter being a lot harder to complete in a way to meets the feedback so far. So much so, I'd prefer to treat the optgroup as a feature request (given it's probably way out of the 80% use case anyway) and go with the implementation that is easiest to address the access bypass issue.

Maybe the above is wrong and there'll be an implementation that works, but if we're persisting metadata from a previously applied workflow in order to detect and resolve an access bypass, we still have the issue of the noisy config diff that was raised. I can't seem to grok a realistic solution using this approach.

Status: Needs review » Needs work

The last submitted patch, 211: 2862041-211.patch, failed testing. View results

xjm’s picture

@webchick, did you catch that the version of the patch with the optgroups also has an extra screen when configuring the view?

webchick’s picture

Nope, #207 lays out exactly what we were asked to choose between. In that case, I'd need to try out the patch/see it demoed in UX meeting/see screenshots of the full experience to see if the extra screen is worth the UX benefit of optgroups or not.

amateescu’s picture

Note that the extra screen can be removed at any point without affecting the functionality of the filter with the optgroups approach at all, I just thought it might be useful :)

I'll post a more detailed reply to #212 and a new patch tonight I hope.

amateescu’s picture

Assigned: Unassigned » amateescu
Status: Needs work » Needs review
FileSize
48.76 KB
11.53 KB

@Sam152, the kernel test that I was trying to write in #211 was strictly meant to check the functionality of calculateDependencies() and onDependencyRemoval(), not the scenario described in #198. I couldn't figure out how to properly update the view storage in a kernel test so I converted it to a browser test instead, which works perfectly.

a workflow can be removed from an entity type/bundle without being deleted. So we can't really rely on onDependencyRemoval to control anything that needs to happen to the view based on a workflow being applicable or not.

One option for that is to finish #2907449: Do not allow removing moderation for an entity bundle that has moderated content, but I think I have an idea for handling it in the filter itself.

amateescu’s picture

Before that, let's remove the extra screen mentioned by @xjm in #214 because it seems contentious and I don't want it to cause more work for anyone :)

webchick’s picture

I don't know if it's contentious or not, cos I haven't seen it. :) But I would say in general that the more you can make these filters match the way all of the other filters in Views work/look, the better. :)

amateescu’s picture

Assigned: amateescu » xjm
FileSize
54.06 KB
10.76 KB

Ok, it took me a while but I got it working. This patch finally adds the scenario from #198 and even goes a bit further than that in order to handle the following case:

  1. Apply editorial to two content types
  2. Set up the view to filter to "Archived" state
  3. Create nodes in the "Published" state and also in the "Archived" state for both content types
  4. Verify that the view only shows the nodes in the "Archived" state from both content types
  5. Remove editorial from one of the content types
  6. Verify that the view only shows the node in the "Archived" state from the content type that's still enabled in the Editorial workflow

I'm pretty sure that no previous patch was passing this scenario.

The implementation works by adding an additional condition on the bundles configured for the workflows that the user can select in the filter. I think it's a reasonable expectation from a filter that lists moderated entities to restrict the results automatically to only those entities whose bundles can actually be moderated (i.e. enabled in a workflow).

I'm done here for now so assigning back to @xjm.

P.S. Keep in mind that if we don't want all this automatic filtering by moderated bundles we can always do #2907449: Do not allow removing moderation for an entity bundle that has moderated content instead.

amateescu’s picture

Removed local debugging leftovers :/

Sam152’s picture

FileSize
107.17 KB

I'm pretty sure that no previous patch was passing this scenario.

That's correct, in a sense this implementation is a little more self-healing instead of the "something doesn't make sense, bail out of all results". Nice job making this work, I couldn't think of a sane way to do it but I think it's entirely reasonable to restrict the set of results to content that is actually moderated when you add a moderation filter.

Found this when manually testing:

This implementation still does suffer from the scenario in #175. That is, you can create the filter and old states will hang around in configuration until the filter is opened and resaved. I believe the position on this was, this is potentially more a views thing than something unique to this patch, but we should cover our expectations around how this works with a test anyway.

jibran’s picture

+++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
@@ -228,7 +228,9 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
-        '#size' => count($options) > 8 ? 8 : count($options),
+        // The value options can be a multidimensional array if the value form
+        // type is a select list, so make sure that they are counted correctly.
+        '#size' => count($options, COUNT_RECURSIVE) > 8 ? 8 : count($options, COUNT_RECURSIVE),

This change makes sense to me. Let's see what @dawehner thinks about it.

phenaproxima’s picture

+++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
@@ -0,0 +1,295 @@
+    $query = $this->workflowStorage->getQuery();
+    $query->condition('type', 'content_moderation', '=');
+    $result = $query->execute();

Nit: You could do $this->workflowStorage->loadByProperties() here, rather than query and load separately.

Sam152’s picture

Nit of the nit: We also have \Drupal\workflows\Entity\Workflow::loadMultipleByType :D

amateescu’s picture

We're not doing \Drupal\workflows\Entity\Workflow::loadMultiple anymore at @tim.plunkett's request, see #208.1. However, we can definitely switch to $this->workflowStorage->loadByProperties() :)

amateescu’s picture

Let's do that :)

dawehner’s picture

+++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
@@ -229,7 +229,9 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
+        '#size' => count($options, COUNT_RECURSIVE) > 8 ? 8 : count($options, COUNT_RECURSIVE),

I had no idea about this flag, nice!

What about using min(count($options, COUNT_RECURSIVE), 8) here? That' s a bit more readable, IMHO.

amateescu’s picture

Sam152’s picture

I opened #2920713: Views InOperator "Select all" option should only be applied when the valueFormType is "checkboxes" for my comment in #222 to not disrupt this issue so much.

From further testing today, I think with the above non-blocking follow-up this is ready for a review by @xjm as @amateescu suggests.

Manuel Garcia’s picture

Status: Needs review » Reviewed & tested by the community

Had a good read on this issue, looks like everything mentioned has been addressed.
Had a read through the patch and it looks good to go to me, so I'm going to take the leap and RTBC this :-)

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 229: 2862041-229.patch, failed testing. View results

Manuel Garcia’s picture

Status: Needs work » Reviewed & tested by the community

Looks like unrelated failure, retesting and setting back to RTBC

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 229: 2862041-229.patch, failed testing. View results

kkus’s picture

In #222 Sam's screenshot shows selecting multiple moderation states. I'm not sure if this should be a separate issue but I thought this is related enough to ask here: should we also have a field that allows us to see the the states of the nodes?

as of patch 229, I get

Configure field: Broken/missing handler

The handler for this item is broken or missing. The following details are available:

Enabling the appropriate module may solve this issue. Otherwise, check to see if there is a module update available.

When I add moderation state to fields.

Thank you

Manuel Garcia’s picture

FileSize
2.77 MB

Thanks @kkus for that, I have just verified that indeed that is the case.

Sam152’s picture

The issue described in #235 is fixed in #2859381: Broken/missing handler for Moderation state field and is only blocked behind some additional testing.

Manuel Garcia’s picture

God I clearly need more coffee!
I was testing the field, and not the filter which is what this issue is providing - apologies!

I have now tested the filter and it is working flawlessly, exposed, with ajax and everything seems to be working great.

kkus’s picture

Sorry, I didn't see #2859381. I was able to get this patch working over the weekend on simplytest.me but as of yesterday I get an error:


The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">Drupal\Core\Database\DatabaseExceptionWrapper</em>: Exception in Content[content]: SQLSTATE[42S22]: Column not found: 1054 Unknown column &#039;node_field_revision.nid&#039; in &#039;on clause&#039;: SELECT COUNT(*) AS expression
FROM 
(SELECT 1 AS expression
FROM 
{node_field_data} node_field_data
INNER JOIN {users_field_data} users_field_data_node_field_data ON node_field_data.uid = users_field_data_node_field_data.uid
INNER JOIN {node_field_data} node_field_data_1 ON node_field_revision.nid = node_field_data_1.nid AND node_field_data_1.langcode = node_field_revision.langcode
INNER JOIN {node_field_revision} node_field_revision ON node_field_data.vid = node_field_revision.vid
LEFT JOIN {content_moderation_state_field_revision} content_moderation_state ON node_field_revision.vid = content_moderation_state.content_entity_revision_id AND (content_moderation_state.content_entity_type_id = :views_join_condition_1 AND content_moderation_state.langcode = node_field_revision.langcode)
WHERE ((node_field_data.status = 1 OR (node_field_data.uid = 0 AND 0 &lt;&gt; 0 AND 1 = 1) OR 1 = 1)) AND (node_field_data_1.type IN (:db_condition_placeholder_0)) AND ((content_moderation_state.workflow = :db_condition_placeholder_1) AND (content_moderation_state.moderation_state = :db_condition_placeholder_2))) subquery; Array
(
    [:db_condition_placeholder_0] =&gt; article
    [:db_condition_placeholder_1] =&gt; editorial
    [:db_condition_placeholder_2] =&gt; draft
    [:views_join_condition_1] =&gt; node
)
 in <em class="placeholder">Drupal\views\Plugin\views\query\Sql-&gt;execute()</em> (line <em class="placeholder">1507</em> of <em class="placeholder">core/modules/views/src/Plugin/views/query/Sql.php</em>). <pre class="backtrace">Drupal\views\ViewExecutable-&gt;execute(NULL) (Line: 1451)
Drupal\views\ViewExecutable-&gt;render() (Line: 183)
Drupal\views\Plugin\views\display\Page-&gt;execute() (Line: 1627)
Drupal\views\ViewExecutable-&gt;executeDisplay(&#039;page_1&#039;, Array) (Line: 77)
Drupal\views\Element\View::preRenderViewElement(Array)
call_user_func(Array, Array) (Line: 378)
Drupal\Core\Render\Renderer-&gt;doRender(Array, ) (Line: 195)
Drupal\Core\Render\Renderer-&gt;render(Array, ) (Line: 226)
Drupal\Core\Render\MainContent\HtmlRenderer-&gt;Drupal\Core\Render\MainContent\{closure}() (Line: 582)
Drupal\Core\Render\Renderer-&gt;executeInRenderContext(Object, Object) (Line: 227)
Drupal\Core\Render\MainContent\HtmlRenderer-&gt;prepare(Array, Object, Object) (Line: 117)
Drupal\Core\Render\MainContent\HtmlRenderer-&gt;renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber-&gt;onViewRenderArray(Object, &#039;kernel.view&#039;, Object) (Line: 108)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher-&gt;dispatch(&#039;kernel.view&#039;, Object) (Line: 158)
Symfony\Component\HttpKernel\HttpKernel-&gt;handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel-&gt;handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session-&gt;handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle-&gt;handle(Object, 1, 1) (Line: 184)
Drupal\page_cache\StackMiddleware\PageCache-&gt;fetch(Object, 1, 1) (Line: 121)
Drupal\page_cache\StackMiddleware\PageCache-&gt;lookup(Object, 1, 1) (Line: 75)
Drupal\page_cache\StackMiddleware\PageCache-&gt;handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware-&gt;handle(Object, 1, 1) (Line: 50)
Drupal\Core\StackMiddleware\NegotiationMiddleware-&gt;handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel-&gt;handle(Object, 1, 1) (Line: 657)
Drupal\Core\DrupalKernel-&gt;handle(Object) (Line: 19)
</pre>

https://screenshots.firefoxusercontent.com/images/d0db2c7f-faee-4b4a-987...

Steps to reproduce:
1. install drupal core branch 8.5.x and patch https://www.drupal.org/files/issues/2862041-229.patch on simplytest.me (default install)
2. enable workflows and content moderation modules
3. configure editorial workflow to add article to the workflow
4. add moderation state filter and save
5. go to /admin/content and select a moderation state such as draft and select filter
6. check if you get an error

Also got this error locally.

larowlan’s picture

#2859381: Broken/missing handler for Moderation state field is in, can we get an updated status here as this is blocking CM going stable, which we'd love to see in 8.5

Manuel Garcia’s picture

Rerolled #229 for now.

Manually fixed conflicts on core/modules/content_moderation/src/ViewsData.php.

Sam152’s picture

I think the postgres fail is genuine:

Failed asserting that Array &0 (
    0 => Array &1 (
        'nid' => '3'
    )
    1 => Array &2 (
        'nid' => '1'
    )
) is identical to Array &0 (
    0 => Array &1 (
        'nid' => '1'
    )
    1 => Array &2 (
        'nid' => '3'
    )
).

Need to order the results somewhere?

amateescu’s picture

Yup, most test views need specific sorting for postgres. This should fix it :)

Status: Needs review » Needs work

The last submitted patch, 243: 2862041-243.patch, failed testing. View results

amateescu’s picture

Of course, the test revision view also needs a sort on the revision ID.

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

Now we have tests passing for all three database types we can RTBC!

Thanks @amateescu!

smaz’s picture

Just given this a brief test too, works very well - thanks!

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
FileSize
169.02 KB
157.52 KB
  1. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,291 @@
    +      // field data or revision table. This allows filtering states against either
    

    nit, >80

  2. +++ b/core/modules/content_moderation/src/Plugin/views/filter/ModerationStateFilter.php
    @@ -0,0 +1,291 @@
    +    if ($this->operator == 'in') {
    

    nit ===

  3. +++ b/core/modules/views/src/Plugin/views/filter/InOperator.php
    @@ -229,7 +229,9 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
    +        '#size' => min(count($options, COUNT_RECURSIVE), 8),
    

    missing test coverage - can we check this attribute in one of the existing web tests?

I did some manual testing of this

Added the 'moderation state' in 'content' category filter to the existing admin/content view - worked as expected.
Added the 'moderation state' in 'content revision' category filter to the existing admin/conten view - tried to filter, received this SQL error

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'node_field_revision.nid' in 'on clause': SELECT COUNT(*) AS expression FROM (SELECT 1 AS expression FROM {node_field_data} node_field_data INNER JOIN {users_field_data} users_field_data_node_field_data ON node_field_data.uid = users_field_data_node_field_data.uid INNER JOIN {node_field_data} node_field_data_1 ON node_field_revision.nid = node_field_data_1.nid AND node_field_data_1.langcode = node_field_revision.langcode INNER JOIN {node_field_revision} node_field_revision ON node_field_data.vid = node_field_revision.vid LEFT JOIN {content_moderation_state_field_revision} content_moderation_state ON node_field_revision.vid = content_moderation_state.content_entity_revision_id AND (content_moderation_state.content_entity_type_id = :views_join_condition_1 AND content_moderation_state.langcode = node_field_revision.langcode) WHERE ((node_field_data.status = 1 OR (node_field_data.uid = 1 AND 1 <> 0 AND 1 = 1) OR 1 = 1)) AND (node_field_data_1.type IN (:db_condition_placeholder_0)) AND ((content_moderation_state.workflow = :db_condition_placeholder_1) AND (content_moderation_state.moderation_state = :db_condition_placeholder_2))) subquery; Array ( [:db_condition_placeholder_0] => article [:db_condition_placeholder_1] => editorial [:db_condition_placeholder_2] => draft [:views_join_condition_1] => node )

So I think we either need to limit access to that filter where the base table isn't a revision table, or we need to fix our join logic (most likely the latter)?

Screenshots attached of my configuration and tests.

timmillwood’s picture

Assigned: xjm » Unassigned
Status: Needs work » Needs review
FileSize
5.08 KB
55.07 KB

This patch fixes 1, 2, and 3 from #248. I didn't get time to fully work out the issue shown in the screenshots. I was able to reproduce the issue and assume something needs to change with the join in \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter::opSimple, but not really sure what.

To fix #248.3 I made the test view exposed filter a multi select so the size attribute is added, then provided an assertion in \Drupal\Tests\content_moderation\Functional\ViewsModerationStateFilterTest::assertFilterStates to size is the number of states, plus one for the optgroup label, but no more than 8.

timmillwood’s picture

Status: Needs review » Needs work

Bumping straight back to needs work to fix our join logic from #248.

amateescu’s picture

The fix for the SQL error from #248 is quite simple, we need to call ensureMyTable() earlier in ModerationStateFilter::opSimple(). Fixed that and tested with a new view which has the content moderation filter on the revision table.

Also improved the test for #248.3.

The last submitted patch, 251: 2862041-251-test-only.patch, failed testing. View results

timmillwood’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @amateescu, looks good to me!

RoSk0’s picture

Issue summary: View changes
larowlan’s picture

Issue tags: +Needs manual testing
+++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php
@@ -188,7 +188,20 @@ public function testWorkflowChanges() {
+      ['test_content_moderation_state_filter_base_table', 'Content: Moderation state'],
+      ['test_content_moderation_state_filter_base_table_filter_on_revision', 'Content revision: Moderation state'],

minor nit: can we name these cases

it helps debugging fails

i.e. make the array keyed

Tagging for a manual test to repeat the steps in #248

timmillwood’s picture

Naming things is the hardest, but here's some names for the test cases.

amateescu’s picture

This should describe better what's being tested.

tedbow’s picture

Manual testing for #248

  1. Enabled Content Moderation
  2. Enable Editorial workflow for articles
  3. Edit 'admin/content' view
  4. Add 'moderation state' == draft filter for the 'content' category
  5. No view results
  6. Add a 'draft' article
  7. 1 view result
  8. Publish atrticle
  9. 0 view results

I tried the same for filter but with the 'content revision' category. The filter seem to have no effect but there was not error.

I was going to remove the "Needs Manual testing" tag. But I am not sure if the "content revision" category filter behaviour is what is expected.

amateescu’s picture

@tedbow, I just tried that scenario locally and adding the filter from "Content revision" works as expected, meaning that it shows the draft article. This is also very well tested in \Drupal\Tests\content_moderation\Functional\ViewsModerationStateFilterTest::testWorkflowChanges()..

larowlan’s picture

Adding review credits

larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Manually tested this again, confirmed the previous issue is resolved.

Committed as 418e499 and pushed to 8.6.x

Cherry-picked as 58cf759 and pushed to 8.5.x.

Added and published a change record.

Unpostponed #2902187: Provide a way for users to moderate content

:tada:

great work folks!

  • larowlan committed 418e499 on 8.6.x
    Issue #2862041 by Sam152, amateescu, timmillwood, jibran, hamrant,...

  • larowlan committed 58cf759 on 8.5.x
    Issue #2862041 by Sam152, amateescu, timmillwood, jibran, hamrant,...

Status: Fixed » Closed (fixed)

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

bramdebacker’s picture

I've been trying to use the moderation state as a filter for my REST export view, but I get the following SQL error:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'node_field_data.type' in 'where clause': SELECT node_field_data_user__field_backpack__tracker_node.nid AS node_field_data_user__field_backpack__tracker_node_nid, user__field_backpack.delta AS user__field_backpack_delta, users_field_data.uid AS uid, node_field_data_user__field_backpack.nid AS node_field_data_user__field_backpack_nid, users_field_data_node__field_author.uid AS users_field_data_node__field_author_uid, node_field_data_node_field_revision.nid AS node_field_data_node_field_revision_nid FROM {users_field_data} users_field_data LEFT JOIN {user__field_backpack} user__field_backpack ON users_field_data.uid = user__field_backpack.entity_id AND user__field_backpack.deleted = :views_join_condition_0 INNER JOIN {node_field_data} node_field_data_user__field_backpack ON user__field_backpack.field_backpack_target_id = node_field_data_user__field_backpack.nid LEFT JOIN {node__field_author} node_field_data_user__field_backpack__node__field_author ON node_field_data_user__field_backpack.nid = node_field_data_user__field_backpack__node__field_author.entity_id AND node_field_data_user__field_backpack__node__field_author.deleted = :views_join_condition_1 LEFT JOIN {users_field_data} users_field_data_node__field_author ON node_field_data_user__field_backpack__node__field_author.field_author_target_id = users_field_data_node__field_author.uid INNER JOIN {node_field_revision} node_field_data_user__field_backpack__node_field_revision ON node_field_data_user__field_backpack.vid = node_field_data_user__field_backpack__node_field_revision.vid LEFT JOIN {node_field_data} node_field_data_node_field_revision ON node_field_data_user__field_backpack__node_field_revision.vid = node_field_data_node_field_revision.vid LEFT JOIN {content_moderation_state_field_revision} content_moderation_state ON node_field_data_user__field_backpack.vid = content_moderation_state.content_entity_revision_id AND (content_moderation_state.content_entity_type_id = :views_join_condition_2 AND content_moderation_state.langcode = node_field_data_user__field_backpack.langcode) INNER JOIN {tracker_node} node_field_data_user__field_backpack__tracker_node ON node_field_data_user__field_backpack.nid = node_field_data_user__field_backpack__tracker_node.nid WHERE ((users_field_data.uid = :users_field_data_uid )) AND ((users_field_data.status = :db_condition_placeholder_4) AND (node_field_data.type IN (:db_condition_placeholder_5)) AND ((content_moderation_state.workflow = :db_condition_placeholder_6) AND (content_moderation_state.moderation_state = :db_condition_placeholder_7))) ORDER BY user__field_backpack_delta DESC; Array ( [:users_field_data_uid] => 1 [:db_condition_placeholder_4] => 1 [:db_condition_placeholder_5] => tip [:db_condition_placeholder_6] => editorial [:db_condition_placeholder_7] => published [:views_join_condition_0] => 0 [:views_join_condition_1] => 0 [:views_join_condition_2] => node )

The view is being used to display content referenced through an entity reference field in user. I'm guessing type is checked in user instead of
the content?

bygeoffthompson’s picture

Hello everyone. Wanted to stop by this long (and great) thread to report my finding while testing this filter. I am working on a site (8.5.3) with core content_moderation and have discovered what I believe to be a UX issue.

It appears like the Content Moderation filter only works for new, never published nodes. As expected, once published, that node no longer populates in the view. This works perfectly. However, a node which was published, then edited and saved as a draft, will not populate the view with this filter.

Create Node -> Save (Published) -> Edit Node -> Save (Draft)

My website's administrators expect to have a single view page where all drafts can be viewed (regardless if it's a brand new node or a previously published node that has had changes applied). Unfortunately I don't believe this is possible with the Content Moderation filter built.

Has anyone else encountered this issue? Happy to provide more information if necessary.

Thanks!

bygeoffthompson’s picture

Update on previous note:

I had my view incorrectly set up (listing Content entities instead of Content Revisions). With that squared-away I have a view that satisfies the criteria stated above. For anyone reading this in a similar situation, this post helped point me in the right direction.

Thanks

le72’s picture

Hi everyone. I see long thread on filtering, but nothing about sort. We really need sorting by Moderation State name and Moderation State wight.
Thanks in advance.
Lev.

amaisano’s picture

Came here because exposing the Content Moderation State filter and using "Select All" causes a "No valid values found on filter" error, which doesn't make any sense. I can see and select a certain workflow state, like "Draft" and it does show the correct results, but when I default the filter to the "Select All" option, the View breaks.

I found I could work around this by selecting "Limit to selected values" and basically selecting all the values, including the "Select All" option. No more errors. So strange!

apaderno’s picture

Version: 8.5.x-dev » 9.1.x-dev
xjm’s picture

Version: 9.1.x-dev » 8.5.x-dev

Restoring committed branch. Please open a new issue if you're encountering problems.

apaderno’s picture

I apologize: I confused the issue with an open one.