updated core from 8.4 to 8.5 : bug with login_destination

This commit is contained in:
Bachir Soussi Chiadmi
2018-03-13 14:01:21 +01:00
parent 0d78da249b
commit e668535a4e
3486 changed files with 89604 additions and 33283 deletions
@@ -0,0 +1,817 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- node
- user
id: moderated_content
label: 'Moderated content'
module: views
description: 'Find and moderate content.'
tag: ''
base_table: node_field_revision
base_field: vid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'view any unpublished content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Filter
reset_button: true
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: full
options:
items_per_page: 50
offset: 0
id: 0
total_pages: null
tags:
previous: ' Previous'
next: 'Next '
first: '« First'
last: 'Last »'
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
quantity: 9
style:
type: table
options:
grouping: { }
row_class: ''
default_row_class: true
override: true
sticky: true
caption: ''
summary: ''
description: ''
columns:
title: title
type: type
name: name
moderation_state: moderation_state
changed: changed
info:
title:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
type:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
name:
sortable: false
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
moderation_state:
sortable: true
default_sort_order: asc
align: ''
separator: ''
empty_column: false
responsive: ''
changed:
sortable: true
default_sort_order: desc
align: ''
separator: ''
empty_column: false
responsive: ''
default: changed
empty_table: true
row:
type: fields
fields:
title:
id: title
table: node_field_revision
field: title
relationship: none
group_type: group
admin_label: ''
label: Title
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: title
plugin_id: field
type:
id: type
table: node_field_data
field: type
relationship: nid
group_type: group
admin_label: ''
label: 'Content type'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
type: entity_reference_label
settings:
link: false
group_column: target_id
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: type
plugin_id: field
name:
id: name
table: users_field_data
field: name
relationship: uid
group_type: group
admin_label: ''
label: Author
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: user_name
settings:
link_to_entity: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: user
entity_field: name
plugin_id: field
moderation_state:
id: moderation_state
table: node_field_revision
field: moderation_state
relationship: none
group_type: group
admin_label: ''
label: 'Moderation state'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: content_moderation_state
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
plugin_id: field
changed:
id: changed
table: node_field_revision
field: changed
relationship: none
group_type: group
admin_label: ''
label: Updated
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: false
ellipsis: false
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: timestamp
settings:
date_format: short
custom_date_format: ''
timezone: ''
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: changed
plugin_id: field
operations:
id: operations
table: node_revision
field: operations
relationship: none
group_type: group
admin_label: ''
label: Operations
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
destination: true
entity_type: node
plugin_id: entity_operations
filters:
latest_revision:
id: latest_revision
table: node_revision
field: latest_revision
relationship: none
group_type: group
admin_label: ''
operator: '='
value: ''
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: latest_revision
title:
id: title
table: node_field_revision
field: title
relationship: none
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: true
expose:
operator_id: title_op
label: Title
description: ''
use_operator: false
operator: title_op
identifier: title
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
entity_field: title
plugin_id: string
type:
id: type
table: node_field_data
field: type
relationship: nid
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: type_op
label: 'Content type'
description: ''
use_operator: false
operator: type_op
identifier: type
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
entity_field: type
plugin_id: bundle
moderation_state:
id: moderation_state
table: node_field_revision
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: in
value:
editorial-draft: editorial-draft
editorial-archived: editorial-archived
group: 1
exposed: true
expose:
operator_id: moderation_state_op
label: 'Moderation state'
description: ''
use_operator: false
operator: moderation_state_op
identifier: moderation_state
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: true
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
langcode:
id: langcode
table: node_field_revision
field: langcode
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: langcode_op
label: Language
description: ''
use_operator: false
operator: langcode_op
identifier: langcode
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
entity_field: langcode
plugin_id: language
moderation_state_1:
id: moderation_state_1
table: node_field_revision
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: 'not in'
value:
editorial-published: editorial-published
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
sorts: { }
title: 'Moderated content'
header: { }
footer: { }
empty:
area_text_custom:
id: area_text_custom
table: views
field: area_text_custom
relationship: none
group_type: group
admin_label: ''
empty: true
tokenize: false
content: 'No moderated content available. Only pending versions of content, such as drafts, are listed here.'
plugin_id: text_custom
relationships:
nid:
id: nid
table: node_field_revision
field: nid
relationship: none
group_type: group
admin_label: 'Get the actual content from a content revision.'
required: false
entity_type: node
entity_field: nid
plugin_id: standard
uid:
id: uid
table: node_field_revision
field: uid
relationship: none
group_type: group
admin_label: User
required: false
entity_type: node
entity_field: uid
plugin_id: standard
arguments: { }
display_extenders: { }
filter_groups:
operator: AND
groups:
1: AND
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
moderated_content:
display_plugin: page
id: moderated_content
display_title: 'Moderated content'
position: 1
display_options:
display_extenders: { }
path: admin/content/moderated
display_description: ''
cache_metadata:
max-age: 0
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
@@ -0,0 +1,18 @@
views.filter.latest_revision:
type: views_filter
label: 'Latest revision'
mapping:
value:
type: string
label: 'Value'
views.filter.moderation_state_filter:
type: views.filter.in_operator
label: 'Moderation state filter'
mapping:
limit_workflows:
type: sequence
label: 'Workflow'
sequence:
type: string
label: 'Workflow'
@@ -1,15 +1,15 @@
name: 'Content Moderation'
type: module
description: 'Provides moderation states for content'
description: 'Provides moderation states for content.'
# version: VERSION
# core: 8.x
package: Core (Experimental)
package: Core
configure: entity.workflow.collection
dependencies:
- workflows
# Information added by Drupal.org packaging script on 2018-01-03
version: '8.4.4'
# Information added by Drupal.org packaging script on 2018-03-07
version: '8.5.0'
core: '8.x'
project: 'drupal'
datestamp: 1515021228
datestamp: 1520457826
@@ -1,3 +1,14 @@
content_moderation.workflows:
deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks'
weight: 100
content_moderation.content:
title: 'Overview'
route_name: system.admin_content
parent_id: system.admin_content
content_moderation.moderated_content:
title: 'Moderated content'
route_name: content_moderation.admin_moderated_content
parent_id: system.admin_content
weight: 1
@@ -8,8 +8,8 @@
use Drupal\content_moderation\EntityOperations;
use Drupal\content_moderation\EntityTypeInfo;
use Drupal\content_moderation\ContentPreprocess;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublishNode;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublishNode;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish;
use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
@@ -20,10 +20,12 @@ use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\workflows\WorkflowInterface;
use Drupal\node\Plugin\Action\PublishNode;
use Drupal\node\Plugin\Action\UnpublishNode;
use Drupal\Core\Action\Plugin\Action\PublishAction;
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
use Drupal\workflows\Entity\Workflow;
use Drupal\views\Entity\View;
/**
* Implements hook_help().
@@ -34,13 +36,20 @@ function content_moderation_help($route_name, RouteMatchInterface $route_match)
case 'help.page.content_moderation':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Content Moderation module provides moderation for content by applying workflows to content. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '</p>';
$output .= '<p>' . t('The Content Moderation module allows you to expand on Drupal\'s "unpublished" and "published" states for content. It allows you to have a published version that is live, but have a separate working copy that is undergoing review before it is published. This is achieved by using <a href=":workflows">Workflows</a> to apply different states and transitions to entities as needed. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation', ':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString()]) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Configuring workflows') . '</dt>';
$output .= '<dd>' . t('Enable the Workflow UI module to create, edit and delete content moderation workflows.') . '</p>';
$output .= '<dt>' . t('Applying workflows') . '</dt>';
$output .= '<dd>' . t('Content Moderation allows you to apply <a href=":workflows">Workflows</a> to content, custom blocks, and other <a href=":field_help" title="Field module help, with background on content entities">content entities</a>, to provide more fine-grained publishing options. For example, a Basic page might have states such as Draft and Published, with allowed transitions such as Draft to Published (making the current revision "live"), and Published to Draft (making a new draft revision of published content).', [':workflows' => Url::fromRoute('help.page', ['name' => 'workflows'])->toString(), ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
if (\Drupal::moduleHandler()->moduleExists('views')) {
$moderated_content_view = View::load('moderated_content');
if (isset($moderated_content_view) && $moderated_content_view->status() === TRUE) {
$output .= '<dt>' . t('Moderating content') . '</dt>';
$output .= '<dd>' . t('You can view a list of content awaiting moderation on the <a href=":moderated">moderated content page</a>. This will show any content in an unpublished state, such as Draft or Archived, to help surface content that requires more work from content editors.', [':moderated' => Url::fromRoute('view.moderated_content.moderated_content')->toString()]) . '</dd>';
}
}
$output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
$output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '</p>';
$output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, they can use the transition to change the state of the content item, from Draft to Published.') . '</dd>';
$output .= '</dl>';
return $output;
}
@@ -118,6 +127,15 @@ function content_moderation_entity_translation_delete(EntityInterface $translati
->entityTranslationDelete($translation);
}
/**
* Implements hook_entity_prepare_form().
*/
function content_moderation_entity_prepare_form(EntityInterface $entity, $operation, FormStateInterface $form_state) {
\Drupal::service('class_resolver')
->getInstanceFromDefinition(EntityTypeInfo::class)
->entityPrepareForm($entity, $operation, $form_state);
}
/**
* Implements hook_form_alter().
*/
@@ -227,13 +245,15 @@ function content_moderation_action_info_alter(&$definitions) {
// The publish/unpublish actions are not valid on moderated entities. So swap
// their implementations out for alternates that will become a no-op on a
// moderated node. If another module has already swapped out those classes,
// moderated entity. If another module has already swapped out those classes,
// though, we'll be polite and do nothing.
if (isset($definitions['node_publish_action']['class']) && $definitions['node_publish_action']['class'] == PublishNode::class) {
$definitions['node_publish_action']['class'] = ModerationOptOutPublishNode::class;
}
if (isset($definitions['node_unpublish_action']['class']) && $definitions['node_unpublish_action']['class'] == UnpublishNode::class) {
$definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class;
foreach ($definitions as &$definition) {
if ($definition['id'] === 'entity:publish_action' && $definition['class'] == PublishAction::class) {
$definition['class'] = ModerationOptOutPublish::class;
}
if ($definition['id'] === 'entity:unpublish_action' && $definition['class'] == UnpublishAction::class) {
$definition['class'] = ModerationOptOutUnpublish::class;
}
}
}
@@ -241,6 +261,7 @@ function content_moderation_action_info_alter(&$definitions) {
* Implements hook_entity_bundle_info_alter().
*/
function content_moderation_entity_bundle_info_alter(&$bundles) {
$translatable = FALSE;
/** @var \Drupal\workflows\WorkflowInterface $workflow */
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
@@ -249,10 +270,18 @@ function content_moderation_entity_bundle_info_alter(&$bundles) {
foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
if (isset($bundles[$entity_type_id][$bundle_id])) {
$bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
// If we have even one moderation-enabled translatable bundle, we need
// to make the moderation state bundle translatable as well, to enable
// the revision translation merge logic also for content moderation
// state revisions.
if (!empty($bundles[$entity_type_id][$bundle_id]['translatable'])) {
$translatable = TRUE;
}
}
}
}
}
$bundles['content_moderation_state']['content_moderation_state']['translatable'] = $translatable;
}
/**
@@ -290,13 +319,3 @@ function content_moderation_workflow_update(WorkflowInterface $entity) {
// Clear field cache so extra field is added or removed.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
* Implements hook_rest_resource_alter().
*/
function content_moderation_rest_resource_alter(&$definitions) {
// ContentModerationState is an internal entity type. Therefore it should not
// be exposed via REST.
// @see \Drupal\content_moderation\ContentModerationStateAccessControlHandler
unset($definitions['entity:content_moderation_state']);
}
@@ -0,0 +1,95 @@
<?php
/**
* @file
* Post update functions for the Content Moderation module.
*/
use Drupal\Core\Site\Settings;
use Drupal\workflows\Entity\Workflow;
/**
* Synchronize moderation state default revisions with their host entities.
*/
function content_moderation_post_update_update_cms_default_revisions(&$sandbox) {
// For every moderated entity, identify the default revision ID, track the
// corresponding "content_moderation_state" revision and save it as the new
// default revision, if needed.
// Initialize sandbox info.
$entity_type_id = &$sandbox['entity_type_id'];
if (!isset($entity_type_id)) {
$sandbox['bundles'] = [];
/** @var \Drupal\workflows\WorkflowInterface $workflow */
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
$plugin = $workflow->getTypePlugin();
foreach ($plugin->getEntityTypes() as $entity_type_id) {
$sandbox['entity_type_ids'][$entity_type_id] = $entity_type_id;
foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle) {
$sandbox['bundles'][$entity_type_id][$bundle] = $bundle;
}
}
}
$sandbox['offset'] = 0;
$sandbox['limit'] = Settings::get('entity_update_batch_size', 50);
$sandbox['total'] = count($sandbox['entity_type_ids']);
$entity_type_id = array_shift($sandbox['entity_type_ids']);
}
// If there are no moderated bundles or we processed all of them, we are done.
$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $content_moderation_state_storage */
$content_moderation_state_storage = $entity_type_manager->getStorage('content_moderation_state');
if (!$entity_type_id) {
$content_moderation_state_storage->resetCache();
$sandbox['#finished'] = 1;
return;
}
// Retrieve a batch of moderated entities to be processed.
$storage = $entity_type_manager->getStorage($entity_type_id);
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
$query = $storage->getQuery()
->accessCheck(FALSE)
->sort($entity_type->getKey('id'))
->range($sandbox['offset'], $sandbox['limit']);
$bundle_key = $entity_type->getKey('bundle');
if ($bundle_key && !empty($sandbox['bundles'][$entity_type_id])) {
$bundles = array_keys($sandbox['bundles'][$entity_type_id]);
$query->condition($bundle_key, $bundles, 'IN');
}
$entity_ids = $query->execute();
// Compute progress status and skip to the next entity type, if needed.
$sandbox['#finished'] = ($sandbox['total'] - count($sandbox['entity_type_ids']) - 1) / $sandbox['total'];
if (!$entity_ids) {
$sandbox['offset'] = 0;
$entity_type_id = array_shift($sandbox['entity_type_ids']) ?: FALSE;
return;
}
// Load the "content_moderation_state" revisions corresponding to the
// moderated entity default revisions.
$result = $content_moderation_state_storage->getQuery()
->allRevisions()
->condition('content_entity_type_id', $entity_type_id)
->condition('content_entity_revision_id', array_keys($entity_ids), 'IN')
->execute();
/** @var \Drupal\Core\Entity\ContentEntityInterface[] $revisions */
$revisions = $content_moderation_state_storage->loadMultipleRevisions(array_keys($result));
// Update "content_moderation_state" data.
foreach ($revisions as $revision) {
if (!$revision->isDefaultRevision()) {
$revision->setNewRevision(FALSE);
$revision->isDefaultRevision(TRUE);
$content_moderation_state_storage->save($revision);
}
}
// Clear static cache to avoid memory issues.
$storage->resetCache($entity_ids);
$sandbox['offset'] += $sandbox['limit'];
}
@@ -1,3 +1,11 @@
content_moderation.admin_moderated_content:
path: '/admin/content/moderated'
defaults:
_controller: '\Drupal\content_moderation\Controller\ModeratedContentController::nodeListing'
_title: 'Moderated content'
requirements:
_permission: 'view any unpublished content'
content_moderation.workflow_type_edit_form:
path: '/admin/config/workflow/workflows/manage/{workflow}/type/{entity_type_id}'
defaults:
@@ -1,7 +1,7 @@
services:
paramconverter.latest_revision:
class: Drupal\content_moderation\ParamConverter\EntityRevisionConverter
arguments: ['@entity.manager', '@content_moderation.moderation_information']
parent: paramconverter.entity
tags:
- { name: paramconverter, priority: 5 }
content_moderation.state_transition_validation:
@@ -20,3 +20,8 @@ services:
arguments: ['@config.manager', '@entity_type.manager']
tags:
- { name: event_subscriber }
content_moderation.route_subscriber:
class: Drupal\content_moderation\Routing\ContentModerationRouteSubscriber
arguments: ['@entity_type.manager']
tags:
- { name: event_subscriber }
@@ -2,7 +2,7 @@
* @file
* Component styles for the content_moderation module.
*/
ul.entity-moderation-form {
.entity-moderation-form {
list-style: none;
display: -webkit-flex; /* Safari */
display: flex;
@@ -12,31 +12,27 @@ ul.entity-moderation-form {
align-items: flex-start;
}
ul.entity-moderation-form li {
.entity-moderation-form__item {
margin-right: 2em;
display: table;
}
ul.entity-moderation-form li:last-child {
.entity-moderation-form__item:last-child {
-webkit-align-self: flex-end; /* Safari */
align-self: flex-end;
margin-right: 0;
}
ul.entity-moderation-form .form-item {
.entity-moderation-form .form-item {
margin-top: 1em;
margin-bottom: 1em;
}
ul.entity-moderation-form .form-item label {
.entity-moderation-form .form-item label {
padding-bottom: 0.25em;
display: table;
}
ul.entity-moderation-form input[type=submit] {
.entity-moderation-form input[type=submit] {
margin-bottom: 1.2em;
}
ul.entity-moderation-form .btn {
margin-bottom: 1.1em;
}
@@ -2,9 +2,9 @@
* @file
* Theme styles for the content_moderation module.
*/
ul.entity-moderation-form {
.entity-moderation-form {
border: 1px dashed #bbb;
margin: 2em 0em;
margin: 2em 0;
background: #fff;
padding-left: 1em;
}
@@ -0,0 +1,25 @@
<?php
namespace Drupal\content_moderation\Controller;
use Drupal\content_moderation\ModeratedNodeListBuilder;
use Drupal\Core\Controller\ControllerBase;
/**
* Defines a controller to list moderated nodes.
*/
class ModeratedContentController extends ControllerBase {
/**
* Provides the listing page for moderated nodes.
*
* @return array
* A render array as expected by drupal_render().
*/
public function nodeListing() {
$entity_type = $this->entityTypeManager()->getDefinition('node');
return $this->entityTypeManager()->createHandlerInstance(ModeratedNodeListBuilder::class, $entity_type)->render();
}
}
@@ -31,6 +31,7 @@ use Drupal\user\UserInterface;
* data_table = "content_moderation_state_field_data",
* revision_data_table = "content_moderation_state_field_revision",
* translatable = TRUE,
* internal = TRUE,
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
@@ -221,4 +222,16 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
return parent::save();
}
/**
* {@inheritdoc}
*/
protected function getFieldsToSkipFromTranslationChangesCheck() {
$field_names = parent::getFieldsToSkipFromTranslationChangesCheck();
// We need to skip the parent entity revision ID, since that will always
// change on every save, otherwise every translation would be marked as
// affected regardless of actual changes.
$field_names[] = 'content_entity_revision_id';
return $field_names;
}
}
@@ -35,11 +35,6 @@ class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInte
// This is probably not necessary if configuration is setup correctly.
$entity->setNewRevision(TRUE);
$entity->isDefaultRevision($default_revision);
if ($entity->hasField('revision_translation_affected')) {
// @todo remove this when revision and translation issues have been
// resolved. https://www.drupal.org/node/2860097
$entity->set('revision_translation_affected', TRUE);
}
// Update publishing status if it can be updated and if it needs updating.
if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) {
@@ -88,7 +88,7 @@ class EntityModerationRouteProvider implements EntityRouteProviderInterface, Ent
->setOption('parameters', [
$entity_type_id => [
'type' => 'entity:' . $entity_type_id,
'load_pending_revision' => 1,
'load_latest_revision' => TRUE,
],
]);
@@ -11,6 +11,8 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\content_moderation\Form\EntityModerationForm;
use Drupal\Core\Routing\RouteBuilderInterface;
use Drupal\workflows\Entity\Workflow;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -48,6 +50,13 @@ class EntityOperations implements ContainerInjectionInterface {
*/
protected $bundleInfo;
/**
* The router builder service.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routerBuilder;
/**
* Constructs a new EntityOperations object.
*
@@ -59,12 +68,15 @@ class EntityOperations implements ContainerInjectionInterface {
* The form builder.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The entity bundle information service.
* @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
* The router builder service.
*/
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info) {
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info, RouteBuilderInterface $router_builder) {
$this->moderationInfo = $moderation_info;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
$this->bundleInfo = $bundle_info;
$this->routerBuilder = $router_builder;
}
/**
@@ -75,7 +87,8 @@ class EntityOperations implements ContainerInjectionInterface {
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.manager'),
$container->get('form_builder'),
$container->get('entity_type.bundle.info')
$container->get('entity_type.bundle.info'),
$container->get('router.builder')
);
}
@@ -98,10 +111,9 @@ class EntityOperations implements ContainerInjectionInterface {
$current_state = $workflow->getTypePlugin()
->getState($entity->moderation_state->value);
// This entity is default if it is new, a new translation, the default
// revision, or the default revision is not published.
// This entity is default if it is new, the default revision, or the
// default revision is not published.
$update_default_revision = $entity->isNew()
|| $entity->isNewTranslation()
|| $current_state->isDefaultRevisionState()
|| !$this->moderationInfo->isDefaultRevisionPublished($entity);
@@ -134,6 +146,12 @@ class EntityOperations implements ContainerInjectionInterface {
if ($this->moderationInfo->isModeratedEntity($entity)) {
$this->updateOrCreateFromEntity($entity);
}
// When updating workflow settings for Content Moderation, we need to
// rebuild routes as we may be enabling new entity types and the related
// entity forms.
elseif ($entity instanceof Workflow && $entity->getTypePlugin()->getPluginId() == 'content_moderation') {
$this->routerBuilder->setRebuildNeeded();
}
}
/**
@@ -147,9 +165,10 @@ class EntityOperations implements ContainerInjectionInterface {
$entity_revision_id = $entity->getRevisionId();
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
$content_moderation_state = ContentModerationStateEntity::loadFromModeratedEntity($entity);
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage('content_moderation_state');
if (!($content_moderation_state instanceof ContentModerationStateInterface)) {
$storage = $this->entityTypeManager->getStorage('content_moderation_state');
$content_moderation_state = $storage->create([
'content_entity_type_id' => $entity->getEntityTypeId(),
'content_entity_id' => $entity->id(),
@@ -159,11 +178,6 @@ class EntityOperations implements ContainerInjectionInterface {
]);
$content_moderation_state->workflow->target_id = $workflow->id();
}
elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
// If a new revision of the content has been created, add a new content
// moderation state revision.
$content_moderation_state->setNewRevision(TRUE);
}
// Sync translations.
if ($entity->getEntityType()->hasKey('langcode')) {
@@ -176,6 +190,12 @@ class EntityOperations implements ContainerInjectionInterface {
}
}
// If a new revision of the content has been created, add a new content
// moderation state revision.
if (!$content_moderation_state->isNew() && $content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
$content_moderation_state = $storage->createRevision($content_moderation_state, $entity->isDefaultRevision());
}
// Create the ContentModerationState entity for the inserted entity.
$moderation_state = $entity->moderation_state->value;
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
@@ -245,27 +265,31 @@ class EntityOperations implements ContainerInjectionInterface {
* @see EntityFieldManagerInterface::getExtraFields()
*/
public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if (!$this->moderationInfo->isModeratedEntity($entity)) {
return;
}
if (!$this->moderationInfo->isLatestRevision($entity)) {
if (isset($entity->in_preview) && $entity->in_preview) {
return;
}
if ($this->moderationInfo->isLiveRevision($entity)) {
// If the component is not defined for this display, we have nothing to do.
if (!$display->getComponent('content_moderation_control')) {
return;
}
// Don't display the moderation form when when:
// - The revision is not translation affected.
// - There are more than one translation languages.
// - The entity has pending revisions.
if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) {
// The moderation form should be displayed only when viewing the latest
// (translation-affecting) revision, unless it was created as published
// default revision.
if (!$entity->isLatestRevision() && !$entity->isLatestTranslationAffectedRevision()) {
return;
}
if (($entity->isDefaultRevision() || $entity->wasDefaultRevision()) && ($moderation_state = $entity->get('moderation_state')->value)) {
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
if ($workflow->getTypePlugin()->getState($moderation_state)->isPublishedState()) {
return;
}
}
$component = $display->getComponent('content_moderation_control');
if ($component) {
$build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
}
$build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
}
}
@@ -7,10 +7,12 @@ use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -268,6 +270,40 @@ class EntityTypeInfo implements ContainerInjectionInterface {
return $fields;
}
/**
* Replaces the entity form entity object with a proper revision object.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being edited.
* @param string $operation
* The entity form operation.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @see hook_entity_prepare_form()
*/
public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
$form_object = $form_state->getFormObject();
if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) {
// Generate a proper revision object for the current entity. This allows
// to correctly handle translatable entities having pending revisions.
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
/** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
$new_revision = $storage->createRevision($entity, FALSE);
// Restore the revision ID as other modules may expect to find it still
// populated. This will reset the "new revision" flag, however the entity
// object will be marked as a new revision again on submit.
// @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
$revision_key = $new_revision->getEntityType()->getKey('revision');
$new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
$form_object->setEntity($new_revision);
}
}
/**
* Alters bundle forms to enforce revision handling.
*
@@ -291,57 +327,15 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
}
}
elseif ($form_object instanceof ContentEntityFormInterface && in_array($form_object->getOperation(), ['edit', 'default'])) {
elseif ($this->isModeratedEntityEditForm($form_object)) {
/** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $form_object->getEntity();
if ($this->moderationInfo->isModeratedEntity($entity)) {
$this->entityTypeManager
->getHandler($entity->getEntityTypeId(), 'moderation')
->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
if (!$this->moderationInfo->isPendingRevisionAllowed($entity)) {
$latest_revision = $this->moderationInfo->getLatestRevision($entity->getEntityTypeId(), $entity->id());
if ($entity->bundle()) {
$bundle_type_id = $entity->getEntityType()->getBundleEntityType();
$bundle = $this->entityTypeManager->getStorage($bundle_type_id)->load($entity->bundle());
$type_label = $bundle->label();
}
else {
$type_label = $entity->getEntityType()->getLabel();
}
$translation = $this->moderationInfo->getAffectedRevisionTranslation($latest_revision);
$args = [
'@type_label' => $type_label,
'@latest_revision_edit_url' => $translation->toUrl('edit-form', ['language' => $translation->language()])->toString(),
'@latest_revision_delete_url' => $translation->toUrl('delete-form', ['language' => $translation->language()])->toString(),
];
$label = $this->t('Unable to save this @type_label.', $args);
$message = $this->t('<a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
$full_message = $this->t('Unable to save this @type_label. <a href="@latest_revision_edit_url">Publish</a> or <a href="@latest_revision_delete_url">delete</a> the latest revision to allow all workflow transitions.', $args);
drupal_set_message($full_message, 'error');
$form['moderation_state']['#access'] = FALSE;
$form['actions']['#access'] = FALSE;
$form['invalid_transitions'] = [
'label' => [
'#type' => 'item',
'#prefix' => '<strong class="label">',
'#markup' => $label,
'#suffix' => '</strong>',
],
'message' => [
'#type' => 'item',
'#markup' => $message,
],
'#weight' => 999,
'#no_valid_transitions' => TRUE,
];
if ($form['footer']) {
$form['invalid_transitions']['#group'] = 'footer';
}
}
// Submit handler to redirect to the latest version, if available.
$form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
@@ -351,15 +345,30 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$form['moderation_state']['#group'] = 'footer';
}
// Duplicate the label of the current moderation state to the meta
// region, if available.
// If the publishing status exists in the meta region, replace it with
// the current state instead.
if (isset($form['meta']['published'])) {
$form['meta']['published']['#markup'] = $form['moderation_state']['widget'][0]['current']['#markup'];
$form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label();
}
}
}
}
/**
* Checks whether the specified form allows to edit a moderated entity.
*
* @param \Drupal\Core\Form\FormInterface $form_object
* The form object.
*
* @return bool
* TRUE if the form should get form moderation, FALSE otherwise.
*/
protected function isModeratedEntityEditForm(FormInterface $form_object) {
return $form_object instanceof ContentEntityFormInterface &&
in_array($form_object->getOperation(), ['edit', 'default'], TRUE) &&
$this->moderationInfo->isModeratedEntity($form_object->getEntity());
}
/**
* Redirect content entity edit forms on save, if there is a pending revision.
*
@@ -17,6 +17,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* The form for editing entity types associated with a workflow.
*
* @internal
*/
class ContentModerationConfigureEntityTypesForm extends FormBase {
@@ -8,12 +8,14 @@ use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\content_moderation\StateTransitionValidation;
use Drupal\content_moderation\StateTransitionValidationInterface;
use Drupal\workflows\Transition;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The EntityModerationForm provides a simple UI for changing moderation state.
*
* @internal
*/
class EntityModerationForm extends FormBase {
@@ -34,7 +36,7 @@ class EntityModerationForm extends FormBase {
/**
* The moderation state transition validation service.
*
* @var \Drupal\content_moderation\StateTransitionValidation
* @var \Drupal\content_moderation\StateTransitionValidationInterface
*/
protected $validation;
@@ -43,15 +45,15 @@ class EntityModerationForm extends FormBase {
*
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
* @param \Drupal\content_moderation\StateTransitionValidation $validation
* @param \Drupal\content_moderation\StateTransitionValidationInterface $validation
* The moderation state transition validation service.
* @param \Drupal\Component\Datetime\Time $time
* The time service.
*/
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, Time $time) {
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidationInterface $validation, Time $time) {
$this->moderationInfo = $moderation_info;
$this->time = $time;
$this->validation = $validation;
$this->time = $time;
}
/**
@@ -136,6 +138,9 @@ class EntityModerationForm extends FormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $form_state->get('entity');
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
$entity = $storage->createRevision($entity, $entity->isDefaultRevision());
$new_state = $form_state->getValue('new_state');
@@ -0,0 +1,128 @@
<?php
namespace Drupal\content_moderation;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\node\NodeListBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of moderated node entities.
*/
class ModeratedNodeListBuilder extends NodeListBuilder {
/**
* The entity storage class.
*
* @var \Drupal\Core\Entity\RevisionableStorageInterface
*/
protected $storage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ModeratedNodeListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, RedirectDestinationInterface $redirect_destination, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($entity_type, $storage, $date_formatter, $redirect_destination);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity.manager')->getStorage($entity_type->id()),
$container->get('date.formatter'),
$container->get('redirect.destination'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function load() {
$revision_ids = $this->getEntityRevisionIds();
return $this->storage->loadMultipleRevisions($revision_ids);
}
/**
* Loads entity revision IDs using a pager sorted by the entity revision ID.
*
* @return array
* An array of entity revision IDs.
*/
protected function getEntityRevisionIds() {
$query = $this->entityTypeManager->getStorage('content_moderation_state')->getAggregateQuery()
->aggregate('content_entity_id', 'MAX')
->groupBy('content_entity_revision_id')
->condition('content_entity_type_id', $this->entityTypeId)
->condition('moderation_state', 'published', '<>')
->sort('content_entity_revision_id', 'DESC');
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
$result = $query->execute();
return $result ? array_column($result, 'content_entity_revision_id') : [];
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header = parent::buildHeader();
$header['status'] = $this->t('Moderation state');
return $header;
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row = parent::buildRow($entity);
$row['status'] = $entity->moderation_state->value;
return $row;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['table']['#empty'] = $this->t('There is no moderated @label yet. Only pending versions of @label, such as drafts, are listed here.', ['@label' => $this->entityType->getLabel()]);
return $build;
}
}
@@ -127,13 +127,6 @@ class ModerationInformation implements ModerationInformationInterface {
}
}
/**
* {@inheritdoc}
*/
public function isPendingRevisionAllowed(ContentEntityInterface $entity) {
return !(!$entity->isRevisionTranslationAffected() && count($entity->getTranslationLanguages()) > 1 && $this->hasPendingRevision($entity));
}
/**
* {@inheritdoc}
*/
@@ -145,8 +138,20 @@ class ModerationInformation implements ModerationInformationInterface {
* {@inheritdoc}
*/
public function hasPendingRevision(ContentEntityInterface $entity) {
return $this->isModeratedEntity($entity)
&& !($this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()) == $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id()));
$result = FALSE;
if ($this->isModeratedEntity($entity)) {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$latest_revision_id = $storage->getLatestTranslationAffectedRevisionId($entity->id(), $entity->language()->getId());
$default_revision_id = $entity->isDefaultRevision() && !$entity->isNewRevision() && ($revision_id = $entity->getRevisionId()) ?
$revision_id : $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id());
if ($latest_revision_id != $default_revision_id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $latest_revision */
$latest_revision = $storage->loadRevision($latest_revision_id);
$result = !$latest_revision->wasDefaultRevision();
}
}
return $result;
}
/**
@@ -172,9 +177,15 @@ class ModerationInformation implements ModerationInformationInterface {
// Loop through each language that has a translation.
foreach ($default_revision->getTranslationLanguages() as $language) {
// Load the translated revision.
$language_revision = $default_revision->getTranslation($language->getId());
$translation = $default_revision->getTranslation($language->getId());
// If the moderation state is empty, it was not stored yet so no point
// in doing further work.
$moderation_state = $translation->moderation_state->value;
if (!$moderation_state) {
continue;
}
// Return TRUE if a translation with a published state is found.
if ($workflow->getTypePlugin()->getState($language_revision->moderation_state->value)->isPublishedState()) {
if ($workflow->getTypePlugin()->getState($moderation_state)->isPublishedState()) {
return TRUE;
}
}
@@ -100,19 +100,6 @@ interface ModerationInformationInterface {
*/
public function getAffectedRevisionTranslation(ContentEntityInterface $entity);
/**
* Determines if pending revisions are allowed.
*
* @internal
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The content entity.
*
* @return bool
* If pending revisions are allowed.
*/
public function isPendingRevisionAllowed(ContentEntityInterface $entity);
/**
* Determines if an entity is a latest revision.
*
@@ -2,104 +2,29 @@
namespace Drupal\content_moderation\ParamConverter;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\ParamConverter\EntityConverter;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Symfony\Component\Routing\Route;
/**
* Defines a class for making sure the edit-route loads the current draft.
*
* @internal
* This class only exists to provide backwards compatibility with the
* load_pending_revision flag, the predecessor to load_latest_revision. The
* core entity converter now natively loads the latest revision of an entity
* when the load_latest_revision flag is present. This flag is also added
* automatically to all entity forms.
*/
class EntityRevisionConverter extends EntityConverter {
/**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInformation;
/**
* EntityRevisionConverter constructor.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager, needed by the parent class.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation info utility service.
*/
public function __construct(EntityManagerInterface $entity_manager, ModerationInformationInterface $moderation_info) {
parent::__construct($entity_manager);
$this->moderationInformation = $moderation_info;
}
/**
* {@inheritdoc}
*/
public function applies($definition, $name, Route $route) {
return $this->hasPendingRevisionFlag($definition) || $this->isEditFormPage($route);
}
/**
* Determines if the route definition includes a pending revision flag.
*
* This is a custom flag defined by the Content Moderation module to load
* pending revisions rather than the default revision on a given route.
*
* @param array $definition
* The parameter definition provided in the route options.
*
* @return bool
* TRUE if the pending revision flag is set, FALSE otherwise.
*/
protected function hasPendingRevisionFlag(array $definition) {
return (isset($definition['load_pending_revision']) && $definition['load_pending_revision']);
}
/**
* Determines if a given route is the edit-form for an entity.
*
* @param \Symfony\Component\Routing\Route $route
* The route definition.
*
* @return bool
* Returns TRUE if the route is the edit form of an entity, FALSE otherwise.
*/
protected function isEditFormPage(Route $route) {
if ($default = $route->getDefault('_entity_form')) {
// If no operation is provided, use 'default'.
$default .= '.default';
list($entity_type_id, $operation) = explode('.', $default);
if (!$this->entityManager->hasDefinition($entity_type_id)) {
return FALSE;
}
$entity_type = $this->entityManager->getDefinition($entity_type_id);
return $operation == 'edit' && $entity_type && $entity_type->isRevisionable();
}
}
/**
* {@inheritdoc}
*/
public function convert($value, $definition, $name, array $defaults) {
$entity = parent::convert($value, $definition, $name, $defaults);
if ($entity && $this->moderationInformation->isModeratedEntity($entity) && !$this->moderationInformation->isLatestRevision($entity)) {
$entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults);
$latest_revision = $this->moderationInformation->getLatestRevision($entity_type_id, $value);
if ($latest_revision instanceof EntityInterface) {
// If the entity type is translatable, ensure we return the proper
// translation object for the current context.
if ($entity instanceof TranslatableInterface) {
$latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, ['operation' => 'entity_upcast']);
}
$entity = $latest_revision;
}
if (!empty($definition['load_pending_revision'])) {
@trigger_error('The load_pending_revision flag has been deprecated. You should use load_latest_revision instead.', E_USER_DEPRECATED);
$definition['load_latest_revision'] = TRUE;
}
return $entity;
return parent::convert($value, $definition, $name, $defaults);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Drupal\content_moderation\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\Plugin\Action\PublishAction;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Alternate action plugin that can opt-out of modifying moderated entities.
*
* @see \Drupal\Core\Action\Plugin\Action\PublishAction
*/
class ModerationOptOutPublish extends PublishAction implements ContainerFactoryPluginInterface {
/**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInfo;
/**
* Bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* ModerationOptOutPublish constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* Bundle info service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
$this->moderationInfo = $moderation_info;
$this->bundleInfo = $bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('entity_type.manager'),
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
$bundle_info = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
$bundle_label = $bundle_info[$entity->bundle()]['label'];
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => $bundle_label, '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($entity, $account, $return_as_object);
}
}
@@ -2,65 +2,28 @@
namespace Drupal\content_moderation\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\Plugin\Action\PublishNode;
use Drupal\content_moderation\ModerationInformationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Alternate action plugin that can opt-out of modifying moderated entities.
*
* @see \Drupal\node\Plugin\Action\PublishNode
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.0.
* Use \Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish
* instead.
*
* @see \Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish
* @see https://www.drupal.org/node/2919303
*/
class ModerationOptOutPublishNode extends PublishNode implements ContainerFactoryPluginInterface {
/**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInfo;
/**
* ModerationOptOutPublishNode constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moderationInfo = $moderation_info;
}
class ModerationOptOutPublishNode extends ModerationOptOutPublish {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('content_moderation.moderation_information')
);
}
/**
* {@inheritdoc}
*/
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $entity */
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($entity, $account, $return_as_object);
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $moderation_info, $bundle_info);
@trigger_error(__NAMESPACE__ . '\ModerationOptOutPublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}
@@ -0,0 +1,84 @@
<?php
namespace Drupal\content_moderation\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\ModerationInformationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Alternate action plugin that can opt-out of modifying moderated entities.
*
* @see \Drupal\Core\Action\Plugin\Action\UnpublishAction
*/
class ModerationOptOutUnpublish extends UnpublishAction implements ContainerFactoryPluginInterface {
/**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInfo;
/**
* Bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* ModerationOptOutUnpublish constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* Bundle info service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager);
$this->moderationInfo = $moderation_info;
$this->bundleInfo = $bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('entity_type.manager'),
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
$bundle_info = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
$bundle_label = $bundle_info[$entity->bundle()]['label'];
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => $bundle_label, '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($entity, $account, $return_as_object);
}
}
@@ -2,65 +2,28 @@
namespace Drupal\content_moderation\Plugin\Action;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\Plugin\Action\UnpublishNode;
use Drupal\content_moderation\ModerationInformationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
* Alternate action plugin that can opt-out of modifying moderated entities.
*
* @see \Drupal\node\Plugin\Action\UnpublishNode
* @deprecated in Drupal 8.5.x, to be removed before Drupal 9.0.0.
* Use \Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish
* instead.
*
* @see \Drupal\content_moderation\Plugin\Action\ModerationOptOutPublish
* @see https://www.drupal.org/node/2919303
*/
class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFactoryPluginInterface {
/**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformationInterface
*/
protected $moderationInfo;
/**
* ModerationOptOutUnpublishNode constructor.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moderationInfo = $moderation_info;
}
class ModerationOptOutUnpublishNode extends ModerationOptOutUnpublish {
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration, $plugin_id, $plugin_definition,
$container->get('content_moderation.moderation_information')
);
}
/**
* {@inheritdoc}
*/
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
/** @var \Drupal\node\NodeInterface $entity */
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
$result = AccessResult::forbidden();
return $return_as_object ? $result : $result->isAllowed();
}
return parent::access($entity, $account, $return_as_object);
public function __construct($configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_info, EntityTypeBundleInfoInterface $bundle_info) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $moderation_info, $bundle_info);
@trigger_error(__NAMESPACE__ . '\ModerationOptOutUnpublishNode is deprecated in Drupal 8.5.x, will be removed before Drupal 9.0.0. Use \Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublish instead. See https://www.drupal.org/node/2919303.', E_USER_DEPRECATED);
}
}
@@ -11,7 +11,7 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\ModerationInformation;
use Drupal\content_moderation\StateTransitionValidation;
use Drupal\content_moderation\StateTransitionValidationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -51,7 +51,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
/**
* Moderation state transition validation service.
*
* @var \Drupal\content_moderation\StateTransitionValidation
* @var \Drupal\content_moderation\StateTransitionValidationInterface
*/
protected $validator;
@@ -74,10 +74,10 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
* Entity type manager.
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
* Moderation information service.
* @param \Drupal\content_moderation\StateTransitionValidation $validator
* @param \Drupal\content_moderation\StateTransitionValidationInterface $validator
* Moderation state transition validation service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidationInterface $validator) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
@@ -5,6 +5,7 @@ namespace Drupal\content_moderation\Plugin\Field;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Field\FieldItemList;
use Drupal\Core\TypedData\ComputedItemListTrait;
/**
* A computed field that provides a content entity's moderation state.
@@ -14,6 +15,38 @@ use Drupal\Core\Field\FieldItemList;
*/
class ModerationStateFieldItemList extends FieldItemList {
use ComputedItemListTrait {
ensureComputedValue as traitEnsureComputedValue;
get as traitGet;
}
/**
* {@inheritdoc}
*/
protected function computeValue() {
$moderation_state = $this->getModerationStateId();
// Do not store NULL values, in the case where an entity does not have a
// moderation workflow associated with it, we do not create list items for
// the computed field.
if ($moderation_state) {
// An entity can only have a single moderation state.
$this->list[0] = $this->createItem(0, $moderation_state);
}
}
/**
* {@inheritdoc}
*/
protected function ensureComputedValue() {
// If the moderation state field is set to an empty value, always recompute
// the state. Empty is not a valid moderation state value, when none is
// present the default state is used.
if (!isset($this->list[0]) || $this->list[0]->isEmpty()) {
$this->valueComputed = FALSE;
}
$this->traitEnsureComputedValue();
}
/**
* Gets the moderation state ID linked to a content entity revision.
*
@@ -90,32 +123,7 @@ class ModerationStateFieldItemList extends FieldItemList {
if ($index !== 0) {
throw new \InvalidArgumentException('An entity can not have multiple moderation states at the same time.');
}
$this->computeModerationFieldItemList();
return isset($this->list[$index]) ? $this->list[$index] : NULL;
}
/**
* {@inheritdoc}
*/
public function getIterator() {
$this->computeModerationFieldItemList();
return parent::getIterator();
}
/**
* Recalculate the moderation field item list.
*/
protected function computeModerationFieldItemList() {
// Compute the value of the moderation state.
$index = 0;
if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) {
$moderation_state = $this->getModerationStateId();
// Do not store NULL values in the static cache.
if ($moderation_state) {
$this->list[$index] = $this->createItem($index, $moderation_state);
}
}
return $this->traitGet($index);
}
/**
@@ -133,7 +141,11 @@ class ModerationStateFieldItemList extends FieldItemList {
public function setValue($values, $notify = TRUE) {
parent::setValue($values, $notify);
if (isset($this->list[0])) {
// If the parent created a field item and if the parent should be notified
// about the change (e.g. this is not initialized with the current value),
// update the moderated entity.
if (isset($this->list[0]) && $notify) {
$this->valueComputed = TRUE;
$this->updateModeratedEntity($this->list[0]->value);
}
}
@@ -157,10 +169,9 @@ class ModerationStateFieldItemList extends FieldItemList {
/** @var \Drupal\content_moderation\ContentModerationState $current_state */
$current_state = $workflow->getTypePlugin()->getState($moderation_state_id);
// This entity is default if it is new, a new translation, the default
// revision state, or the default revision is not published.
// This entity is default if it is new, the default revision state, or the
// default revision is not published.
$update_default_revision = $entity->isNew()
|| $entity->isNewTranslation()
|| $current_state->isDefaultRevisionState()
|| !$content_moderation_info->isDefaultRevisionPublished($entity);
@@ -0,0 +1,285 @@
<?php
namespace Drupal\content_moderation\Plugin\views\filter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
use Drupal\views\Plugin\views\filter\InOperator;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a filter for the moderation state of an entity.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("moderation_state_filter")
*/
class ModerationStateFilter extends InOperator implements DependentWithRemovalPluginInterface {
/**
* {@inheritdoc}
*/
protected $valueFormType = 'select';
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The bundle information service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* The storage handler of the workflow entity type.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $workflowStorage;
/**
* Creates an instance of ModerationStateFilter.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, EntityStorageInterface $workflow_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->bundleInfo = $bundle_info;
$this->workflowStorage = $workflow_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info'),
$container->get('entity_type.manager')->getStorage('workflow')
);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), $this->entityTypeManager->getDefinition('workflow')->getListCacheTags());
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), $this->entityTypeManager->getDefinition('workflow')->getListCacheContexts());
}
/**
* {@inheritdoc}
*/
public function getValueOptions() {
if (isset($this->valueOptions)) {
return $this->valueOptions;
}
$this->valueOptions = [];
// Find all workflows which are moderating entity types of the same type the
// view is displaying.
foreach ($this->workflowStorage->loadByProperties(['type' => 'content_moderation']) as $workflow) {
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface $workflow_type */
$workflow_type = $workflow->getTypePlugin();
if (in_array($this->getEntityType(), $workflow_type->getEntityTypes(), TRUE)) {
foreach ($workflow_type->getStates() as $state_id => $state) {
$this->valueOptions[$workflow->label()][implode('-', [$workflow->id(), $state_id])] = $state->label();
}
}
}
return $this->valueOptions;
}
/**
* {@inheritdoc}
*/
public function ensureMyTable() {
if (!isset($this->tableAlias)) {
$table_alias = $this->query->ensureTable($this->table, $this->relationship);
// Filter the moderation states of the content via the
// ContentModerationState field revision table, joining either the entity
// field data or revision table. This allows filtering states against
// either the default or latest revision, depending on the relationship of
// the filter.
$left_entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
$entity_type = $this->entityTypeManager->getDefinition('content_moderation_state');
$configuration = [
'table' => $entity_type->getRevisionDataTable(),
'field' => 'content_entity_revision_id',
'left_table' => $table_alias,
'left_field' => $left_entity_type->getKey('revision'),
'extra' => [
[
'field' => 'content_entity_type_id',
'value' => $left_entity_type->id(),
],
],
];
if ($left_entity_type->isTranslatable()) {
$configuration['extra'][] = [
'field' => $entity_type->getKey('langcode'),
'left_field' => $left_entity_type->getKey('langcode'),
];
}
$join = Views::pluginManager('join')->createInstance('standard', $configuration);
$this->tableAlias = $this->query->addRelationship('content_moderation_state', $join, 'content_moderation_state_field_revision');
}
return $this->tableAlias;
}
/**
* {@inheritdoc}
*/
protected function opSimple() {
if (empty($this->value)) {
return;
}
$this->ensureMyTable();
$entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
if ($entity_type->hasKey('bundle')) {
// Get a list of bundles that are being moderated by the workflows
// configured in this filter.
$workflow_ids = $this->getWorkflowIds();
$moderated_bundles = [];
foreach ($this->bundleInfo->getBundleInfo($this->getEntityType()) as $bundle_id => $bundle) {
if (isset($bundle['workflow']) && in_array($bundle['workflow'], $workflow_ids, TRUE)) {
$moderated_bundles[] = $bundle_id;
}
}
// If we have a list of moderated bundles, restrict the query to show only
// entities in those bundles.
if ($moderated_bundles) {
$entity_base_table_alias = $this->table;
// The bundle field of an entity type is not revisionable so we need to
// join the base table.
$entity_base_table = $entity_type->getBaseTable();
$entity_revision_base_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable();
if ($this->table === $entity_revision_base_table) {
$configuration = [
'table' => $entity_base_table,
'field' => $entity_type->getKey('id'),
'left_table' => $entity_revision_base_table,
'left_field' => $entity_type->getKey('id'),
'type' => 'INNER',
];
$join = Views::pluginManager('join')->createInstance('standard', $configuration);
$entity_base_table_alias = $this->query->addRelationship($entity_base_table, $join, $entity_revision_base_table);
}
$this->query->addWhere($this->options['group'], "$entity_base_table_alias.{$entity_type->getKey('bundle')}", $moderated_bundles, 'IN');
}
// Otherwise, force the query to return an empty result.
else {
$this->query->addWhereExpression($this->options['group'], '1 = 0');
return;
}
}
if ($this->operator === 'in') {
$operator = "=";
}
else {
$operator = "<>";
}
// The values are strings composed from the workflow ID and the state ID, so
// we need to create a complex WHERE condition.
$field = new Condition('OR');
foreach ((array) $this->value as $value) {
list($workflow_id, $state_id) = explode('-', $value, 2);
$and = new Condition('AND');
$and
->condition("$this->tableAlias.workflow", $workflow_id, '=')
->condition("$this->tableAlias.$this->realField", $state_id, $operator);
$field->condition($and);
}
$this->query->addWhere($this->options['group'], $field);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
if ($workflow_ids = $this->getWorkflowIds()) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
foreach ($this->workflowStorage->loadMultiple($workflow_ids) as $workflow) {
$dependencies[$workflow->getConfigDependencyKey()][] = $workflow->getConfigDependencyName();
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
// See if this handler is responsible for any of the dependencies being
// removed. If this is the case, indicate that this handler needs to be
// removed from the View.
$remove = FALSE;
// Get all the current dependencies for this handler.
$current_dependencies = $this->calculateDependencies();
foreach ($current_dependencies as $group => $dependency_list) {
// Check if any of the handler dependencies match the dependencies being
// removed.
foreach ($dependency_list as $config_key) {
if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) {
// This handlers dependency matches a dependency being removed,
// indicate that this handler needs to be removed.
$remove = TRUE;
break 2;
}
}
}
return $remove;
}
/**
* Gets the list of Workflow IDs configured for this filter.
*
* @return array
* And array of workflow IDs.
*/
protected function getWorkflowIds() {
$workflow_ids = [];
foreach ((array) $this->value as $value) {
list($workflow_id) = explode('-', $value, 2);
$workflow_ids[] = $workflow_id;
}
return array_unique($workflow_ids);
}
}
@@ -0,0 +1,113 @@
<?php
namespace Drupal\content_moderation\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\workflows\Entity\Workflow;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for moderated revisionable entity forms.
*
* @internal
* There is ongoing discussion about how pending revisions should behave.
* The logic enabling pending revision support is likely to change once a
* decision is made.
*
* @see https://www.drupal.org/node/2940575
*/
class ContentModerationRouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* An associative array of moderated entity types keyed by ID.
*
* @var \Drupal\Core\Entity\ContentEntityTypeInterface[]
*/
protected $moderatedEntityTypes;
/**
* ContentModerationRouteSubscriber constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
foreach ($collection as $route) {
$this->setLatestRevisionFlag($route);
}
}
/**
* Ensure revisionable entities load the latest revision on entity forms.
*
* @param \Symfony\Component\Routing\Route $route
* The route object.
*/
protected function setLatestRevisionFlag(Route $route) {
if (!$entity_form = $route->getDefault('_entity_form')) {
return;
}
// Only set the flag on entity types which are revisionable.
list($entity_type) = explode('.', $entity_form, 2);
if (!isset($this->getModeratedEntityTypes()[$entity_type]) || !$this->getModeratedEntityTypes()[$entity_type]->isRevisionable()) {
return;
}
$parameters = $route->getOption('parameters') ?: [];
foreach ($parameters as &$parameter) {
if ($parameter['type'] === 'entity:' . $entity_type && !isset($parameter['load_latest_revision'])) {
$parameter['load_latest_revision'] = TRUE;
}
}
$route->setOption('parameters', $parameters);
}
/**
* Returns the moderated entity types.
*
* @return \Drupal\Core\Entity\ContentEntityTypeInterface[]
* An associative array of moderated entity types keyed by ID.
*/
protected function getModeratedEntityTypes() {
if (!isset($this->moderatedEntityTypes)) {
$entity_types = $this->entityTypeManager->getDefinitions();
/** @var \Drupal\workflows\WorkflowInterface $workflow */
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
$plugin = $workflow->getTypePlugin();
foreach ($plugin->getEntityTypes() as $entity_type_id) {
$this->moderatedEntityTypes[$entity_type_id] = $entity_types[$entity_type_id];
}
}
}
return $this->moderatedEntityTypes;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events = parent::getSubscribedEvents();
// This needs to run after that EntityResolverManager has set the route
// entity type.
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -200];
return $events;
}
}
@@ -78,7 +78,12 @@ class ViewsData {
],
],
],
'field' => ['default_formatter' => 'content_moderation_state'],
'field' => [
'id' => 'field',
'default_formatter' => 'content_moderation_state',
'field_name' => 'moderation_state',
],
'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
];
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
@@ -97,7 +102,12 @@ class ViewsData {
],
],
],
'field' => ['default_formatter' => 'content_moderation_state'],
'field' => [
'id' => 'field',
'default_formatter' => 'content_moderation_state',
'field_name' => 'moderation_state',
],
'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE],
];
}
@@ -1,8 +1,8 @@
{{ attach_library('content_moderation/content_moderation') }}
<ul class="entity-moderation-form">
<li>{{ form.current }}</li>
<li>{{ form.new_state }}</li>
<li>{{ form.revision_log }}</li>
<li>{{ form.submit }}</li>
<li class="entity-moderation-form__item">{{ form.current }}</li>
<li class="entity-moderation-form__item">{{ form.new_state }}</li>
<li class="entity-moderation-form__item">{{ form.revision_log }}</li>
<li class="entity-moderation-form__item">{{ form.submit }}</li>
</ul>
{{ form|without('current', 'new_state', 'revision_log', 'submit') }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,804 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* Content for the update path test in #2941736.
*
* @see \Drupal\Tests\content_moderation\Functional\DefaultContentModerationStateRevisionUpdateTest.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->insert('block_content')
->fields(array(
'id',
'revision_id',
'type',
'uuid',
'langcode',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'type' => 'test_block_content',
'uuid' => '811fac6c-8184-4de5-99eb-9e70d28709f4',
'langcode' => 'en',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'type' => 'test_block_content',
'uuid' => 'b89f025c-0538-4075-bd8e-96acf74211c9',
'langcode' => 'en',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'type' => 'test_block_content',
'uuid' => '62e428e1-88a6-478c-a8c6-a554ca2332ae',
'langcode' => 'en',
))
->execute();
$connection->insert('block_content_field_data')
->fields(array(
'id',
'revision_id',
'type',
'langcode',
'info',
'changed',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'type' => 'test_block_content',
'langcode' => 'en',
'info' => 'draft pending revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'type' => 'test_block_content',
'langcode' => 'en',
'info' => 'published default revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'type' => 'test_block_content',
'langcode' => 'en',
'info' => 'archived default revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('block_content_field_revision')
->fields(array(
'id',
'revision_id',
'langcode',
'info',
'changed',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
'info' => 'draft pending revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'info' => 'draft pending revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'langcode' => 'en',
'info' => 'published default revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '4',
'langcode' => 'en',
'info' => 'archived default revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'langcode' => 'en',
'info' => 'archived default revision',
'changed' => '1517725800',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('block_content_revision')
->fields(array(
'id',
'revision_id',
'langcode',
'revision_user',
'revision_created',
'revision_log',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
'revision_user' => NULL,
'revision_created' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'revision_user' => NULL,
'revision_created' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'id' => '2',
'revision_id' => '3',
'langcode' => 'en',
'revision_user' => NULL,
'revision_created' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'id' => '3',
'revision_id' => '4',
'langcode' => 'en',
'revision_user' => NULL,
'revision_created' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'id' => '3',
'revision_id' => '5',
'langcode' => 'en',
'revision_user' => NULL,
'revision_created' => '1517725800',
'revision_log' => NULL,
))
->execute();
$connection->delete('config')
->condition('name', ['workflows.workflow.editorial'], 'IN')
->execute();
$connection->insert('config')
->fields(array(
'collection',
'name',
'data',
))
->values(array(
'collection' => '',
'name' => 'block_content.type.test_block_content',
'data' => 'a:8:{s:4:"uuid";s:36:"966baba6-525e-48fe-b8c5-a5f131b1857f";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:0:{}s:2:"id";s:18:"test_block_content";s:5:"label";s:18:"Test Block Content";s:8:"revision";N;s:11:"description";N;}',
))
->values(array(
'collection' => '',
'name' => 'workflows.workflow.editorial',
'data' => 'a:9:{s:4:"uuid";s:36:"08b548c7-ff59-468b-9347-7d697680d035";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:37:"block_content.type.test_block_content";i:1;s:17:"node.type.article";}s:6:"module";a:1:{i:0;s:18:"content_moderation";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"T_JxNjYlfoRBi7Bj1zs5Xv9xv1btuBkKp5C1tNrjMhI";}s:2:"id";s:9:"editorial";s:5:"label";s:9:"Editorial";s:4:"type";s:18:"content_moderation";s:13:"type_settings";a:3:{s:6:"states";a:3:{s:8:"archived";a:4:{s:5:"label";s:8:"Archived";s:6:"weight";i:5;s:9:"published";b:0;s:16:"default_revision";b:1;}s:5:"draft";a:4:{s:5:"label";s:5:"Draft";s:9:"published";b:0;s:16:"default_revision";b:0;s:6:"weight";i:-5;}s:9:"published";a:4:{s:5:"label";s:9:"Published";s:9:"published";b:1;s:16:"default_revision";b:1;s:6:"weight";i:0;}}s:11:"transitions";a:5:{s:7:"archive";a:4:{s:5:"label";s:7:"Archive";s:4:"from";a:1:{i:0;s:9:"published";}s:2:"to";s:8:"archived";s:6:"weight";i:2;}s:14:"archived_draft";a:4:{s:5:"label";s:16:"Restore to Draft";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:5:"draft";s:6:"weight";i:3;}s:18:"archived_published";a:4:{s:5:"label";s:7:"Restore";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:9:"published";s:6:"weight";i:4;}s:16:"create_new_draft";a:4:{s:5:"label";s:16:"Create New Draft";s:2:"to";s:5:"draft";s:6:"weight";i:0;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}s:7:"publish";a:4:{s:5:"label";s:7:"Publish";s:2:"to";s:9:"published";s:6:"weight";i:1;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}}s:12:"entity_types";a:2:{s:13:"block_content";a:1:{i:0;s:18:"test_block_content";}s:4:"node";a:1:{i:0;s:7:"article";}}}}',
))
->execute();
$connection->insert('content_moderation_state')
->fields(array(
'id',
'revision_id',
'uuid',
'langcode',
))
->values(array(
'id' => '1',
'revision_id' => '2',
'uuid' => '3ce04732-f65f-4937-aa49-821f5842ae06',
'langcode' => 'en',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'uuid' => 'a6507b55-3001-4748-8d32-f4fa47319754',
'langcode' => 'en',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'uuid' => '112d2bd2-552b-4e2f-9a6d-526740ba1b38',
'langcode' => 'en',
))
->values(array(
'id' => '4',
'revision_id' => '7',
'uuid' => 'a85d0d06-e046-4509-b9b4-75d78dcdd91e',
'langcode' => 'en',
))
->values(array(
'id' => '5',
'revision_id' => '8',
'uuid' => '3797f5de-116b-4d75-b7e3-5206e6f97c41',
'langcode' => 'en',
))
->values(array(
'id' => '6',
'revision_id' => '10',
'uuid' => '8d9b11c1-8ddf-4c61-bb8d-9ac724e28d9e',
'langcode' => 'en',
))
->execute();
$connection->insert('content_moderation_state_field_data')
->fields(array(
'id',
'revision_id',
'langcode',
'uid',
'workflow',
'moderation_state',
'content_entity_type_id',
'content_entity_id',
'content_entity_revision_id',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'draft',
'content_entity_type_id' => 'node',
'content_entity_id' => '1',
'content_entity_revision_id' => '2',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'node',
'content_entity_id' => '2',
'content_entity_revision_id' => '3',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'archived',
'content_entity_type_id' => 'node',
'content_entity_id' => '3',
'content_entity_revision_id' => '5',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '4',
'revision_id' => '7',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'draft',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '1',
'content_entity_revision_id' => '2',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '5',
'revision_id' => '8',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '2',
'content_entity_revision_id' => '3',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '6',
'revision_id' => '10',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'archived',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '3',
'content_entity_revision_id' => '5',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('content_moderation_state_field_revision')
->fields(array(
'id',
'revision_id',
'langcode',
'uid',
'workflow',
'moderation_state',
'content_entity_type_id',
'content_entity_id',
'content_entity_revision_id',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'node',
'content_entity_id' => '1',
'content_entity_revision_id' => '1',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'draft',
'content_entity_type_id' => 'node',
'content_entity_id' => '1',
'content_entity_revision_id' => '2',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'node',
'content_entity_id' => '2',
'content_entity_revision_id' => '3',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '4',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'node',
'content_entity_id' => '3',
'content_entity_revision_id' => '4',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'archived',
'content_entity_type_id' => 'node',
'content_entity_id' => '3',
'content_entity_revision_id' => '5',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '4',
'revision_id' => '6',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '1',
'content_entity_revision_id' => '1',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '4',
'revision_id' => '7',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'draft',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '1',
'content_entity_revision_id' => '2',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '5',
'revision_id' => '8',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '2',
'content_entity_revision_id' => '3',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '6',
'revision_id' => '9',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'published',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '3',
'content_entity_revision_id' => '4',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'id' => '6',
'revision_id' => '10',
'langcode' => 'en',
'uid' => '0',
'workflow' => 'editorial',
'moderation_state' => 'archived',
'content_entity_type_id' => 'block_content',
'content_entity_id' => '3',
'content_entity_revision_id' => '5',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('content_moderation_state_revision')
->fields(array(
'id',
'revision_id',
'langcode',
))
->values(array(
'id' => '1',
'revision_id' => '1',
'langcode' => 'en',
))
->values(array(
'id' => '1',
'revision_id' => '2',
'langcode' => 'en',
))
->values(array(
'id' => '2',
'revision_id' => '3',
'langcode' => 'en',
))
->values(array(
'id' => '3',
'revision_id' => '4',
'langcode' => 'en',
))
->values(array(
'id' => '3',
'revision_id' => '5',
'langcode' => 'en',
))
->values(array(
'id' => '4',
'revision_id' => '6',
'langcode' => 'en',
))
->values(array(
'id' => '4',
'revision_id' => '7',
'langcode' => 'en',
))
->values(array(
'id' => '5',
'revision_id' => '8',
'langcode' => 'en',
))
->values(array(
'id' => '6',
'revision_id' => '9',
'langcode' => 'en',
))
->values(array(
'id' => '6',
'revision_id' => '10',
'langcode' => 'en',
))
->execute();
$connection->insert('key_value')
->fields(array(
'collection',
'name',
'value',
))
->values(array(
'collection' => 'config.entity.key_store.block_content_type',
'name' => 'uuid:966baba6-525e-48fe-b8c5-a5f131b1857f',
'value' => 'a:1:{i:0;s:37:"block_content.type.test_block_content";}',
))
->execute();
$connection->insert('node')
->fields(array(
'nid',
'vid',
'type',
'uuid',
'langcode',
))
->values(array(
'nid' => '1',
'vid' => '1',
'type' => 'article',
'uuid' => '11143847-fe18-4808-a797-8b15966adf4c',
'langcode' => 'en',
))
->values(array(
'nid' => '2',
'vid' => '3',
'type' => 'article',
'uuid' => '336e6941-9340-419e-a763-65d4c11ea031',
'langcode' => 'en',
))
->values(array(
'nid' => '3',
'vid' => '5',
'type' => 'article',
'uuid' => '3eebe337-f977-4a32-94d2-4095947f125d',
'langcode' => 'en',
))
->execute();
$connection->insert('node_field_data')
->fields(array(
'nid',
'vid',
'type',
'langcode',
'status',
'title',
'uid',
'created',
'changed',
'promote',
'sticky',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'nid' => '1',
'vid' => '1',
'type' => 'article',
'langcode' => 'en',
'status' => '1',
'title' => 'draft pending revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '2',
'vid' => '3',
'type' => 'article',
'langcode' => 'en',
'status' => '1',
'title' => 'published default revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '3',
'vid' => '5',
'type' => 'article',
'langcode' => 'en',
'status' => '0',
'title' => 'archived default revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('node_field_revision')
->fields(array(
'nid',
'vid',
'langcode',
'status',
'title',
'uid',
'created',
'changed',
'promote',
'sticky',
'default_langcode',
'revision_translation_affected',
))
->values(array(
'nid' => '1',
'vid' => '1',
'langcode' => 'en',
'status' => '1',
'title' => 'draft pending revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '1',
'vid' => '2',
'langcode' => 'en',
'status' => '0',
'title' => 'draft pending revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '2',
'vid' => '3',
'langcode' => 'en',
'status' => '1',
'title' => 'published default revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '3',
'vid' => '4',
'langcode' => 'en',
'status' => '1',
'title' => 'archived default revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->values(array(
'nid' => '3',
'vid' => '5',
'langcode' => 'en',
'status' => '0',
'title' => 'archived default revision',
'uid' => '0',
'created' => '1517725800',
'changed' => '1517725800',
'promote' => '1',
'sticky' => '0',
'default_langcode' => '1',
'revision_translation_affected' => '1',
))
->execute();
$connection->insert('node_revision')
->fields(array(
'nid',
'vid',
'langcode',
'revision_uid',
'revision_timestamp',
'revision_log',
))
->values(array(
'nid' => '1',
'vid' => '1',
'langcode' => 'en',
'revision_uid' => '0',
'revision_timestamp' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'nid' => '1',
'vid' => '2',
'langcode' => 'en',
'revision_uid' => '0',
'revision_timestamp' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'nid' => '2',
'vid' => '3',
'langcode' => 'en',
'revision_uid' => '0',
'revision_timestamp' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'nid' => '3',
'vid' => '4',
'langcode' => 'en',
'revision_uid' => '0',
'revision_timestamp' => '1517725800',
'revision_log' => NULL,
))
->values(array(
'nid' => '3',
'vid' => '5',
'langcode' => 'en',
'revision_uid' => '0',
'revision_timestamp' => '1517725800',
'revision_log' => NULL,
))
->execute();
@@ -8,8 +8,8 @@ dependencies:
- content_moderation
- node
# Information added by Drupal.org packaging script on 2018-01-03
version: '8.4.4'
# Information added by Drupal.org packaging script on 2018-03-07
version: '8.5.0'
core: '8.x'
project: 'drupal'
datestamp: 1515021228
datestamp: 1520457826
@@ -0,0 +1,205 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- node
- user
id: test_content_moderation_field_state_test
label: test_content_moderation_field_state_test
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 10
offset: 0
style:
type: default
row:
type: fields
options:
default_field_elements: true
inline: { }
separator: ''
hide_empty: false
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
moderation_state:
id: moderation_state
table: node_field_data
field: moderation_state
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: content_moderation_state
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
plugin_id: field
filters: { }
sorts: { }
title: test_content_moderation_field_state_test
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: test-content-moderation-field-state-test
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }
@@ -0,0 +1,261 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- node
- user
id: test_content_moderation_state_filter_base_table
label: test_content_moderation_state_filter_base_table
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: nid
plugin_id: field
filters:
moderation_state:
id: moderation_state
table: node_field_data
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: moderation_state_op
label: 'Default Revision State'
description: ''
use_operator: false
operator: moderation_state_op
identifier: default_revision_state
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
moderation_state_1:
id: moderation_state_1
table: node_field_data
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: 'not empty'
value: { }
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
sorts:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: nid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:workflow_list'
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: filter-test-path
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:workflow_list'
@@ -0,0 +1,223 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- node
- user
id: test_content_moderation_state_filter_base_table_filter_on_revision
label: test_content_moderation_state_filter_base_table_filter_on_revision
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: false
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: nid
plugin_id: field
filters:
moderation_state:
id: moderation_state
table: node_field_revision
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: moderation_state_op
label: 'Moderation state'
description: ''
use_operator: false
operator: moderation_state_op
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
sorts:
nid:
id: nid
table: node_field_data
field: nid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: nid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:workflow_list'
page_1:
display_plugin: page
id: page_1
display_title: Page
position: 1
display_options:
display_extenders: { }
path: filter-on-revision-test-path
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:workflow_list'
@@ -0,0 +1,168 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- entity_test
id: test_content_moderation_state_filter_entity_test
label: test_content_moderation_state_filter_entity_test
module: views
description: ''
tag: ''
base_table: entity_test_no_bundle
base_field: id
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
entity_id:
id: entity_id
table: content_revision_tracker
field: entity_id
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
plugin_id: standard
filters:
moderation_state:
id: moderation_state
table: entity_test_no_bundle
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: moderation_state_op
label: 'Moderation state'
description: ''
use_operator: false
operator: moderation_state_op
identifier: moderation_state
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: entity_test_no_bundle
plugin_id: moderation_state_filter
sorts: { }
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_interface'
- url
tags: { }
@@ -0,0 +1,218 @@
langcode: en
status: true
dependencies:
module:
- content_moderation
- user
id: test_content_moderation_state_filter_revision_table
label: test_content_moderation_state_filter_revision_table
module: views
description: ''
tag: ''
base_table: node_field_revision
base_field: vid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'view all revisions'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
nid:
id: nid
table: node_field_revision
field: nid
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: number_integer
settings:
thousand_separator: ''
prefix_suffix: true
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
entity_type: node
entity_field: nid
plugin_id: field
filters:
moderation_state:
id: moderation_state
table: node_field_revision
field: moderation_state
relationship: none
group_type: group
admin_label: ''
operator: in
value: { }
group: 1
exposed: true
expose:
operator_id: moderation_state_op
label: 'Moderation state'
description: ''
use_operator: false
operator: moderation_state_op
identifier: moderation_state
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
reduce: false
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
entity_type: node
plugin_id: moderation_state_filter
sorts:
vid:
id: vid
table: node_field_revision
field: vid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: vid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags: { }
@@ -8,9 +8,10 @@ dependencies:
- content_moderation
- node
- views
- entity_test
# Information added by Drupal.org packaging script on 2018-01-03
version: '8.4.4'
# Information added by Drupal.org packaging script on 2018-03-07
version: '8.5.0'
core: '8.x'
project: 'drupal'
datestamp: 1515021228
datestamp: 1520457826
@@ -0,0 +1,101 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Test updating the ContentModerationState entity default revisions.
*
* @group Update
* @see content_moderation_post_update_update_cms_default_revisions
*/
class DefaultContentModerationStateRevisionUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
__DIR__ . '/../../fixtures/update/drupal-8.4.0-content_moderation_installed.php',
__DIR__ . '/../../fixtures/update/drupal-8.default-cms-entity-id-2941736.php',
];
}
/**
* Test updating the default revision.
*/
public function testUpdateDefaultRevision() {
$this->runUpdates();
foreach (['node', 'block_content'] as $entity_type_id) {
$draft_pending_revision = $this->getEntityByLabel($entity_type_id, 'draft pending revision');
$this->assertFalse($draft_pending_revision->isLatestRevision());
$this->assertCompositeEntityMatchesDefaultRevisionId($draft_pending_revision);
$published_default_revision = $this->getEntityByLabel($entity_type_id, 'published default revision');
$this->assertTrue($published_default_revision->isLatestRevision());
$this->assertCompositeEntityMatchesDefaultRevisionId($published_default_revision);
$archived_default_revision = $this->getEntityByLabel($entity_type_id, 'archived default revision');
$this->assertTrue($archived_default_revision->isLatestRevision());
$this->assertCompositeEntityMatchesDefaultRevisionId($archived_default_revision);
}
}
/**
* Assert for the given entity, the default revision ID matches.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity to use for the assertion.
*/
protected function assertCompositeEntityMatchesDefaultRevisionId(ContentEntityInterface $entity) {
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_list = $entity_type_manager->getStorage('content_moderation_state')
->loadByProperties([
'content_entity_type_id' => $entity->getEntityTypeId(),
'content_entity_id' => $entity->id(),
]);
$content_moderation_state_entity = array_shift($entity_list);
$this->assertEquals($entity->getLoadedRevisionId(), $content_moderation_state_entity->content_entity_revision_id->value);
// Check that the data table records were updated correctly.
/** @var \Drupal\Core\Database\Connection $database */
$database = $this->container->get('database');
$query = 'SELECT * FROM {content_moderation_state_field_data} WHERE id = :id';
$records = $database->query($query, [':id' => $content_moderation_state_entity->id()])
->fetchAllAssoc('langcode');
foreach ($records as $langcode => $record) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $translation */
$translation = $content_moderation_state_entity->getTranslation($langcode);
foreach ((array) $record as $field_name => $value) {
if ($translation->hasField($field_name)) {
$items = $translation->get($field_name)->getValue();
$this->assertEquals(current($items[0]), $value);
}
}
}
}
/**
* Load an entity by label.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $label
* The label of the entity to load.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The loaded entity.
*/
protected function getEntityByLabel($entity_type_id, $label) {
$entity_type_manager = $this->container->get('entity_type.manager');
$label_field = $entity_type_manager->getDefinition($entity_type_id)->getKey('label');
$entity_list = $entity_type_manager->getStorage($entity_type_id)
->loadByProperties([$label_field => $label]);
return array_shift($entity_list);
}
}
@@ -0,0 +1,129 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Tests moderated content administration page functionality.
*
* @group content_moderation
*/
class ModeratedContentViewTest extends BrowserTestBase {
/**
* A user with permission to bypass access content.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
public static $modules = ['content_moderation', 'node', 'views'];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page'])->save();
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save();
$this->drupalCreateContentType(['type' => 'unmoderated_type', 'name' => 'Unmoderated type'])->save();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
$workflow->save();
$this->adminUser = $this->drupalCreateUser(['access administration pages', 'view any unpublished content', 'administer nodes', 'bypass node access']);
}
/**
* Tests the moderated content page.
*/
public function testModeratedContentPage() {
$assert_sesison = $this->assertSession();
$this->drupalLogin($this->adminUser);
// Use an explicit changed time to ensure the expected order in the content
// admin listing. We want these to appear in the table in the same order as
// they appear in the following code, and the 'moderated_content' view has a
// table style configuration with a default sort on the 'changed' field
// descending.
$time = \Drupal::time()->getRequestTime();
$excluded_nodes['published_page'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'published']);
$excluded_nodes['published_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']);
$excluded_nodes['unmoderated_type'] = $this->drupalCreateNode(['type' => 'unmoderated_type', 'changed' => $time--]);
$excluded_nodes['unmoderated_type']->setNewRevision(TRUE);
$excluded_nodes['unmoderated_type']->isDefaultRevision(FALSE);
$excluded_nodes['unmoderated_type']->changed->value = $time--;
$excluded_nodes['unmoderated_type']->save();
$nodes['published_then_draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published', 'title' => 'first article - published']);
$nodes['published_then_draft_article']->setNewRevision(TRUE);
$nodes['published_then_draft_article']->setTitle('first article - draft');
$nodes['published_then_draft_article']->moderation_state->value = 'draft';
$nodes['published_then_draft_article']->changed->value = $time--;
$nodes['published_then_draft_article']->save();
$nodes['published_then_archived_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']);
$nodes['published_then_archived_article']->setNewRevision(TRUE);
$nodes['published_then_archived_article']->moderation_state->value = 'archived';
$nodes['published_then_archived_article']->changed->value = $time--;
$nodes['published_then_archived_article']->save();
$nodes['draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'draft']);
$nodes['draft_page_1'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'draft']);
$nodes['draft_page_2'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time, 'moderation_state' => 'draft']);
// Verify view, edit, and delete links for any content.
$this->drupalGet('admin/content/moderated');
$assert_sesison->statusCodeEquals(200);
// Check that nodes with pending revisions appear in the view.
$node_type_labels = $this->xpath('//td[contains(@class, "views-field-type")]');
$delta = 0;
foreach ($nodes as $node) {
$assert_sesison->linkByHrefExists('node/' . $node->id());
$assert_sesison->linkByHrefExists('node/' . $node->id() . '/edit');
$assert_sesison->linkByHrefExists('node/' . $node->id() . '/delete');
// Verify that we can see the content type label.
$this->assertEquals($node->type->entity->label(), trim($node_type_labels[$delta]->getText()));
$delta++;
}
// Check that nodes that are not moderated or do not have a pending revision
// do not appear in the view.
foreach ($excluded_nodes as $node) {
$assert_sesison->linkByHrefNotExists('node/' . $node->id());
}
// Check that the latest revision is displayed.
$assert_sesison->pageTextContains('first article - draft');
$assert_sesison->pageTextNotContains('first article - published');
// Verify filtering by moderation state.
$this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft']]);
$assert_sesison->linkByHrefExists('node/' . $nodes['published_then_draft_article']->id() . '/edit');
$assert_sesison->linkByHrefExists('node/' . $nodes['draft_article']->id() . '/edit');
$assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
$assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
$assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit');
// Verify filtering by moderation state and content type.
$this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft', 'type' => 'page']]);
$assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit');
$assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_2']->id() . '/edit');
$assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_draft_article']->id() . '/edit');
$assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit');
$assert_sesison->linkByHrefNotExists('node/' . $nodes['draft_article']->id() . '/edit');
}
}
@@ -2,7 +2,9 @@
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\workflows\Entity\Workflow;
use Drupal\Core\Url;
/**
* Tests the moderation form, specifically on nodes.
@@ -79,6 +81,21 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->assertResponse(200);
$this->assertField('edit-new-state', 'The node view page has a moderation form.');
// Preview the draft.
$this->drupalPostForm($edit_path, [
'body[0][value]' => 'Second version of the content.',
'moderation_state[0][state]' => 'draft',
], t('Preview'));
// The preview view should not have a moderation form.
$preview_url = Url::fromRoute('entity.node.preview', [
'node_preview' => $node->uuid(),
'view_mode_id' => 'full',
]);
$this->assertResponse(200);
$this->assertUrl($preview_url);
$this->assertNoField('edit-new-state', 'The node preview page has no moderation form.');
// The latest version page should not show, because there is still no
// pending revision.
$this->drupalGet($latest_version_path);
@@ -295,14 +312,6 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalGet($latest_version_path, ['language' => $french]);
$this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]'));
// It should not be possible to add a new english revision.
$this->drupalGet($edit_path);
$this->assertSession()->fieldNotExists('moderation_state[0][state]');
$this->assertSession()->pageTextContains('Unable to save this Moderated content.');
$this->clickLink('Publish');
$this->assertSession()->fieldValueEquals('body[0][value]', 'Third version of the content.');
$this->drupalGet($edit_path);
$this->clickLink('Delete');
$this->assertSession()->buttonExists('Delete');
@@ -323,7 +332,7 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalGet($latest_version_path, ['language' => $french]);
$this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
// Now we can publish the english (revision 5).
// Publish the English pending revision (revision 5).
$this->drupalGet($edit_path);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -336,13 +345,13 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalGet($latest_version_path);
$this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
// Make sure we're allowed to create a pending french revision.
// Make sure we are allowed to create a pending French revision.
$this->drupalGet($edit_path, ['language' => $french]);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
$this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
// Add a english pending revision (revision 6).
// Add an English pending revision (revision 6).
$this->drupalGet($edit_path);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -354,16 +363,10 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalGet($latest_version_path);
$this->assertTrue($this->xpath('//ul[@class="entity-moderation-form"]'));
// Make sure we're not allowed to create a pending french revision.
$this->drupalGet($edit_path, ['language' => $french]);
$this->assertSession()->fieldNotExists('moderation_state[0][state]');
$this->assertSession()->pageTextContains('Unable to save this Moderated content.');
$this->drupalGet($latest_version_path, ['language' => $french]);
$this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
// We should be able to publish the english pending revision (revision 7)
// Publish the English pending revision (revision 7)
$this->drupalGet($edit_path);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
@@ -376,45 +379,18 @@ class ModerationFormTest extends ModerationStateTestBase {
$this->drupalGet($latest_version_path);
$this->assertFalse($this->xpath('//ul[@class="entity-moderation-form"]'));
// Make sure we're allowed to create a pending french revision.
// Make sure we are allowed to create a pending French revision.
$this->drupalGet($edit_path, ['language' => $french]);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
$this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
// Make sure we're allowed to create a pending english revision.
// Make sure we are allowed to create a pending English revision.
$this->drupalGet($edit_path);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
$this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
// Create new moderated content. (revision 1).
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'Second moderated content',
'body[0][value]' => 'First version of the content.',
'moderation_state[0][state]' => 'published',
], t('Save'));
$node = $this->drupalGetNodeByTitle('Second moderated content');
$this->assertTrue($node->language(), 'en');
$edit_path = sprintf('node/%d/edit', $node->id());
$translate_path = sprintf('node/%d/translations/add/en/fr', $node->id());
// Add a pending revision (revision 2).
$this->drupalGet($edit_path);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
$this->assertSession()->optionExists('moderation_state[0][state]', 'published');
$this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
$this->drupalPostForm(NULL, [
'body[0][value]' => 'Second version of the content.',
'moderation_state[0][state]' => 'draft',
], t('Save'));
// It shouldn't be possible to translate as we have a pending revision.
$this->drupalGet($translate_path);
$this->assertSession()->fieldNotExists('moderation_state[0][state]');
$this->assertSession()->pageTextContains('Unable to save this Moderated content.');
// Create new moderated content (revision 1).
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'Third moderated content',
@@ -444,11 +420,6 @@ class ModerationFormTest extends ModerationStateTestBase {
'moderation_state[0][state]' => 'draft',
], t('Save (this translation)'));
// Editing the original translation should not be possible.
$this->drupalGet($edit_path);
$this->assertSession()->fieldNotExists('moderation_state[0][state]');
$this->assertSession()->pageTextContains('Unable to save this Moderated content.');
// Updating and publishing the french translation is still possible.
$this->drupalGet($edit_path, ['language' => $french]);
$this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
@@ -468,6 +439,23 @@ class ModerationFormTest extends ModerationStateTestBase {
], t('Save (this translation)'));
}
/**
* Test the moderation_state field when an alternative widget is set.
*/
public function testAlternativeModerationStateWidget() {
$entity_form_display = EntityFormDisplay::load('node.moderated_content.default');
$entity_form_display->setComponent('moderation_state', [
'type' => 'string_textfield',
'region' => 'content',
]);
$entity_form_display->save();
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'Test content',
'moderation_state[0][value]' => 'published',
], 'Save');
$this->assertSession()->pageTextContains('Moderated content Test content has been created.');
}
/**
* Tests that workflows and states can not be deleted if they are in use.
*
@@ -2,6 +2,8 @@
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\node\NodeInterface;
/**
* Test content_moderation functionality with localization and translation.
*
@@ -22,19 +24,23 @@ class ModerationLocaleTest extends ModerationStateTestBase {
];
/**
* Tests article translations can be moderated separately.
* {@inheritdoc}
*/
public function testTranslateModeratedContent() {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->rootUser);
// Enable moderation on Article node type.
$this->createContentTypeFromUi('Article', 'article', TRUE);
// Add French language.
$edit = [
'predefined_langcode' => 'fr',
];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// Add French and Italian languages.
foreach (['fr', 'it'] as $langcode) {
$edit = [
'predefined_langcode' => $langcode,
];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
}
// Enable content translation on articles.
$this->drupalGet('admin/config/regional/content-language');
@@ -48,7 +54,12 @@ class ModerationLocaleTest extends ModerationStateTestBase {
// Adding languages requires a container rebuild in the test running
// environment so that multilingual services are used.
$this->rebuildContainer();
}
/**
* Tests article translations can be moderated separately.
*/
public function testTranslateModeratedContent() {
// Create a published article in English.
$edit = [
'title[0][value]' => 'Published English node',
@@ -191,4 +202,358 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->assertFalse($french_node->isPublished());
}
/**
* Tests that individual translations can be moderated independently.
*/
public function testLanguageIndependentContentModeration() {
// Create a published article in English (revision 1).
$this->drupalGet('node/add/article');
$node = $this->submitNodeForm('Test 1.1 EN', 'published');
$this->assertNotLatestVersionPage($node);
$edit_path = $node->toUrl('edit-form');
$translate_path = $node->toUrl('drupal:content-translation-overview');
// Create a new English draft (revision 2).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 1.2 EN', 'draft', TRUE);
$this->assertLatestVersionPage($node);
// Add a French translation draft (revision 3).
$this->drupalGet($translate_path);
$this->clickLink(t('Add'));
$this->submitNodeForm('Test 1.3 FR', 'draft');
$fr_node = $this->loadTranslation($node, 'fr');
$this->assertLatestVersionPage($fr_node);
$this->assertModerationForm($node);
// Add an Italian translation draft (revision 4).
$this->drupalGet($translate_path);
$this->clickLink(t('Add'));
$this->submitNodeForm('Test 1.4 IT', 'draft');
$it_node = $this->loadTranslation($node, 'it');
$this->assertLatestVersionPage($it_node);
$this->assertModerationForm($node);
$this->assertModerationForm($fr_node);
// Publish the English draft (revision 5).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 1.5 EN', 'published', TRUE);
$this->assertNotLatestVersionPage($node);
$this->assertModerationForm($fr_node);
$this->assertModerationForm($it_node);
// Publish the Italian draft (revision 6).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 2);
$this->submitNodeForm('Test 1.6 IT', 'published');
$this->assertNotLatestVersionPage($it_node);
$this->assertNoModerationForm($node);
$this->assertModerationForm($fr_node);
// Publish the French draft (revision 7).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 1);
$this->submitNodeForm('Test 1.7 FR', 'published');
$this->assertNotLatestVersionPage($fr_node);
$this->assertNoModerationForm($node);
$this->assertNoModerationForm($it_node);
// Create an Italian draft (revision 8).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 2);
$this->submitNodeForm('Test 1.8 IT', 'draft');
$this->assertLatestVersionPage($it_node);
$this->assertNoModerationForm($node);
$this->assertNoModerationForm($fr_node);
// Create a French draft (revision 9).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 1);
$this->submitNodeForm('Test 1.9 FR', 'draft');
$this->assertLatestVersionPage($fr_node);
$this->assertNoModerationForm($node);
$this->assertModerationForm($it_node);
// Create an English draft (revision 10).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 1.10 EN', 'draft');
$this->assertLatestVersionPage($node);
$this->assertModerationForm($fr_node);
$this->assertModerationForm($it_node);
// Now start from a draft article in English (revision 1).
$this->drupalGet('node/add/article');
$node2 = $this->submitNodeForm('Test 2.1 EN', 'draft', TRUE);
$this->assertNotLatestVersionPage($node2, TRUE);
$edit_path = $node2->toUrl('edit-form');
$translate_path = $node2->toUrl('drupal:content-translation-overview');
// Add a French translation (revision 2).
$this->drupalGet($translate_path);
$this->clickLink(t('Add'));
$this->submitNodeForm('Test 2.2 FR', 'draft');
$fr_node2 = $this->loadTranslation($node2, 'fr');
$this->assertNotLatestVersionPage($fr_node2, TRUE);
$this->assertModerationForm($node2, FALSE);
// Add an Italian translation (revision 3).
$this->drupalGet($translate_path);
$this->clickLink(t('Add'));
$this->submitNodeForm('Test 2.3 IT', 'draft');
$it_node2 = $this->loadTranslation($node2, 'it');
$this->assertNotLatestVersionPage($it_node2, TRUE);
$this->assertModerationForm($node2, FALSE);
$this->assertModerationForm($fr_node2, FALSE);
// Publish the English draft (revision 4).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 2.4 EN', 'published', TRUE);
$this->assertNotLatestVersionPage($node2);
$this->assertModerationForm($fr_node2, FALSE);
$this->assertModerationForm($it_node2, FALSE);
// Publish the Italian draft (revision 5).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 2);
$this->submitNodeForm('Test 2.5 IT', 'published');
$this->assertNotLatestVersionPage($it_node2);
$this->assertNoModerationForm($node2);
$this->assertModerationForm($fr_node2, FALSE);
// Publish the French draft (revision 6).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 1);
$this->submitNodeForm('Test 2.6 FR', 'published');
$this->assertNotLatestVersionPage($fr_node2);
$this->assertNoModerationForm($node2);
$this->assertNoModerationForm($it_node2);
// Now that all revision translations are published, verify that the
// moderation form is never displayed on revision pages.
/** @var \Drupal\node\NodeStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('node');
foreach (range(11, 16) as $revision_id) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $storage->loadRevision($revision_id);
foreach ($revision->getTranslationLanguages() as $langcode => $language) {
if ($revision->isRevisionTranslationAffected()) {
$this->drupalGet($revision->toUrl('revision'));
$this->assertFalse($this->hasModerationForm(), 'Moderation form is not displayed correctly for revision ' . $revision_id);
break;
}
}
}
// Create an Italian draft (revision 7).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 2);
$this->submitNodeForm('Test 2.7 IT', 'draft');
$this->assertLatestVersionPage($it_node2);
$this->assertNoModerationForm($node2);
$this->assertNoModerationForm($fr_node2);
// Create a French draft (revision 8).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 1);
$this->submitNodeForm('Test 2.8 FR', 'draft');
$this->assertLatestVersionPage($fr_node2);
$this->assertNoModerationForm($node2);
$this->assertModerationForm($it_node2);
// Create an English draft (revision 9).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 2.9 EN', 'draft', TRUE);
$this->assertLatestVersionPage($node2);
$this->assertModerationForm($fr_node2);
$this->assertModerationForm($it_node2);
// Now publish a draft in another language first and verify that the
// moderation form is not displayed on the English node view page.
$this->drupalGet('node/add/article');
$node3 = $this->submitNodeForm('Test 3.1 EN', 'published');
$this->assertNotLatestVersionPage($node3);
$edit_path = $node3->toUrl('edit-form');
$translate_path = $node3->toUrl('drupal:content-translation-overview');
// Create an English draft (revision 2).
$this->drupalGet($edit_path);
$this->submitNodeForm('Test 3.2 EN', 'draft', TRUE);
$this->assertLatestVersionPage($node3);
// Add a French translation (revision 3).
$this->drupalGet($translate_path);
$this->clickLink(t('Add'));
$this->submitNodeForm('Test 3.3 FR', 'draft');
$fr_node3 = $this->loadTranslation($node3, 'fr');
$this->assertLatestVersionPage($fr_node3);
$this->assertModerationForm($node3);
// Publish the French draft (revision 4).
$this->drupalGet($translate_path);
$this->clickLink(t('Edit'), 1);
$this->submitNodeForm('Test 3.4 FR', 'published');
$this->assertNotLatestVersionPage($fr_node3);
$this->assertModerationForm($node3);
}
/**
* Checks that new translation values are populated properly.
*/
public function testNewTranslationSourceValues() {
// Create a published article in Italian (revision 1).
$this->drupalGet('node/add/article');
$node = $this->submitNodeForm('Test 1.1 IT', 'published', TRUE, 'it');
$this->assertNotLatestVersionPage($node);
// Create a new draft (revision 2).
$this->drupalGet($node->toUrl('edit-form'));
$this->submitNodeForm('Test 1.2 IT', 'draft', TRUE);
$this->assertLatestVersionPage($node);
// Create an English draft (revision 3) and verify that the Italian draft
// values are used as source values.
$url = $node->toUrl('drupal:content-translation-add');
$url->setRouteParameter('source', 'it');
$url->setRouteParameter('target', 'en');
$this->drupalGet($url);
$this->assertSession()->pageTextContains('Test 1.2 IT');
$this->submitNodeForm('Test 1.3 EN', 'draft');
$this->assertLatestVersionPage($node);
// Create a French draft (without saving) and verify that the Italian draft
// values are used as source values.
$url->setRouteParameter('target', 'fr');
$this->drupalGet($url);
$this->assertSession()->pageTextContains('Test 1.2 IT');
// Now switch source language and verify that the English draft values are
// used as source values.
$url->setRouteParameter('source', 'en');
$this->drupalGet($url);
$this->assertSession()->pageTextContains('Test 1.3 EN');
}
/**
* Submits the node form at the current URL with the specified values.
*
* @param string $title
* The node title.
* @param string $moderation_state
* The moderation state.
* @param bool $default_translation
* (optional) Whether we are editing the default translation.
* @param string|null $langcode
* (optional) The node language. Defaults to English.
*
* @return \Drupal\node\NodeInterface|null
* A node object if a new one is being created, NULL otherwise.
*/
protected function submitNodeForm($title, $moderation_state, $default_translation = FALSE, $langcode = 'en') {
$is_new = strpos($this->getSession()->getCurrentUrl(), '/node/add/') !== FALSE;
$edit = [
'title[0][value]' => $title,
'moderation_state[0][state]' => $moderation_state,
];
if ($is_new) {
$default_translation = TRUE;
$edit['langcode[0][value]'] = $langcode;
}
$submit = $default_translation ? t('Save') : t('Save (this translation)');
$this->drupalPostForm(NULL, $edit, $submit);
$message = $is_new ? "Article $title has been created." : "Article $title has been updated.";
$this->assertSession()->pageTextContains($message);
return $is_new ? $this->drupalGetNodeByTitle($title) : NULL;
}
/**
* Loads the node translation for the specified language.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
* @param string $langcode
* The translation language code.
*
* @return \Drupal\node\NodeInterface
* The node translation object.
*/
protected function loadTranslation(NodeInterface $node, $langcode) {
/** @var \Drupal\node\NodeStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('node');
/** @var \Drupal\node\NodeInterface $node */
$node = $storage->loadRevision($storage->getLatestRevisionId($node->id()));
return $node->getTranslation($langcode);
}
/**
* Asserts that this is the "latest version" page for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*/
public function assertLatestVersionPage(NodeInterface $node) {
$this->assertEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl());
$this->assertModerationForm($node);
}
/**
* Asserts that this is not the "latest version" page for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
* @param bool $moderation_form
* (optional) Whether the page should contain the moderation form. Defaults
* to FALSE.
*/
public function assertNotLatestVersionPage(NodeInterface $node, $moderation_form = FALSE) {
$this->assertNotEquals($node->toUrl('latest-version')->setAbsolute()->toString(), $this->getSession()->getCurrentUrl());
if ($moderation_form) {
$this->assertModerationForm($node, FALSE);
}
else {
$this->assertNoModerationForm($node);
}
}
/**
* Asserts that the moderation form is displayed for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
* @param bool $latest_tab
* (optional) Whether the node form is expected to be displayed on the
* latest version page or on the node view page. Defaults to the former.
*/
public function assertModerationForm(NodeInterface $node, $latest_tab = TRUE) {
$this->drupalGet($node->toUrl());
$this->assertEquals(!$latest_tab, $this->hasModerationForm());
$this->drupalGet($node->toUrl('latest-version'));
$this->assertEquals($latest_tab, $this->hasModerationForm());
}
/**
* Asserts that the moderation form is not displayed for the specified node.
*
* @param \Drupal\node\NodeInterface $node
* A node object.
*/
public function assertNoModerationForm(NodeInterface $node) {
$this->drupalGet($node->toUrl());
$this->assertFalse($this->hasModerationForm());
$this->drupalGet($node->toUrl('latest-version'));
$this->assertEquals(403, $this->getSession()->getStatusCode());
}
/**
* Checks whether the page contains the moderation form.
*
* @return bool
* TRUE if the moderation form could be find in the page, FALSE otherwise.
*/
public function hasModerationForm() {
return (bool) $this->xpath('//ul[@class="entity-moderation-form"]');
}
}
@@ -38,6 +38,10 @@ class ModerationRevisionRevertTest extends BrowserTestBase {
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
$workflow->save();
/** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
$router_builder = $this->container->get('router.builder');
$router_builder->rebuildIfNeeded();
$admin = $this->drupalCreateUser([
'access content overview',
'administer nodes',
@@ -132,6 +132,9 @@ abstract class ModerationStateTestBase extends BrowserTestBase {
// @see content_moderation_workflow_insert()
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
/** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */
$router_builder = $this->container->get('router.builder');
$router_builder->rebuildIfNeeded();
}
/**
@@ -0,0 +1,310 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\views\Functional\ViewTestBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Drupal\workflows\Entity\Workflow;
/**
* Tests the views 'moderation_state_filter' filter plugin.
*
* @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
*
* @group content_moderation
*/
class ViewsModerationStateFilterTest extends ViewTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_moderation_test_views',
'node',
'content_moderation',
'workflows',
'workflow_type_test',
'entity_test',
'language',
'content_translation',
'views_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp(FALSE);
NodeType::create([
'type' => 'example_a',
])->save();
NodeType::create([
'type' => 'example_b',
])->save();
$new_workflow = Workflow::create([
'type' => 'content_moderation',
'id' => 'new_workflow',
'label' => 'New workflow',
]);
$new_workflow->getTypePlugin()->addState('bar', 'Bar');
$new_workflow->save();
$this->drupalLogin($this->drupalCreateUser(['administer workflows', 'administer views']));
}
/**
* Tests the dependency handling of the moderation state filter.
*
* @covers ::calculateDependencies
* @covers ::onDependencyRemoval
*/
public function testModerationStateFilterDependencyHandling() {
// First, check that the view doesn't have any config dependency when there
// are no states configured in the filter.
$view_id = 'test_content_moderation_state_filter_base_table';
$view = Views::getView($view_id);
$this->assertWorkflowDependencies([], $view);
$this->assertTrue($view->storage->status());
// Configure the Editorial workflow for a node bundle, set the filter value
// to use one of its states and check that the workflow is now a dependency
// of the view.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => TRUE,
], 'Save');
$edit['options[value][]'] = ['editorial-published'];
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$view = Views::getView($view_id);
$this->assertWorkflowDependencies(['editorial'], $view);
$this->assertTrue($view->storage->status());
// Create another workflow and repeat the checks above.
$this->drupalPostForm('admin/config/workflow/workflows/add', [
'label' => 'Translation',
'id' => 'translation',
'workflow_type' => 'content_moderation',
], 'Save');
$this->drupalPostForm('admin/config/workflow/workflows/manage/translation/add_state', [
'label' => 'Needs Review',
'id' => 'needs_review',
], 'Save');
$this->drupalPostForm('admin/config/workflow/workflows/manage/translation/type/node', [
'bundles[example_b]' => TRUE,
], 'Save');
$edit['options[value][]'] = ['editorial-published', 'translation-needs_review'];
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$view = Views::getView($view_id);
$this->assertWorkflowDependencies(['editorial', 'translation'], $view);
$this->assertTrue(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
$this->assertTrue($view->storage->status());
// Remove the 'Translation' workflow.
$this->drupalPostForm('admin/config/workflow/workflows/manage/translation/delete', [], 'Delete');
// Check that the view has been disabled, the filter has been deleted, the
// view can be saved and there are no more config dependencies.
$view = Views::getView($view_id);
$this->assertFalse($view->storage->status());
$this->assertFalse(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$this->assertWorkflowDependencies([], $view);
}
/**
* Tests the moderation state filter when the configured workflow is changed.
*
* @dataProvider providerTestWorkflowChanges
*/
public function testWorkflowChanges($view_id, $filter_name) {
// Update the view and make the default filter not exposed anymore,
// otherwise all results will be shown when there are no more moderated
// bundles left.
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", [], 'Hide filter');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
// First, apply the Editorial workflow to both of our content types.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => TRUE,
'bundles[example_b]' => TRUE,
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
// Add a few nodes in various moderation states.
$this->createNode(['type' => 'example_a', 'moderation_state' => 'published']);
$this->createNode(['type' => 'example_b', 'moderation_state' => 'published']);
$archived_node_a = $this->createNode(['type' => 'example_a', 'moderation_state' => 'archived']);
$archived_node_b = $this->createNode(['type' => 'example_b', 'moderation_state' => 'archived']);
// Configure the view to only show nodes in the 'archived' moderation state.
$edit['options[value][]'] = ['editorial-archived'];
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
// Check that only the archived nodes from both bundles are displayed by the
// view.
$view = Views::getView($view_id);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()], ['nid' => $archived_node_b->id()]], ['nid' => 'nid']);
// Remove the Editorial workflow from one of the bundles.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => TRUE,
'bundles[example_b]' => FALSE,
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
$view = Views::getView($view_id);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()]], ['nid' => 'nid']);
// Check that the view can still be edited and saved without any
// intervention.
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
// Remove the Editorial workflow from both bundles.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => FALSE,
'bundles[example_b]' => FALSE,
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
$view = Views::getView($view_id);
$this->executeView($view);
// Check that the view doesn't return any result.
$this->assertEmpty($view->result);
// Check that the view can not be edited without any intervention anymore
// because the user needs to fix the filter.
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$this->assertSession()->pageTextContains("No valid values found on filter: $filter_name.");
}
/**
* Data provider for testWorkflowChanges.
*
* @return string[]
* An array of view IDs.
*/
public function providerTestWorkflowChanges() {
return [
'view on base table, filter on base table' => [
'test_content_moderation_state_filter_base_table',
'Content: Moderation state'
],
'view on base table, filter on revision table' => [
'test_content_moderation_state_filter_base_table_filter_on_revision',
'Content revision: Moderation state'
],
];
}
/**
* Tests the content moderation state filter caching is correct.
*/
public function testFilterRenderCache() {
// Initially all states of the workflow are displayed.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => TRUE,
], 'Save');
$this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived']);
// Adding a new state to the editorial workflow will display that state in
// the list of filters.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
'label' => 'Foo',
'id' => 'foo',
], 'Save');
$this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo']);
// Adding a second workflow to nodes will also show new states.
$this->drupalPostForm('admin/config/workflow/workflows/manage/new_workflow/type/node', [
'bundles[example_b]' => TRUE,
], 'Save');
$this->assertFilterStates(['All', 'editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar']);
// Add a few more states and change the exposed filter to allow multiple
// selections so we can check that the size of the select element does not
// exceed 8 options.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
'label' => 'Foo 2',
'id' => 'foo2',
], 'Save');
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/add_state', [
'label' => 'Foo 3',
'id' => 'foo3',
], 'Save');
$view_id = 'test_content_moderation_state_filter_base_table';
$edit['options[expose][multiple]'] = TRUE;
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$this->assertFilterStates(['editorial-draft', 'editorial-published', 'editorial-archived', 'editorial-foo', 'editorial-foo2', 'editorial-foo3', 'new_workflow-draft', 'new_workflow-published', 'new_workflow-bar'], TRUE);
}
/**
* Assert the states which appear in the filter.
*
* @param array $states
* The states which should appear in the filter.
* @param bool $check_size
* (optional) Whether to check that size of the select element is not
* greater than 8. Defaults to FALSE.
*/
protected function assertFilterStates($states, $check_size = FALSE) {
$this->drupalGet('/filter-test-path');
$assert_session = $this->assertSession();
// Check that the select contains the correct number of options.
$assert_session->elementsCount('css', '#edit-default-revision-state option', count($states));
// Check that the size of the select element does not exceed 8 options.
if ($check_size) {
$this->assertGreaterThan(8, count($states));
$assert_session->elementAttributeContains('css', '#edit-default-revision-state', 'size', 8);
}
// Check that an option exists for each of the expected states.
foreach ($states as $state) {
$assert_session->optionExists('Default Revision State', $state);
}
}
/**
* Asserts the views dependencies on workflow config entities.
*
* @param string[] $workflow_ids
* An array of workflow IDs to check.
* @param \Drupal\views\ViewExecutable $view
* An executable View object.
*/
protected function assertWorkflowDependencies(array $workflow_ids, ViewExecutable $view) {
$dependencies = $view->getDependencies();
$expected = [];
foreach (Workflow::loadMultiple($workflow_ids) as $workflow) {
$expected[] = $workflow->getConfigDependencyName();
}
if ($expected) {
$this->assertSame($expected, $dependencies['config']);
}
else {
$this->assertTrue(!isset($dependencies['config']));
}
}
}
@@ -18,7 +18,7 @@ class ContentModerationStateResourceTest extends KernelTestBase {
public static $modules = ['serialization', 'rest', 'content_moderation'];
/**
* @see content_moderation_rest_resource_alter()
* @see \Drupal\content_moderation\Entity\ContentModerationState
*/
public function testCreateContentModerationStateResource() {
$this->setExpectedException(PluginNotFoundException::class, 'The "entity:content_moderation_state" plugin does not exist.');
@@ -174,12 +174,11 @@ class ContentModerationStateTest extends KernelTestBase {
}
/**
* Tests removal of content moderation state entity field data.
* Tests removal of content moderation state entity.
*
* @dataProvider basicModerationTestCases
*/
public function testContentModerationStateDataRemoval($entity_type_id) {
// Test content moderation state deletion.
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->createEntity($entity_type_id);
$entity->save();
@@ -187,44 +186,80 @@ class ContentModerationStateTest extends KernelTestBase {
$entity->delete();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertFalse($content_moderation_state);
}
// Test content moderation state revision deletion.
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity2 */
$entity2 = $this->createEntity($entity_type_id);
$entity2->save();
$revision = clone $entity2;
/**
* Tests removal of content moderation state entity revisions.
*
* @dataProvider basicModerationTestCases
*/
public function testContentModerationStateRevisionDataRemoval($entity_type_id) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->createEntity($entity_type_id);
$entity->save();
$revision = clone $entity;
$revision->isDefaultRevision(FALSE);
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
$this->assertTrue($content_moderation_state);
$entity2 = $this->reloadEntity($entity2);
$entity2->setNewRevision(TRUE);
$entity2->save();
$entity = $this->reloadEntity($entity);
$entity->setNewRevision(TRUE);
$entity->save();
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
$entity_storage->deleteRevision($revision->getRevisionId());
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
$this->assertFalse($content_moderation_state);
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity2);
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertTrue($content_moderation_state);
}
/**
* Tests removal of content moderation state pending entity revisions.
*
* @dataProvider basicModerationTestCases
*/
public function testContentModerationStatePendingRevisionDataRemoval($entity_type_id) {
$entity = $this->createEntity($entity_type_id);
$entity->moderation_state = 'published';
$entity->save();
$entity->setNewRevision(TRUE);
$entity->moderation_state = 'draft';
$entity->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertTrue($content_moderation_state);
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
$entity_storage->deleteRevision($entity->getRevisionId());
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertFalse($content_moderation_state);
}
/**
* Tests removal of content moderation state translations.
*
* @dataProvider basicModerationTestCases
*/
public function testContentModerationStateTranslationDataRemoval($entity_type_id) {
// Test content moderation state translation deletion.
if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity3 */
$entity3 = $this->createEntity($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $this->createEntity($entity_type_id);
$langcode = 'it';
ConfigurableLanguage::createFromLangcode($langcode)
->save();
$entity3->save();
$translation = $entity3->addTranslation($langcode, ['title' => 'Titolo test']);
$entity->save();
$translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']);
// Make sure we add values for all of the required fields.
if ($entity_type_id == 'block_content') {
$translation->info = $this->randomString();
}
$translation->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertTrue($content_moderation_state->hasTranslation($langcode));
$entity3->removeTranslation($langcode);
$entity3->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity3);
$entity->removeTranslation($langcode);
$entity->save();
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
$this->assertFalse($content_moderation_state->hasTranslation($langcode));
}
}
@@ -536,6 +571,48 @@ class ContentModerationStateTest extends KernelTestBase {
], $workflow->getDependencies());
}
/**
* Test the revision default state of the moderation state entity revisions.
*
* @param string $entity_type_id
* The ID of entity type to be tested.
*
* @dataProvider basicModerationTestCases
*/
public function testRevisionDefaultState($entity_type_id) {
// Check that the revision default state of the moderated entity and the
// content moderation state entity always match.
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $cms_storage */
$cms_storage = $this->entityTypeManager->getStorage('content_moderation_state');
$entity = $this->createEntity($entity_type_id);
$entity->get('moderation_state')->value = 'published';
$storage->save($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
$cms_entity = $cms_storage->loadUnchanged(1);
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
$entity->get('moderation_state')->value = 'published';
$storage->save($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
$cms_entity = $cms_storage->loadUnchanged(1);
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
$entity->get('moderation_state')->value = 'draft';
$storage->save($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
$cms_entity = $cms_storage->loadUnchanged(1);
$this->assertEquals($entity->getLoadedRevisionId() - 1, $cms_entity->get('content_entity_revision_id')->value);
$entity->get('moderation_state')->value = 'published';
$storage->save($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
$cms_entity = $cms_storage->loadUnchanged(1);
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
}
/**
* Creates an entity.
*
@@ -2,21 +2,22 @@
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter
* @group content_moderation
* @group legacy
*/
class EntityRevisionConverterTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'user',
'entity_test',
'system',
'content_moderation',
'node',
@@ -28,74 +29,36 @@ class EntityRevisionConverterTest extends KernelTestBase {
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('content_moderation_state');
$this->installSchema('system', 'router');
$this->installSchema('system', 'sequences');
$this->installSchema('node', 'node_access');
\Drupal::service('router.builder')->rebuild();
}
/**
* @covers ::convert
* @expectedDeprecationMessage The load_pending_revision flag has been deprecated. You should use load_latest_revision instead.
*/
public function testConvertNonRevisionableEntityType() {
$entity_test = EntityTest::create([
'name' => 'test',
]);
$entity_test->save();
/** @var \Symfony\Component\Routing\RouterInterface $router */
$router = \Drupal::service('router.no_access_checks');
$result = $router->match('/entity_test/' . $entity_test->id());
$this->assertInstanceOf(EntityTest::class, $result['entity_test']);
$this->assertEquals($entity_test->getRevisionId(), $result['entity_test']->getRevisionId());
}
/**
* @covers ::convert
*/
public function testConvertWithRevisionableEntityType() {
$this->installConfig(['content_moderation']);
$node_type = NodeType::create([
public function testDeprecatedLoadPendingRevisionFlag() {
NodeType::create([
'type' => 'article',
]);
$node_type->save();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
$workflow->save();
])->save();
$revision_ids = [];
$node = Node::create([
'title' => 'test',
'type' => 'article',
]);
$node->moderation_state->value = 'published';
$node->save();
$revision_ids[] = $node->getRevisionId();
$node->isDefaultRevision(FALSE);
$node->setNewRevision(TRUE);
$node->save();
$revision_ids[] = $node->getRevisionId();
$node->setNewRevision(TRUE);
$node->moderation_state->value = 'draft';
$node->save();
$revision_ids[] = $node->getRevisionId();
/** @var \Symfony\Component\Routing\RouterInterface $router */
$router = \Drupal::service('router.no_access_checks');
$result = $router->match('/node/' . $node->id() . '/edit');
$this->assertInstanceOf(Node::class, $result['node']);
$this->assertEquals($revision_ids[2], $result['node']->getRevisionId());
$this->assertFalse($result['node']->isDefaultRevision());
$converted = $this->container->get('paramconverter.latest_revision')->convert($node->id(), [
'load_pending_revision' => TRUE,
'type' => 'entity:node',
], 'node', []);
$this->assertEquals($converted->getLoadedRevisionId(), $node->getLoadedRevisionId());
}
}
@@ -181,7 +181,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
]);
$node->save();
$node_fr = $node->addTranslation('fr');
$node_fr = $node->addTranslation('fr', $node->toArray());
$node_fr->setTitle('French Published Node');
$node_fr->save();
$this->assertEquals('published', $node_fr->moderation_state->value);
@@ -207,7 +207,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$this->assertCount(0, $violations);
// From the latest french revision, there should also be no violation.
$node_fr = $node->getTranslation('fr');
$node_fr = Node::load($node->id())->getTranslation('fr');
$this->assertEquals('published', $node_fr->moderation_state->value);
$node_fr->moderation_state = 'archived';
$violations = $node_fr->validate();
@@ -217,7 +217,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
/**
* Tests that content without prior moderation information can be moderated.
*/
public function testLegacyContent() {
public function testExistingContentWithNoModeration() {
$node_type = NodeType::create([
'type' => 'example',
]);
@@ -251,7 +251,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
/**
* Tests that content without prior moderation information can be translated.
*/
public function testLegacyMultilingualContent() {
public function testExistingMultilingualContentWithNoModeration() {
// Enable French.
ConfigurableLanguage::createFromLangcode('fr')->save();
@@ -43,6 +43,10 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
$this->installEntitySchema('content_moderation_state');
$this->installConfig('content_moderation');
NodeType::create([
'type' => 'unmoderated',
])->save();
$node_type = NodeType::create([
'type' => 'example',
]);
@@ -79,6 +83,51 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
$this->assertEquals(['draft'], $states);
}
/**
* @covers ::getValue
*/
public function testGetValue() {
$this->assertEquals([['value' => 'draft']], $this->testNode->moderation_state->getValue());
}
/**
* @covers ::get
*/
public function testGet() {
$this->assertEquals('draft', $this->testNode->moderation_state->get(0)->value);
$this->setExpectedException(\InvalidArgumentException::class);
$this->testNode->moderation_state->get(2);
}
/**
* Tests the computed field when it is unset or set to an empty value.
*/
public function testSetEmptyState() {
$this->testNode->moderation_state->value = '';
$this->assertEquals('draft', $this->testNode->moderation_state->value);
$this->testNode->moderation_state = '';
$this->assertEquals('draft', $this->testNode->moderation_state->value);
unset($this->testNode->moderation_state);
$this->assertEquals('draft', $this->testNode->moderation_state->value);
}
/**
* Test the list class with a non moderated entity.
*/
public function testNonModeratedEntity() {
$unmoderated_node = Node::create([
'type' => 'unmoderated',
'title' => 'Test title',
]);
$unmoderated_node->save();
$this->assertEquals(0, $unmoderated_node->moderation_state->count());
$unmoderated_node->moderation_state = NULL;
$this->assertEquals(0, $unmoderated_node->moderation_state->count());
}
/**
* Tests that moderation state changes also change the related entity state.
*/
@@ -124,4 +173,15 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
$this->assertTrue($test_node->isPublished());
}
/**
* Test the moderation_state field after an entity has been serialized.
*/
public function testEntityUnserialize() {
$this->testNode->moderation_state->value = 'draft';
$unserialized = unserialize(serialize($this->testNode));
$this->assertEquals('Test title', $unserialized->title->value);
$this->assertEquals('draft', $unserialized->moderation_state->value);
}
}
@@ -108,14 +108,12 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
$expected_result = [
[
'nid' => $node->id(),
// @todo I would have expected that the content_moderation_state default
// revision is the same one as in the node, but it isn't.
// Joins from the base table to the default revision of the
// content_moderation.
'moderation_state' => 'draft',
'moderation_state' => 'published',
// Joins from the revision table to the default revision of the
// content_moderation.
'moderation_state_1' => 'draft',
'moderation_state_1' => 'published',
// Joins from the revision table to the revision of the
// content_moderation.
'moderation_state_2' => 'published',
@@ -124,4 +122,27 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
$this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1', 'moderation_state_2' => 'moderation_state_2']);
}
/**
* Tests the content moderation state views field.
*/
public function testContentModerationStateField() {
$node = Node::create([
'type' => 'page',
'title' => 'Test title',
]);
$node->moderation_state->value = 'published';
$node->save();
$view = Views::getView('test_content_moderation_field_state_test');
$view->execute();
$expected_result = [
[
'title' => 'Test title',
'moderation_state' => 'published',
],
];
$this->assertIdenticalResultset($view, $expected_result, ['title' => 'title', 'moderation_state' => 'moderation_state']);
}
}
@@ -0,0 +1,298 @@
<?php
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\entity_test\Entity\EntityTestNoBundle;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Views;
use Drupal\workflows\Entity\Workflow;
/**
* Tests the views 'moderation_state_filter' filter plugin.
*
* @coversDefaultClass \Drupal\content_moderation\Plugin\views\filter\ModerationStateFilter
*
* @group content_moderation
*/
class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_moderation_test_views',
'node',
'content_moderation',
'workflows',
'workflow_type_test',
'entity_test',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp(FALSE);
$this->installEntitySchema('user');
$this->installEntitySchema('node');
$this->installEntitySchema('content_moderation_state');
$this->installEntitySchema('entity_test_no_bundle');
$this->installSchema('node', 'node_access');
$this->installConfig('content_moderation_test_views');
$this->installConfig('content_moderation');
$node_type = NodeType::create([
'type' => 'example',
]);
$node_type->save();
$node_type = NodeType::create([
'type' => 'another_example',
]);
$node_type->save();
$node_type = NodeType::create([
'type' => 'example_non_moderated',
]);
$node_type->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
}
/**
* Tests the content moderation state filter.
*/
public function testStateFilterViewsRelationship() {
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->getTypePlugin()->addState('translated_draft', 'Bar');
$configuration = $workflow->getTypePlugin()->getConfiguration();
$configuration['states']['translated_draft'] += [
'published' => FALSE,
'default_revision' => FALSE,
];
$workflow->getTypePlugin()->setConfiguration($configuration);
$workflow->save();
// Create a published default revision and one forward draft revision.
$node = Node::create([
'type' => 'example',
'title' => 'Test Node',
'moderation_state' => 'published',
]);
$node->save();
$node->setNewRevision();
$node->moderation_state = 'draft';
$node->save();
// Create a draft default revision.
$second_node = Node::create([
'type' => 'example',
'title' => 'Second Node',
'moderation_state' => 'draft',
]);
$second_node->save();
// Create a published default revision.
$third_node = Node::create([
'type' => 'example',
'title' => 'Third node',
'moderation_state' => 'published',
]);
$third_node->save();
// Add a non-moderated node.
$fourth_node = Node::create([
'type' => 'example_non_moderated',
'title' => 'Fourth node',
]);
$fourth_node->save();
// Create a translated published revision.
$translated_forward_revision = $third_node->addTranslation('fr');
$translated_forward_revision->title = 'Translated Node';
$translated_forward_revision->setNewRevision(TRUE);
$translated_forward_revision->moderation_state = 'translated_draft';
$translated_forward_revision->save();
// The three default revisions are listed when no filter is specified.
$this->assertNodesWithFilters([$node, $second_node, $third_node], []);
// The default revision of node one and three are published.
$this->assertNodesWithFilters([$node, $third_node], [
'default_revision_state' => 'editorial-published',
]);
// The default revision of node two is draft.
$this->assertNodesWithFilters([$second_node], [
'default_revision_state' => 'editorial-draft',
]);
// Test the same three revisions on a view displaying content revisions.
// Both nodes have one draft revision.
$this->assertNodesWithFilters([$node, $second_node], [
'moderation_state' => 'editorial-draft',
], 'test_content_moderation_state_filter_revision_table');
// Creating a new forward revision of node three, creates a second published
// revision of of the original language, hence there are two published
// revisions of node three.
$this->assertNodesWithFilters([$node, $third_node, $third_node], [
'moderation_state' => 'editorial-published',
], 'test_content_moderation_state_filter_revision_table');
// There is a single forward translated revision with a new state, which is
// also filterable.
$this->assertNodesWithFilters([$translated_forward_revision], [
'moderation_state' => 'editorial-translated_draft',
], 'test_content_moderation_state_filter_revision_table');
}
/**
* Test the moderation filter with a non-translatable entity type.
*/
public function testNonTranslatableEntityType() {
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
$workflow->save();
$test_entity = EntityTestNoBundle::create([
'moderation_state' => 'draft',
]);
$test_entity->save();
$view = Views::getView('test_content_moderation_state_filter_entity_test');
$view->setExposedInput([
'moderation_state' => 'editorial-draft',
]);
$view->execute();
$this->assertIdenticalResultset($view, [['id' => $test_entity->id()]], ['id' => 'id']);
}
/**
* Tests the list of states in the filter plugin.
*/
public function testStateFilterStatesList() {
// By default a view of nodes will not have states to filter.
$this->assertPluginStates([]);
// Adding a content type to the editorial workflow will enable all of the
// editorial states.
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
$this->assertPluginStates([
'Editorial' => [
'editorial-draft' => 'Draft',
'editorial-published' => 'Published',
'editorial-archived' => 'Archived',
],
]);
// Adding a workflow which is not content moderation will not add any
// additional states to the views filter.
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test']);
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->save();
$this->assertPluginStates([
'Editorial' => [
'editorial-draft' => 'Draft',
'editorial-published' => 'Published',
'editorial-archived' => 'Archived',
],
]);
// Adding a new content moderation workflow will add additional states to
// filter.
$workflow = Workflow::create(['id' => 'moderation_test', 'type' => 'content_moderation', 'label' => 'Moderation test']);
$workflow->getTypePlugin()->addState('foo', 'Foo State');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
$this->assertPluginStates([
'Editorial' => [
'editorial-draft' => 'Draft',
'editorial-published' => 'Published',
'editorial-archived' => 'Archived',
],
'Moderation test' => [
'moderation_test-foo' => 'Foo State',
'moderation_test-draft' => 'Draft',
'moderation_test-published' => 'Published',
],
]);
// Deleting a workflow will remove the states from the filter.
$workflow = Workflow::load('moderation_test');
$workflow->delete();
$this->assertPluginStates([
'Editorial' => [
'editorial-draft' => 'Draft',
'editorial-published' => 'Published',
'editorial-archived' => 'Archived',
],
]);
// Deleting a state from a workflow will remove the state from the filter.
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->deleteState('archived');
$workflow->save();
$this->assertPluginStates([
'Editorial' => [
'editorial-draft' => 'Draft',
'editorial-published' => 'Published',
],
]);
}
/**
* Assert the plugin states.
*
* @param string[] $states
* The states which should appear in the filter.
*/
protected function assertPluginStates($states) {
$plugin = Views::pluginManager('filter')->createInstance('moderation_state_filter', []);
$view = Views::getView('test_content_moderation_state_filter_base_table');
$plugin->init($view, $view->getDisplay());
$this->assertEquals($states, $plugin->getValueOptions());
}
/**
* Assert the nodes appear when the test view is executed.
*
* @param \Drupal\node\NodeInterface[] $nodes
* Nodes to assert are in the views result.
* @param array $filters
* An array of filters to apply to the view.
* @param string $view_id
* The view to execute for the results.
*/
protected function assertNodesWithFilters(array $nodes, array $filters, $view_id = 'test_content_moderation_state_filter_base_table') {
$view = Views::getView($view_id);
$view->setExposedInput($filters);
$view->execute();
// Verify the join configuration.
$query = $view->getQuery();
$join = $query->getTableInfo('content_moderation_state')['join'];
$configuration = $join->configuration;
$this->assertEquals('content_moderation_state_field_revision', $configuration['table']);
$this->assertEquals('content_entity_revision_id', $configuration['field']);
$this->assertEquals('vid', $configuration['left_field']);
$this->assertEquals('content_entity_type_id', $configuration['extra'][0]['field']);
$this->assertEquals('node', $configuration['extra'][0]['value']);
$this->assertEquals('langcode', $configuration['extra'][1]['field']);
$this->assertEquals('langcode', $configuration['extra'][1]['left_field']);
$expected_result = [];
foreach ($nodes as $node) {
$expected_result[] = ['nid' => $node->id()];
}
$this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid']);
}
}
@@ -0,0 +1,227 @@
<?php
namespace Drupal\Tests\content_moderation\Unit;
use Drupal\content_moderation\Routing\ContentModerationRouteSubscriber;
use Drupal\Core\Entity\Entity;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* @coversDefaultClass \Drupal\content_moderation\Routing\ContentModerationRouteSubscriber
*
* @group content_moderation
*/
class ContentModerationRouteSubscriberTest extends UnitTestCase {
/**
* The test content moderation route subscriber.
*
* @var \Drupal\content_moderation\Routing\ContentModerationRouteSubscriber
*/
protected $routeSubscriber;
/**
* {@inheritdoc}
*/
protected function setUp() {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$this->routeSubscriber = new ContentModerationRouteSubscriber($entity_type_manager);
$this->setupEntityTypes();
}
/**
* Creates the entity manager mock returning entity type objects.
*/
protected function setupEntityTypes() {
$definition = $this->createMock(EntityTypeInterface::class);
$definition->expects($this->any())
->method('getClass')
->will($this->returnValue(SimpleTestEntity::class));
$definition->expects($this->any())
->method('isRevisionable')
->willReturn(FALSE);
$revisionable_definition = $this->createMock(EntityTypeInterface::class);
$revisionable_definition->expects($this->any())
->method('getClass')
->will($this->returnValue(SimpleTestEntity::class));
$revisionable_definition->expects($this->any())
->method('isRevisionable')
->willReturn(TRUE);
$entity_types = [
'entity_test' => $definition,
'entity_test_rev' => $revisionable_definition,
];
$reflector = new \ReflectionProperty($this->routeSubscriber, 'moderatedEntityTypes');
$reflector->setAccessible(TRUE);
$reflector->setValue($this->routeSubscriber, $entity_types);
}
/**
* Data provider for ::testSetLatestRevisionFlag.
*/
public function setLatestRevisionFlagTestCases() {
return [
'Entity parameter not on an entity form' => [
[],
[
'entity_test' => [
'type' => 'entity:entity_test_rev',
],
],
],
'Entity parameter on an entity form' => [
[
'_entity_form' => 'entity_test_rev.edit'
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
],
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
'load_latest_revision' => TRUE,
],
],
],
'Entity form with no operation' => [
[
'_entity_form' => 'entity_test_rev'
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
],
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
'load_latest_revision' => TRUE,
],
],
],
'Non-moderated entity form' => [
[
'_entity_form' => 'entity_test_mulrev'
],
[
'entity_test_mulrev' => [
'type' => 'entity:entity_test_mulrev',
],
],
],
'Multiple entity parameters on an entity form' => [
[
'_entity_form' => 'entity_test_rev.edit'
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
],
'node' => [
'type' => 'entity:node',
],
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
'load_latest_revision' => TRUE,
],
'node' => [
'type' => 'entity:node',
],
],
],
'Overridden load_latest_revision flag does not change' => [
[
'_entity_form' => 'entity_test_rev.edit'
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
'load_latest_revision' => FALSE,
],
],
],
'Non-revisionable entity type will not change' => [
[
'_entity_form' => 'entity_test.edit'
],
[
'entity_test' => [
'type' => 'entity:entity_test',
],
],
FALSE,
FALSE,
],
'Overridden load_latest_revision flag does not change with multiple parameters' => [
[
'_entity_form' => 'entity_test_rev.edit'
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
],
'node' => [
'type' => 'entity:node',
'load_latest_revision' => FALSE,
],
],
[
'entity_test_rev' => [
'type' => 'entity:entity_test_rev',
'load_latest_revision' => TRUE,
],
'node' => [
'type' => 'entity:node',
'load_latest_revision' => FALSE,
],
],
],
];
}
/**
* Tests that the "load_latest_revision" flag is handled correctly.
*
* @param array $defaults
* The route defaults.
* @param array $parameters
* The route parameters.
* @param array|bool $expected_parameters
* (optional) The expected route parameters. Defaults to FALSE.
*
* @covers ::setLatestRevisionFlag
*
* @dataProvider setLatestRevisionFlagTestCases
*/
public function testSetLatestRevisionFlag($defaults, $parameters, $expected_parameters = FALSE) {
$route = new Route('/foo/{entity_test}', $defaults, [], [
'parameters' => $parameters,
]);
$route_collection = new RouteCollection();
$route_collection->add('test', $route);
$event = new RouteBuildEvent($route_collection);
$this->routeSubscriber->onAlterRoutes($event);
// If expected parameters have not been provided, assert they are unchanged.
$this->assertEquals($expected_parameters ?: $parameters, $route->getOption('parameters'));
}
}
/**
* A concrete entity.
*/
class SimpleTestEntity extends Entity {
}