Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
|
@ -0,0 +1,8 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
id: archived
|
||||
label: Archived
|
||||
published: false
|
||||
default_revision: true
|
||||
weight: -8
|
|
@ -0,0 +1,8 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
id: draft
|
||||
label: Draft
|
||||
published: false
|
||||
default_revision: false
|
||||
weight: -10
|
|
@ -0,0 +1,8 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies: { }
|
||||
id: published
|
||||
label: Published
|
||||
published: true
|
||||
default_revision: true
|
||||
weight: -9
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.archived
|
||||
- content_moderation.state.draft
|
||||
id: archived_draft
|
||||
label: 'Un-archive to Draft'
|
||||
stateFrom: archived
|
||||
stateTo: draft
|
||||
weight: -5
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.archived
|
||||
- content_moderation.state.published
|
||||
id: archived_published
|
||||
label: 'Un-archive'
|
||||
stateFrom: archived
|
||||
stateTo: published
|
||||
weight: -4
|
|
@ -0,0 +1,10 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.draft
|
||||
id: draft_draft
|
||||
label: 'Create New Draft'
|
||||
stateFrom: draft
|
||||
stateTo: draft
|
||||
weight: -10
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.draft
|
||||
- content_moderation.state.published
|
||||
id: draft_published
|
||||
label: 'Publish'
|
||||
stateFrom: draft
|
||||
stateTo: published
|
||||
weight: -9
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.archived
|
||||
- content_moderation.state.published
|
||||
id: published_archived
|
||||
label: 'Archive'
|
||||
stateFrom: published
|
||||
stateTo: archived
|
||||
weight: -6
|
|
@ -0,0 +1,11 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.draft
|
||||
- content_moderation.state.published
|
||||
id: published_draft
|
||||
label: 'Create New Draft'
|
||||
stateFrom: published
|
||||
stateTo: draft
|
||||
weight: -8
|
|
@ -0,0 +1,10 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- content_moderation.state.published
|
||||
id: published_published
|
||||
label: 'Publish'
|
||||
stateFrom: published
|
||||
stateTo: published
|
||||
weight: -7
|
|
@ -0,0 +1,79 @@
|
|||
content_moderation.state.*:
|
||||
type: config_entity
|
||||
label: 'Moderation state config'
|
||||
mapping:
|
||||
id:
|
||||
type: string
|
||||
label: 'ID'
|
||||
label:
|
||||
type: label
|
||||
label: 'Label'
|
||||
published:
|
||||
type: boolean
|
||||
label: 'Is published'
|
||||
default_revision:
|
||||
type: boolean
|
||||
label: 'Is default revision'
|
||||
weight:
|
||||
type: integer
|
||||
label: 'Weight'
|
||||
|
||||
content_moderation.state_transition.*:
|
||||
type: config_entity
|
||||
label: 'Moderation state transition config'
|
||||
mapping:
|
||||
id:
|
||||
type: string
|
||||
label: 'ID'
|
||||
label:
|
||||
type: label
|
||||
label: 'Label'
|
||||
stateFrom:
|
||||
type: string
|
||||
label: 'From state'
|
||||
stateTo:
|
||||
type: string
|
||||
label: 'To state'
|
||||
weight:
|
||||
type: integer
|
||||
label: 'Weight'
|
||||
|
||||
node.type.*.third_party.content_moderation:
|
||||
type: mapping
|
||||
label: 'Enable moderation states for this node type'
|
||||
mapping:
|
||||
enabled:
|
||||
type: boolean
|
||||
label: 'Moderation states enabled'
|
||||
allowed_moderation_states:
|
||||
type: sequence
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Moderation state'
|
||||
default_moderation_state:
|
||||
type: string
|
||||
label: 'Moderation state for new content'
|
||||
|
||||
block_content.type.*.third_party.content_moderation:
|
||||
type: mapping
|
||||
label: 'Enable moderation states for this block content type'
|
||||
mapping:
|
||||
enabled:
|
||||
type: boolean
|
||||
label: 'Moderation states enabled'
|
||||
allowed_moderation_states:
|
||||
type: sequence
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Moderation state'
|
||||
default_moderation_state:
|
||||
type: string
|
||||
label: 'Moderation state for new block content'
|
||||
|
||||
views.filter.latest_revision:
|
||||
type: views_filter
|
||||
label: 'Latest revision'
|
||||
mapping:
|
||||
value:
|
||||
type: string
|
||||
label: 'Value'
|
|
@ -0,0 +1,7 @@
|
|||
name: 'Content Moderation'
|
||||
type: module
|
||||
description: 'Provides moderation states for content'
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
package: Core (Experimental)
|
||||
configure: content_moderation.overview
|
|
@ -0,0 +1,5 @@
|
|||
entity-moderation-form:
|
||||
version: VERSION
|
||||
css:
|
||||
layout:
|
||||
css/entity-moderation-form.css: {}
|
|
@ -0,0 +1,11 @@
|
|||
entity.moderation_state.add_form:
|
||||
route_name: 'entity.moderation_state.add_form'
|
||||
title: 'Add moderation state'
|
||||
appears_on:
|
||||
- entity.moderation_state.collection
|
||||
|
||||
entity.moderation_state_transition.add_form:
|
||||
route_name: 'entity.moderation_state_transition.add_form'
|
||||
title: 'Add moderation state transition'
|
||||
appears_on:
|
||||
- entity.moderation_state_transition.collection
|
|
@ -0,0 +1,21 @@
|
|||
# Moderation state menu items definition
|
||||
content_moderation.overview:
|
||||
title: 'Content moderation'
|
||||
route_name: content_moderation.overview
|
||||
description: 'Configure states and transitions for entities.'
|
||||
parent: system.admin_config_workflow
|
||||
|
||||
entity.moderation_state.collection:
|
||||
title: 'Moderation states'
|
||||
route_name: entity.moderation_state.collection
|
||||
description: 'Administer moderation states.'
|
||||
parent: content_moderation.overview
|
||||
weight: 10
|
||||
|
||||
# Moderation state transition menu items definition
|
||||
entity.moderation_state_transition.collection:
|
||||
title: 'Moderation state transitions'
|
||||
route_name: entity.moderation_state_transition.collection
|
||||
description: 'Administer moderation states transitions.'
|
||||
parent: content_moderation.overview
|
||||
weight: 20
|
|
@ -0,0 +1,3 @@
|
|||
moderation_state.entities:
|
||||
deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks'
|
||||
weight: 100
|
224
web/core/modules/content_moderation/content_moderation.module
Normal file
224
web/core/modules/content_moderation/content_moderation.module
Normal file
|
@ -0,0 +1,224 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains content_moderation.module.
|
||||
*/
|
||||
|
||||
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\Menu\EditTab;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\node\Plugin\Action\PublishNode;
|
||||
use Drupal\node\Plugin\Action\UnpublishNode;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function content_moderation_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
// Main module help for the content_moderation module.
|
||||
case 'help.page.content_moderation':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Content Moderation module provides basic moderation for content. This lets site admins define states for content, and then define transitions between those states. 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 .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Moderation states') . '</dt>';
|
||||
$output .= '<dd>' . t('Moderation states provide the <em>Draft</em> and <em>Archived</em> states as additions to the basic <em>Published</em> option. You can click the blue <em>Add Moderation state</em> button and create new states.') . '</dd>';
|
||||
$output .= '<dt>' . t('Moderation state transitions') . '</dt>';
|
||||
$output .= '<dd>' . t('Using the "Moderation state transitions" screen, you can create the actual workflow. You decide the direction in which content moves from state to state, and which user roles are allowed to make that move.') . '</dd>';
|
||||
$output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
|
||||
$output .= '<dd>' . t('Each state 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 .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_base_field_info().
|
||||
*/
|
||||
function content_moderation_entity_base_field_info(EntityTypeInterface $entity_type) {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityBaseFieldInfo($entity_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*/
|
||||
function content_moderation_entity_type_alter(array &$entity_types) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityTypeAlter($entity_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_operation().
|
||||
*/
|
||||
function content_moderation_entity_operation(EntityInterface $entity) {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityOperation($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets required flag based on enabled state.
|
||||
*/
|
||||
function content_moderation_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityBundleFieldInfoAlter($fields, $entity_type, $bundle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_presave().
|
||||
*/
|
||||
function content_moderation_entity_presave(EntityInterface $entity) {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityOperations::class)
|
||||
->entityPresave($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_insert().
|
||||
*/
|
||||
function content_moderation_entity_insert(EntityInterface $entity) {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityOperations::class)
|
||||
->entityInsert($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_update().
|
||||
*/
|
||||
function content_moderation_entity_update(EntityInterface $entity) {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityOperations::class)
|
||||
->entityUpdate($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_local_tasks_alter().
|
||||
*/
|
||||
function content_moderation_local_tasks_alter(&$local_tasks) {
|
||||
$content_entity_type_ids = array_keys(array_filter(\Drupal::entityTypeManager()->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->isRevisionable();
|
||||
}));
|
||||
|
||||
foreach ($content_entity_type_ids as $content_entity_type_id) {
|
||||
if (isset($local_tasks["entity.$content_entity_type_id.edit_form"])) {
|
||||
$local_tasks["entity.$content_entity_type_id.edit_form"]['class'] = EditTab::class;
|
||||
$local_tasks["entity.$content_entity_type_id.edit_form"]['entity_type_id'] = $content_entity_type_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_alter().
|
||||
*/
|
||||
function content_moderation_form_alter(&$form, FormStateInterface $form_state, $form_id) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->formAlter($form, $form_state, $form_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK().
|
||||
*
|
||||
* Many default node templates rely on $page to determine whether to output the
|
||||
* node title as part of the node content.
|
||||
*/
|
||||
function content_moderation_preprocess_node(&$variables) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(ContentPreprocess::class)
|
||||
->preprocessNode($variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_extra_field_info().
|
||||
*/
|
||||
function content_moderation_entity_extra_field_info() {
|
||||
return \Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityExtraFieldInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_view().
|
||||
*/
|
||||
function content_moderation_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityOperations::class)
|
||||
->entityView($build, $entity, $display, $view_mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_node_access().
|
||||
*
|
||||
* Nodes in particular should be viewable if unpublished and the user has
|
||||
* the appropriate permission. This permission is therefore effectively
|
||||
* mandatory for any user that wants to moderate things.
|
||||
*/
|
||||
function content_moderation_node_access(NodeInterface $node, $operation, AccountInterface $account) {
|
||||
/** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
|
||||
$moderation_info = Drupal::service('content_moderation.moderation_information');
|
||||
|
||||
$access_result = NULL;
|
||||
if ($operation === 'view') {
|
||||
$access_result = (!$node->isPublished())
|
||||
? AccessResult::allowedIfHasPermission($account, 'view any unpublished content')
|
||||
: AccessResult::neutral();
|
||||
|
||||
$access_result->addCacheableDependency($node);
|
||||
}
|
||||
elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state && $node->moderation_state->target_id) {
|
||||
/** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
|
||||
$transition_validation = \Drupal::service('content_moderation.state_transition_validation');
|
||||
|
||||
$valid_transition_targets = $transition_validation->getValidTransitionTargets($node, $account);
|
||||
$access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
|
||||
|
||||
$access_result->addCacheableDependency($node);
|
||||
$access_result->addCacheableDependency($account);
|
||||
foreach ($valid_transition_targets as $valid_transition_target) {
|
||||
$access_result->addCacheableDependency($valid_transition_target);
|
||||
}
|
||||
}
|
||||
|
||||
return $access_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function content_moderation_theme() {
|
||||
return ['entity_moderation_form' => ['render element' => 'form']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_action_info_alter().
|
||||
*/
|
||||
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,
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
view any unpublished content:
|
||||
title: 'View any unpublished content'
|
||||
description: 'This permission is necessary for any users that may moderate content.'
|
||||
|
||||
'view moderation states':
|
||||
title: 'View moderation states'
|
||||
description: 'View moderation states.'
|
||||
|
||||
'administer moderation states':
|
||||
title: 'Administer moderation states'
|
||||
description: 'Create and edit moderation states.'
|
||||
'restrict access': TRUE
|
||||
|
||||
'administer moderation state transitions':
|
||||
title: 'Administer content moderation state transitions'
|
||||
description: 'Create and edit content moderation state transitions.'
|
||||
'restrict access': TRUE
|
||||
|
||||
view latest version:
|
||||
title: 'View the latest version'
|
||||
description: 'View the latest version of an entity. (Also requires "View any unpublished content" permission)'
|
||||
|
||||
permission_callbacks:
|
||||
- \Drupal\content_moderation\Permissions::transitionPermissions
|
|
@ -0,0 +1,7 @@
|
|||
content_moderation.overview:
|
||||
path: '/admin/config/workflow/moderation'
|
||||
defaults:
|
||||
_controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
|
||||
_title: 'Content moderation'
|
||||
requirements:
|
||||
_permission: 'access administration pages'
|
|
@ -0,0 +1,22 @@
|
|||
services:
|
||||
paramconverter.latest_revision:
|
||||
class: Drupal\content_moderation\ParamConverter\EntityRevisionConverter
|
||||
arguments: ['@entity.manager', '@content_moderation.moderation_information']
|
||||
tags:
|
||||
- { name: paramconverter, priority: 5 }
|
||||
content_moderation.state_transition_validation:
|
||||
class: \Drupal\content_moderation\StateTransitionValidation
|
||||
arguments: ['@entity_type.manager', '@entity.query']
|
||||
content_moderation.moderation_information:
|
||||
class: Drupal\content_moderation\ModerationInformation
|
||||
arguments: ['@entity_type.manager']
|
||||
access_check.latest_revision:
|
||||
class: Drupal\content_moderation\Access\LatestRevisionCheck
|
||||
arguments: ['@content_moderation.moderation_information']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _content_moderation_latest_version }
|
||||
content_moderation.revision_tracker:
|
||||
class: Drupal\content_moderation\RevisionTracker
|
||||
arguments: ['@database']
|
||||
tags:
|
||||
- { name: backend_overridable }
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provide views data for content_moderation.module.
|
||||
*
|
||||
* @ingroup views_module_handlers
|
||||
*/
|
||||
|
||||
use Drupal\content_moderation\ViewsData;
|
||||
|
||||
/**
|
||||
* Implements hook_views_data().
|
||||
*/
|
||||
function content_moderation_views_data() {
|
||||
return _content_moderation_views_data_object()->getViewsData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_views_data_alter().
|
||||
*/
|
||||
function content_moderation_views_data_alter(array &$data) {
|
||||
_content_moderation_views_data_object()->alterViewsData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ViewsData object to respond to views hooks.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ViewsData
|
||||
* The content moderation ViewsData object.
|
||||
*/
|
||||
function _content_moderation_views_data_object() {
|
||||
return new ViewsData(
|
||||
\Drupal::service('entity_type.manager'),
|
||||
\Drupal::service('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
ul.entity-moderation-form {
|
||||
list-style: none;
|
||||
display: -webkit-flex; /* Safari */
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap; /* Safari */
|
||||
flex-wrap: wrap;
|
||||
-webkit-justify-content: space-around; /* Safari */
|
||||
justify-content: space-around;
|
||||
-webkit-align-items: flex-end; /* Safari */
|
||||
align-items: flex-end;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
|
||||
ul.entity-moderation-form input[type=submit] {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Access check for the entity moderation tab.
|
||||
*/
|
||||
class LatestRevisionCheck implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* Constructs a new LatestRevisionCheck.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_information) {
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that there is a forward revision available.
|
||||
*
|
||||
* This checker assumes the presence of an '_entity_access' requirement key
|
||||
* in the same form as used by EntityAccessCheck.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityAccessCheck
|
||||
*/
|
||||
public function access(Route $route, RouteMatchInterface $route_match) {
|
||||
// This tab should not show up unless there's a reason to show it.
|
||||
$entity = $this->loadEntity($route, $route_match);
|
||||
return $this->moderationInfo->hasForwardRevision($entity)
|
||||
? AccessResult::allowed()->addCacheableDependency($entity)
|
||||
: AccessResult::forbidden()->addCacheableDependency($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default revision of the entity this route is for.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The parametrized route.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* returns the Entity in question.
|
||||
*
|
||||
* @throws \Exception
|
||||
* A generic exception is thrown if the entity couldn't be loaded. This
|
||||
* almost always implies a developer error, so it should get turned into
|
||||
* an HTTP 500.
|
||||
*/
|
||||
protected function loadEntity(Route $route, RouteMatchInterface $route_match) {
|
||||
$entity_type = $route->getOption('_content_moderation_entity_type');
|
||||
|
||||
if ($entity = $route_match->getParameter($entity_type)) {
|
||||
if ($entity instanceof EntityInterface) {
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
throw new \Exception(sprintf('%s is not a valid entity route. The LatestRevisionCheck access checker may only be used with a route that has a single entity parameter.', $route_match->getRouteName()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
|
||||
/**
|
||||
* An interface for Content moderation state entity.
|
||||
*
|
||||
* Content moderation state entities track the moderation state of other content
|
||||
* entities.
|
||||
*/
|
||||
interface ContentModerationStateInterface extends ContentEntityInterface, EntityOwnerInterface {
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityTypeInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
|
||||
|
||||
/**
|
||||
* Defines the content moderation state schema handler.
|
||||
*/
|
||||
class ContentModerationStateStorageSchema extends SqlContentEntityStorageSchema {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
|
||||
$schema = parent::getEntitySchema($entity_type, $reset);
|
||||
|
||||
// Creates an index to ensure that the lookup in
|
||||
// \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList::getModerationState()
|
||||
// is performant.
|
||||
$schema['content_moderation_state_field_data']['indexes'] += array(
|
||||
'content_moderation_state__lookup' => array('content_entity_type_id', 'content_entity_id', 'content_entity_revision_id'),
|
||||
);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Determines whether a route is the "Latest version" tab of a node.
|
||||
*/
|
||||
class ContentPreprocess implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The route match service.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface $routeMatch
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* Current route match service.
|
||||
*/
|
||||
public function __construct(RouteMatchInterface $route_match) {
|
||||
$this->routeMatch = $route_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('current_route_match')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for hook_preprocess_HOOK().
|
||||
*
|
||||
* @param array $variables
|
||||
* Theme variables to preprocess.
|
||||
*/
|
||||
public function preprocessNode(array &$variables) {
|
||||
// Set the 'page' template variable when the node is being displayed on the
|
||||
// "Latest version" tab provided by content_moderation.
|
||||
$variables['page'] = $variables['page'] || $this->isLatestVersionPage($variables['node']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a route is the "Latest version" tab of a node.
|
||||
*
|
||||
* @param \Drupal\node\Entity\Node $node
|
||||
* A node.
|
||||
*
|
||||
* @return bool
|
||||
* True if the current route is the latest version tab of the given node.
|
||||
*/
|
||||
public function isLatestVersionPage(Node $node) {
|
||||
return $this->routeMatch->getRouteName() == 'entity.node.latest_version'
|
||||
&& ($pageNode = $this->routeMatch->getParameter('node'))
|
||||
&& $pageNode->id() == $node->id();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\content_moderation\ContentModerationStateInterface;
|
||||
use Drupal\Core\Entity\ContentEntityBase;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\user\UserInterface;
|
||||
|
||||
/**
|
||||
* Defines the Content moderation state entity.
|
||||
*
|
||||
* @ContentEntityType(
|
||||
* id = "content_moderation_state",
|
||||
* label = @Translation("Content moderation state"),
|
||||
* label_singular = @Translation("content moderation state"),
|
||||
* label_plural = @Translation("content moderation states"),
|
||||
* label_count = @PluralTranslation(
|
||||
* singular = "@count content moderation state",
|
||||
* plural = "@count content moderation states"
|
||||
* ),
|
||||
* handlers = {
|
||||
* "storage_schema" = "Drupal\content_moderation\ContentModerationStateStorageSchema",
|
||||
* "views_data" = "\Drupal\views\EntityViewsData",
|
||||
* },
|
||||
* base_table = "content_moderation_state",
|
||||
* revision_table = "content_moderation_state_revision",
|
||||
* data_table = "content_moderation_state_field_data",
|
||||
* revision_data_table = "content_moderation_state_field_revision",
|
||||
* translatable = TRUE,
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "revision" = "revision_id",
|
||||
* "uuid" = "uuid",
|
||||
* "uid" = "uid",
|
||||
* "langcode" = "langcode",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ContentModerationState extends ContentEntityBase implements ContentModerationStateInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
|
||||
$fields = parent::baseFieldDefinitions($entity_type);
|
||||
|
||||
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('User'))
|
||||
->setDescription(t('The username of the entity creator.'))
|
||||
->setSetting('target_type', 'user')
|
||||
->setDefaultValueCallback('Drupal\content_moderation\Entity\ContentModerationState::getCurrentUserId')
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('Moderation state'))
|
||||
->setDescription(t('The moderation state of the referenced content.'))
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setRequired(TRUE)
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE)
|
||||
->addConstraint('ModerationState', []);
|
||||
|
||||
$fields['content_entity_type_id'] = BaseFieldDefinition::create('string')
|
||||
->setLabel(t('Content entity type ID'))
|
||||
->setDescription(t('The ID of the content entity type this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$fields['content_entity_id'] = BaseFieldDefinition::create('integer')
|
||||
->setLabel(t('Content entity ID'))
|
||||
->setDescription(t('The ID of the content entity this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
// @todo https://www.drupal.org/node/2779931 Add constraint that enforces
|
||||
// unique content_entity_type_id, content_entity_id and
|
||||
// content_entity_revision_id.
|
||||
|
||||
$fields['content_entity_revision_id'] = BaseFieldDefinition::create('integer')
|
||||
->setLabel(t('Content entity revision ID'))
|
||||
->setDescription(t('The revision ID of the content entity this moderation state is for.'))
|
||||
->setRequired(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOwner() {
|
||||
return $this->get('uid')->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOwnerId() {
|
||||
return $this->getEntityKey('uid');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOwnerId($uid) {
|
||||
$this->set('uid', $uid);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOwner(UserInterface $account) {
|
||||
$this->set('uid', $account->id());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates an entity's moderation state whilst saving that entity.
|
||||
*
|
||||
* @param \Drupal\content_moderation\Entity\ContentModerationState $content_moderation_state
|
||||
* The content moderation entity content entity to create or save.
|
||||
*
|
||||
* @internal
|
||||
* This method should only be called as a result of saving the related
|
||||
* content entity.
|
||||
*/
|
||||
public static function updateOrCreateFromEntity(ContentModerationState $content_moderation_state) {
|
||||
$content_moderation_state->realSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value callback for the 'uid' base field definition.
|
||||
*
|
||||
* @see \Drupal\content_moderation\Entity\ContentModerationState::baseFieldDefinitions()
|
||||
*
|
||||
* @return array
|
||||
* An array of default values.
|
||||
*/
|
||||
public static function getCurrentUserId() {
|
||||
return array(\Drupal::currentUser()->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save() {
|
||||
$related_entity = \Drupal::entityTypeManager()
|
||||
->getStorage($this->content_entity_type_id->value)
|
||||
->loadRevision($this->content_entity_revision_id->value);
|
||||
if ($related_entity instanceof TranslatableInterface) {
|
||||
$related_entity = $related_entity->getTranslation($this->activeLangcode);
|
||||
}
|
||||
$related_entity->moderation_state->target_id = $this->moderation_state->target_id;
|
||||
return $related_entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an entity permanently.
|
||||
*
|
||||
* When saving existing entities, the entity is assumed to be complete,
|
||||
* partial updates of entities are not supported.
|
||||
*
|
||||
* @return int
|
||||
* Either SAVED_NEW or SAVED_UPDATED, depending on the operation performed.
|
||||
*
|
||||
* @throws \Drupal\Core\Entity\EntityStorageException
|
||||
* In case of failures an exception is thrown.
|
||||
*/
|
||||
protected function realSave() {
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Customizations for block content entities.
|
||||
*/
|
||||
class BlockContentModerationHandler extends ModerationHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision_information']['revision']['#default_value'] = TRUE;
|
||||
$form['revision_information']['revision']['#disabled'] = TRUE;
|
||||
$form['revision_information']['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision']['#default_value'] = 1;
|
||||
$form['revision']['#disabled'] = TRUE;
|
||||
$form['revision']['#description'] = $this->t('Revisions must be required when moderation is enabled.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Common customizations for most/all entities.
|
||||
*
|
||||
* This class is intended primarily as a base class.
|
||||
*/
|
||||
class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
|
||||
// This is probably not necessary if configuration is setup correctly.
|
||||
$entity->setNewRevision(TRUE);
|
||||
$entity->isDefaultRevision($default_revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle) {
|
||||
// The Revisions portion of Entity API is not uniformly applied or
|
||||
// consistent. Until that's fixed, we'll make a best-attempt to apply it to
|
||||
// the common entity patterns so as to avoid every entity type needing to
|
||||
// implement this method, although some will still need to do so for now.
|
||||
// This is the API that should be universal, but isn't yet.
|
||||
// @see \Drupal\node\Entity\NodeType
|
||||
if (method_exists($bundle, 'setNewRevision')) {
|
||||
$bundle->setNewRevision(TRUE);
|
||||
}
|
||||
// This is the raw property used by NodeType, and likely others.
|
||||
elseif ($bundle->get('new_revision') !== NULL) {
|
||||
$bundle->set('new_revision', TRUE);
|
||||
}
|
||||
// This is the raw property used by BlockContentType, and maybe others.
|
||||
elseif ($bundle->get('revision') !== NULL) {
|
||||
$bundle->set('revision', TRUE);
|
||||
}
|
||||
|
||||
$bundle->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Defines operations that need to vary by entity type.
|
||||
*
|
||||
* Much of the logic contained in this handler is an indication of flaws
|
||||
* in the Entity API that are insufficiently standardized between entity types.
|
||||
* Hopefully over time functionality can be removed from this interface.
|
||||
*/
|
||||
interface ModerationHandlerInterface {
|
||||
|
||||
/**
|
||||
* Operates on moderated content entities preSave().
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to modify.
|
||||
* @param bool $default_revision
|
||||
* Whether the new revision should be made the default revision.
|
||||
* @param bool $published_state
|
||||
* Whether the state being transitioned to is a published state or not.
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state);
|
||||
|
||||
/**
|
||||
* Operates on the bundle definition that has been marked as moderated.
|
||||
*
|
||||
* Note: The values on the EntityModerationForm itself are already saved
|
||||
* so do not need to be saved here. If any changes are made to the bundle
|
||||
* object here it is this method's responsibility to call save() on it.
|
||||
*
|
||||
* The most common use case is to force revisions on for this bundle if
|
||||
* moderation is enabled. That, sadly, does not have a common API in core.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle
|
||||
* The bundle definition that is being saved.
|
||||
*/
|
||||
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle);
|
||||
|
||||
/**
|
||||
* Alters entity forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id);
|
||||
|
||||
/**
|
||||
* Alters bundle forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id);
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Customizations for node entities.
|
||||
*/
|
||||
class NodeModerationHandler extends ModerationHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
|
||||
if ($this->shouldModerate($entity, $published_state)) {
|
||||
parent::onPresave($entity, $default_revision, $published_state);
|
||||
// Only nodes have a concept of published.
|
||||
/** @var \Drupal\node\NodeInterface $entity */
|
||||
$entity->setPublished($published_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form['revision']['#disabled'] = TRUE;
|
||||
$form['revision']['#default_value'] = TRUE;
|
||||
$form['revision']['#description'] = $this->t('Revisions are required.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
/* @var \Drupal\node\Entity\NodeType $entity */
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
|
||||
if ($entity->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
|
||||
// Force the revision checkbox on.
|
||||
$form['workflow']['options']['#default_value']['revision'] = 'revision';
|
||||
$form['workflow']['options']['revision']['#disabled'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity's default revision and/or state needs adjusting.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to check.
|
||||
* @param bool $published_state
|
||||
* Whether the state being transitioned to is a published state or not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE when either the default revision or the state needs to be updated.
|
||||
*/
|
||||
protected function shouldModerate(ContentEntityInterface $entity, $published_state) {
|
||||
// @todo clarify the first condition.
|
||||
// First condition is needed so you can add a translation.
|
||||
// Second condition checks to see if the published status has changed.
|
||||
return $entity->isDefaultTranslation() || $entity->isPublished() !== $published_state;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\content_moderation\ModerationStateInterface;
|
||||
|
||||
/**
|
||||
* Defines the Moderation state entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "moderation_state",
|
||||
* label = @Translation("Moderation state"),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\content_moderation\ModerationStateAccessControlHandler",
|
||||
* "list_builder" = "Drupal\content_moderation\ModerationStateListBuilder",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\content_moderation\Form\ModerationStateForm",
|
||||
* "edit" = "Drupal\content_moderation\Form\ModerationStateForm",
|
||||
* "delete" = "Drupal\content_moderation\Form\ModerationStateDeleteForm"
|
||||
* },
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
|
||||
* },
|
||||
* },
|
||||
* config_prefix = "state",
|
||||
* admin_permission = "administer moderation states",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "uuid" = "uuid",
|
||||
* "weight" = "weight",
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/config/workflow/moderation/states/add",
|
||||
* "edit-form" = "/admin/config/workflow/moderation/states/{moderation_state}",
|
||||
* "delete-form" = "/admin/config/workflow/moderation/states/{moderation_state}/delete",
|
||||
* "collection" = "/admin/config/workflow/moderation/states"
|
||||
* },
|
||||
* config_export = {
|
||||
* "id",
|
||||
* "label",
|
||||
* "published",
|
||||
* "default_revision",
|
||||
* "weight",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class ModerationState extends ConfigEntityBase implements ModerationStateInterface {
|
||||
|
||||
/**
|
||||
* The Moderation state ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The Moderation state label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* Whether this state represents a published node.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $published;
|
||||
|
||||
/**
|
||||
* Relative weight of this state.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight;
|
||||
|
||||
/**
|
||||
* Whether this state represents a default revision of the node.
|
||||
*
|
||||
* If this is a published state, then this property is ignored.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $default_revision;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isPublishedState() {
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isDefaultRevisionState() {
|
||||
return $this->published || $this->default_revision;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\content_moderation\ModerationStateTransitionInterface;
|
||||
|
||||
/**
|
||||
* Defines the Moderation state transition entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "moderation_state_transition",
|
||||
* label = @Translation("Moderation state transition"),
|
||||
* handlers = {
|
||||
* "list_builder" = "Drupal\content_moderation\ModerationStateTransitionListBuilder",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
|
||||
* "edit" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
|
||||
* "delete" = "Drupal\content_moderation\Form\ModerationStateTransitionDeleteForm"
|
||||
* },
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
|
||||
* },
|
||||
* },
|
||||
* config_prefix = "state_transition",
|
||||
* admin_permission = "administer moderation state transitions",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "uuid" = "uuid",
|
||||
* "weight" = "weight"
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/config/workflow/moderation/transitions/add",
|
||||
* "edit-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}",
|
||||
* "delete-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}/delete",
|
||||
* "collection" = "/admin/config/workflow/moderation/transitions"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ModerationStateTransition extends ConfigEntityBase implements ModerationStateTransitionInterface {
|
||||
|
||||
/**
|
||||
* The Moderation state transition ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The Moderation state transition label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* ID of from state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $stateFrom;
|
||||
|
||||
/**
|
||||
* ID of to state.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $stateTo;
|
||||
|
||||
/**
|
||||
* Relative weight of this transition.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
parent::calculateDependencies();
|
||||
|
||||
if ($this->stateFrom) {
|
||||
$this->addDependency('config', ModerationState::load($this->stateFrom)->getConfigDependencyName());
|
||||
}
|
||||
if ($this->stateTo) {
|
||||
$this->addDependency('config', ModerationState::load($this->stateTo)->getConfigDependencyName());
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFromState() {
|
||||
return $this->stateFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getToState() {
|
||||
return $this->stateTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getWeight() {
|
||||
return $this->weight;
|
||||
}
|
||||
|
||||
}
|
266
web/core/modules/content_moderation/src/EntityOperations.php
Normal file
266
web/core/modules/content_moderation/src/EntityOperations.php
Normal file
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\content_moderation\Form\EntityModerationForm;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for reacting to entity events.
|
||||
*/
|
||||
class EntityOperations implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The Moderation Information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The Form Builder service.
|
||||
*
|
||||
* @var \Drupal\Core\Form\FormBuilderInterface
|
||||
*/
|
||||
protected $formBuilder;
|
||||
|
||||
/**
|
||||
* The Revision Tracker service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\RevisionTrackerInterface
|
||||
*/
|
||||
protected $tracker;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityOperations object.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* Moderation information service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager service.
|
||||
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
|
||||
* The form builder.
|
||||
* @param \Drupal\content_moderation\RevisionTrackerInterface $tracker
|
||||
* The revision tracker.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->formBuilder = $form_builder;
|
||||
$this->tracker = $tracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('form_builder'),
|
||||
$container->get('content_moderation.revision_tracker')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on an entity and set published status based on the moderation state.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*/
|
||||
public function entityPresave(EntityInterface $entity) {
|
||||
if (!$this->moderationInfo->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
if ($entity->moderation_state->target_id) {
|
||||
$moderation_state = $this->entityTypeManager
|
||||
->getStorage('moderation_state')
|
||||
->load($entity->moderation_state->target_id);
|
||||
$published_state = $moderation_state->isPublishedState();
|
||||
|
||||
// This entity is default if it is new, the default revision, or the
|
||||
// default revision is not published.
|
||||
$update_default_revision = $entity->isNew()
|
||||
|| $moderation_state->isDefaultRevisionState()
|
||||
|| !$this->isDefaultRevisionPublished($entity);
|
||||
|
||||
// Fire per-entity-type logic for handling the save process.
|
||||
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_insert()
|
||||
*/
|
||||
public function entityInsert(EntityInterface $entity) {
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->updateOrCreateFromEntity($entity);
|
||||
$this->setLatestRevision($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook bridge.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_update()
|
||||
*/
|
||||
public function entityUpdate(EntityInterface $entity) {
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->updateOrCreateFromEntity($entity);
|
||||
$this->setLatestRevision($entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates the moderation state of an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to update or create a moderation state for.
|
||||
*/
|
||||
protected function updateOrCreateFromEntity(EntityInterface $entity) {
|
||||
$moderation_state = $entity->moderation_state->target_id;
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if (!$moderation_state) {
|
||||
$moderation_state = $this->entityTypeManager
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
|
||||
->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
}
|
||||
|
||||
// @todo what if $entity->moderation_state->target_id is null at this point?
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$entity_id = $entity->id();
|
||||
$entity_revision_id = $entity->getRevisionId();
|
||||
|
||||
$storage = $this->entityTypeManager->getStorage('content_moderation_state');
|
||||
$entities = $storage->loadByProperties([
|
||||
'content_entity_type_id' => $entity_type_id,
|
||||
'content_entity_id' => $entity_id,
|
||||
]);
|
||||
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = reset($entities);
|
||||
if (!($content_moderation_state instanceof ContentModerationStateInterface)) {
|
||||
$content_moderation_state = $storage->create([
|
||||
'content_entity_type_id' => $entity_type_id,
|
||||
'content_entity_id' => $entity_id,
|
||||
]);
|
||||
}
|
||||
else {
|
||||
// Create a new revision.
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
}
|
||||
|
||||
// Sync translations.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$entity_langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($entity_langcode)) {
|
||||
$content_moderation_state->addTranslation($entity_langcode);
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $entity_langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($entity_langcode);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the ContentModerationState entity for the inserted entity.
|
||||
$content_moderation_state->set('content_entity_revision_id', $entity_revision_id);
|
||||
$content_moderation_state->set('moderation_state', $moderation_state);
|
||||
ContentModerationState::updateOrCreateFromEntity($content_moderation_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the latest revision.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The content entity to create content_moderation_state entity for.
|
||||
*/
|
||||
protected function setLatestRevision(EntityInterface $entity) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$this->tracker->setLatestRevision(
|
||||
$entity->getEntityTypeId(),
|
||||
$entity->id(),
|
||||
$entity->language()->getId(),
|
||||
$entity->getRevisionId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on entities being assembled before rendering.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_view()
|
||||
* @see EntityFieldManagerInterface::getExtraFields()
|
||||
*/
|
||||
public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
|
||||
if (!$this->moderationInfo->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
if (!$this->moderationInfo->isLatestRevision($entity)) {
|
||||
return;
|
||||
}
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if ($entity->isDefaultRevision()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$component = $display->getComponent('content_moderation_control');
|
||||
if ($component) {
|
||||
$build['content_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
|
||||
$build['content_moderation_control']['#weight'] = $component['weight'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the default revision for the given entity is published.
|
||||
*
|
||||
* The default revision is the same as the entity retrieved by "default" from
|
||||
* the storage handler. If the entity is translated, use the default revision
|
||||
* of the same language as the given entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the default revision is published. FALSE otherwise.
|
||||
*/
|
||||
protected function isDefaultRevisionPublished(EntityInterface $entity) {
|
||||
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
|
||||
$default_revision = $storage->load($entity->id());
|
||||
|
||||
// Ensure we are comparing the same translation as the current entity.
|
||||
if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
|
||||
// If there is no translation, then there is no default revision and is
|
||||
// therefore not published.
|
||||
if (!$default_revision->hasTranslation($entity->language()->getId())) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$default_revision = $default_revision->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
return $default_revision && $default_revision->moderation_state->entity->isPublishedState();
|
||||
}
|
||||
|
||||
}
|
408
web/core/modules/content_moderation/src/EntityTypeInfo.php
Normal file
408
web/core/modules/content_moderation/src/EntityTypeInfo.php
Normal file
|
@ -0,0 +1,408 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
|
||||
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\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
|
||||
use Drupal\content_moderation\Form\BundleModerationConfigurationForm;
|
||||
use Drupal\content_moderation\Routing\EntityModerationRouteProvider;
|
||||
use Drupal\content_moderation\Routing\EntityTypeModerationRouteProvider;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Manipulates entity type information.
|
||||
*
|
||||
* This class contains primarily bridged hooks for compile-time or
|
||||
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
|
||||
*/
|
||||
class EntityTypeInfo implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* A keyed array of custom moderation handlers for given entity types.
|
||||
*
|
||||
* Any entity not specified will use a common default.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $moderationHandlers = [
|
||||
'node' => NodeModerationHandler::class,
|
||||
'block_content' => BlockContentModerationHandler::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* EntityTypeInfo constructor.
|
||||
*
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
|
||||
* The translation service. for form alters.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager.
|
||||
*/
|
||||
public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
|
||||
$this->stringTranslation = $translation;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->currentUser = $current_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('current_user')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds Moderation configuration to appropriate entity types.
|
||||
*
|
||||
* This is an alter hook bridge.
|
||||
*
|
||||
* @param EntityTypeInterface[] $entity_types
|
||||
* The master entity type list to alter.
|
||||
*
|
||||
* @see hook_entity_type_alter()
|
||||
*/
|
||||
public function entityTypeAlter(array &$entity_types) {
|
||||
foreach ($this->filterNonRevisionableEntityTypes($entity_types) as $type_name => $type) {
|
||||
$entity_types[$type_name] = $this->addModerationToEntityType($type);
|
||||
$entity_types[$type->get('bundle_of')] = $this->addModerationToEntity($entity_types[$type->get('bundle_of')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies an entity definition to include moderation support.
|
||||
*
|
||||
* This primarily just means an extra handler. A Generic one is provided,
|
||||
* but individual entity types can provide their own as appropriate.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
|
||||
* The content entity definition to modify.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityTypeInterface
|
||||
* The modified content entity definition.
|
||||
*/
|
||||
protected function addModerationToEntity(ContentEntityTypeInterface $type) {
|
||||
if (!$type->hasHandlerClass('moderation')) {
|
||||
$handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
|
||||
$type->setHandlerClass('moderation', $handler_class);
|
||||
}
|
||||
|
||||
if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
|
||||
$type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
|
||||
}
|
||||
|
||||
// @todo Core forgot to add a direct way to manipulate route_provider, so
|
||||
// we have to do it the sloppy way for now.
|
||||
$providers = $type->getRouteProviderClasses() ?: [];
|
||||
if (empty($providers['moderation'])) {
|
||||
$providers['moderation'] = EntityModerationRouteProvider::class;
|
||||
$type->setHandlerClass('route_provider', $providers);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures moderation configuration support on a entity type definition.
|
||||
*
|
||||
* That "configuration support" includes a configuration form, a hypermedia
|
||||
* link, and a route provider to tie it all together. There's also a
|
||||
* moderation handler for per-entity-type variation.
|
||||
*
|
||||
* @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $type
|
||||
* The config entity definition to modify.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface
|
||||
* The modified config entity definition.
|
||||
*/
|
||||
protected function addModerationToEntityType(ConfigEntityTypeInterface $type) {
|
||||
if ($type->hasLinkTemplate('edit-form') && !$type->hasLinkTemplate('moderation-form')) {
|
||||
$type->setLinkTemplate('moderation-form', $type->getLinkTemplate('edit-form') . '/moderation');
|
||||
}
|
||||
|
||||
if (!$type->getFormClass('moderation')) {
|
||||
$type->setFormClass('moderation', BundleModerationConfigurationForm::class);
|
||||
}
|
||||
|
||||
// @todo Core forgot to add a direct way to manipulate route_provider, so
|
||||
// we have to do it the sloppy way for now.
|
||||
$providers = $type->getRouteProviderClasses() ?: [];
|
||||
if (empty($providers['moderation'])) {
|
||||
$providers['moderation'] = EntityTypeModerationRouteProvider::class;
|
||||
$type->setHandlerClass('route_provider', $providers);
|
||||
}
|
||||
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an operation on bundles that should have a Moderation form.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity on which to define an operation.
|
||||
*
|
||||
* @return array
|
||||
* An array of operation definitions.
|
||||
*
|
||||
* @see hook_entity_operation()
|
||||
*/
|
||||
public function entityOperation(EntityInterface $entity) {
|
||||
$operations = [];
|
||||
$type = $entity->getEntityType();
|
||||
$bundle_of = $type->getBundleOf();
|
||||
if ($this->currentUser->hasPermission('administer moderation states') && $bundle_of &&
|
||||
$this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of))
|
||||
) {
|
||||
$operations['manage-moderation'] = [
|
||||
'title' => t('Manage moderation'),
|
||||
'weight' => 27,
|
||||
'url' => Url::fromRoute("entity.{$type->id()}.moderation", [$entity->getEntityTypeId() => $entity->id()]),
|
||||
];
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the "extra fields" for a bundle.
|
||||
*
|
||||
* This is a hook bridge.
|
||||
*
|
||||
* @see hook_entity_extra_field_info()
|
||||
*
|
||||
* @return array
|
||||
* A nested array of 'pseudo-field' elements. Each list is nested within the
|
||||
* following keys: entity type, bundle name, context (either 'form' or
|
||||
* 'display'). The keys are the name of the elements as appearing in the
|
||||
* renderable array (either the entity form or the displayed entity). The
|
||||
* value is an associative array:
|
||||
* - label: The human readable name of the element. Make sure you sanitize
|
||||
* this appropriately.
|
||||
* - description: A short description of the element contents.
|
||||
* - weight: The default weight of the element.
|
||||
* - visible: (optional) The default visibility of the element. Defaults to
|
||||
* TRUE.
|
||||
* - edit: (optional) String containing markup (normally a link) used as the
|
||||
* element's 'edit' operation in the administration interface. Only for
|
||||
* 'form' context.
|
||||
* - delete: (optional) String containing markup (normally a link) used as
|
||||
* the element's 'delete' operation in the administration interface. Only
|
||||
* for 'form' context.
|
||||
*/
|
||||
public function entityExtraFieldInfo() {
|
||||
$return = [];
|
||||
foreach ($this->getModeratedBundles() as $bundle) {
|
||||
$return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
|
||||
'label' => $this->t('Moderation control'),
|
||||
'description' => $this->t("Status listing and form for the entity's moderation state."),
|
||||
'weight' => -20,
|
||||
'visible' => TRUE,
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterable list of entity names and bundle names under moderation.
|
||||
*
|
||||
* That is, this method returns a list of bundles that have Content
|
||||
* Moderation enabled on them.
|
||||
*
|
||||
* @return \Generator
|
||||
* A generator, yielding a 2 element associative array:
|
||||
* - entity: The machine name of an entity type, such as "node" or
|
||||
* "block_content".
|
||||
* - bundle: The machine name of a bundle, such as "page" or "article".
|
||||
*/
|
||||
protected function getModeratedBundles() {
|
||||
/** @var ConfigEntityTypeInterface $type */
|
||||
foreach ($this->filterNonRevisionableEntityTypes($this->entityTypeManager->getDefinitions()) as $type_name => $type) {
|
||||
$result = $this->entityTypeManager
|
||||
->getStorage($type_name)
|
||||
->getQuery()
|
||||
->condition('third_party_settings.content_moderation.enabled', TRUE)
|
||||
->execute();
|
||||
|
||||
foreach ($result as $bundle_name) {
|
||||
yield ['entity' => $type->getBundleOf(), 'bundle' => $bundle_name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds base field info to an entity type.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* Entity type for adding base fields to.
|
||||
*
|
||||
* @return \Drupal\Core\Field\BaseFieldDefinition[]
|
||||
* New fields added by moderation state.
|
||||
*/
|
||||
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
|
||||
if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel($this->t('Moderation state'))
|
||||
->setDescription($this->t('The moderation state of this piece of content.'))
|
||||
->setComputed(TRUE)
|
||||
->setClass(ModerationStateFieldItemList::class)
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setDisplayOptions('view', [
|
||||
'label' => 'hidden',
|
||||
'type' => 'hidden',
|
||||
'weight' => -5,
|
||||
])
|
||||
->setDisplayOptions('form', [
|
||||
'type' => 'moderation_state_default',
|
||||
'weight' => 5,
|
||||
'settings' => [],
|
||||
])
|
||||
->addConstraint('ModerationState', [])
|
||||
->setDisplayConfigurable('form', FALSE)
|
||||
->setDisplayConfigurable('view', FALSE)
|
||||
->setReadOnly(FALSE)
|
||||
->setTranslatable(TRUE);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ModerationState constraint to bundles whose entities are moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface[] $fields
|
||||
* The array of bundle field definitions.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param string $bundle
|
||||
* The bundle.
|
||||
*
|
||||
* @see hook_entity_bundle_field_info_alter();
|
||||
*/
|
||||
public function entityBundleFieldInfoAlter(&$fields, EntityTypeInterface $entity_type, $bundle) {
|
||||
if (!empty($fields['moderation_state']) && $this->moderationInfo->shouldModerateEntitiesOfBundle($entity_type, $bundle)) {
|
||||
$fields['moderation_state']->addConstraint('ModerationState', []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters bundle forms to enforce revision handling.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form id.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
$form_object = $form_state->getFormObject();
|
||||
if ($form_object instanceof BundleEntityFormBase) {
|
||||
$type = $form_object->getEntity()->getEntityType();
|
||||
if ($this->moderationInfo->canModerateEntitiesOfEntityType($type)) {
|
||||
$this->entityTypeManager->getHandler($type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
|
||||
}
|
||||
}
|
||||
elseif ($form_object instanceof ContentEntityFormInterface) {
|
||||
$entity = $form_object->getEntity();
|
||||
if ($this->moderationInfo->isModeratedEntity($entity)) {
|
||||
$this->entityTypeManager
|
||||
->getHandler($entity->getEntityTypeId(), 'moderation')
|
||||
->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
|
||||
// Submit handler to redirect to the latest version, if available.
|
||||
$form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect content entity edit forms on save, if there is a forward revision.
|
||||
*
|
||||
* When saving their changes, editors should see those changes displayed on
|
||||
* the next page.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
|
||||
/* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
|
||||
$moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
|
||||
if ($moderation_info->hasForwardRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
|
||||
$entity_type_id = $entity->getEntityTypeId();
|
||||
$form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters entity type lists to return only revisionable entity types.
|
||||
*
|
||||
* @param EntityTypeInterface[] $entity_types
|
||||
* The master entity type list filter.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityTypeInterface[]
|
||||
* An array of revisionable entity types which are configuration entities.
|
||||
*/
|
||||
protected function filterNonRevisionableEntityTypes(array $entity_types) {
|
||||
return array_filter($entity_types, function (EntityTypeInterface $type) use ($entity_types) {
|
||||
return ($type instanceof ConfigEntityTypeInterface)
|
||||
&& ($bundle_of = $type->get('bundle_of'))
|
||||
&& $entity_types[$bundle_of]->isRevisionable();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Form for configuring moderation usage on a given entity bundle.
|
||||
*/
|
||||
class BundleModerationConfigurationForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('entity_type.manager'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Blank out the base form ID so that form alters that use the base form ID to
|
||||
* target both add and edit forms don't pick up this form.
|
||||
*/
|
||||
public function getBaseFormId() {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
|
||||
$bundle = $form_state->getFormObject()->getEntity();
|
||||
$form['enable_moderation_state'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Enable moderation states.'),
|
||||
'#description' => $this->t('Content of this type must transition through moderation states in order to be published.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE),
|
||||
];
|
||||
|
||||
// Add a special message when moderation is being disabled.
|
||||
if ($bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
|
||||
$form['enable_moderation_state_note'] = [
|
||||
'#type' => 'item',
|
||||
'#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => FALSE],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple();
|
||||
$label = function(ModerationState $state) {
|
||||
return $state->label();
|
||||
};
|
||||
|
||||
$options_published = array_map($label, array_filter($states, function(ModerationState $state) {
|
||||
return $state->isPublishedState();
|
||||
}));
|
||||
|
||||
$options_unpublished = array_map($label, array_filter($states, function(ModerationState $state) {
|
||||
return !$state->isPublishedState();
|
||||
}));
|
||||
|
||||
$form['allowed_moderation_states_unpublished'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Allowed moderation states (Unpublished)'),
|
||||
'#description' => $this->t('The allowed unpublished moderation states this content-type can be assigned.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_unpublished)),
|
||||
'#options' => $options_unpublished,
|
||||
'#required' => TRUE,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$form['allowed_moderation_states_published'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Allowed moderation states (Published)'),
|
||||
'#description' => $this->t('The allowed published moderation states this content-type can be assigned.'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_published)),
|
||||
'#options' => $options_published,
|
||||
'#required' => TRUE,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// The key of the array needs to be a user-facing string so we have to fully
|
||||
// render the translatable string to a real string, or else PHP errors on an
|
||||
// object used as an array key.
|
||||
$options = [
|
||||
$this->t('Unpublished')->render() => $options_unpublished,
|
||||
$this->t('Published')->render() => $options_published,
|
||||
];
|
||||
|
||||
$form['default_moderation_state'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Default moderation state'),
|
||||
'#options' => $options,
|
||||
'#description' => $this->t('Select the moderation state for new content'),
|
||||
'#default_value' => $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'),
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name=enable_moderation_state]' => ['checked' => TRUE],
|
||||
],
|
||||
],
|
||||
];
|
||||
$form['#entity_builders'][] = [$this, 'formBuilderCallback'];
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form builder callback.
|
||||
*
|
||||
* @todo This should be folded into the form method.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $bundle
|
||||
* The bundle entity updated with the submitted values.
|
||||
* @param array $form
|
||||
* The complete form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function formBuilderCallback($entity_type_id, EntityInterface $bundle, &$form, FormStateInterface $form_state) {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
if ($bundle instanceof ThirdPartySettingsInterface) {
|
||||
$bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state'));
|
||||
$bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished'))));
|
||||
$bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->getValue('enable_moderation_state')) {
|
||||
$allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')));
|
||||
|
||||
if (($default = $form_state->getValue('default_moderation_state')) && !in_array($default, $allowed, TRUE)) {
|
||||
$form_state->setErrorByName('default_moderation_state', $this->t('The default moderation state must be one of the allowed states.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// If moderation is enabled, revisions MUST be enabled as well. Otherwise we
|
||||
// can't have forward revisions.
|
||||
if ($form_state->getValue('enable_moderation_state')) {
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
|
||||
$bundle = $form_state->getFormObject()->getEntity();
|
||||
|
||||
$this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle);
|
||||
}
|
||||
|
||||
parent::submitForm($form, $form_state);
|
||||
|
||||
drupal_set_message($this->t('Your settings have been saved.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* The EntityModerationForm provides a simple UI for changing moderation state.
|
||||
*/
|
||||
class EntityModerationForm extends FormBase {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The moderation state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* EntityModerationForm constructor.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validation
|
||||
* The moderation state transition validation service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->validation = $validation;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('content_moderation.state_transition_validation'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_moderation_entity_moderation_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) {
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
|
||||
$transitions = $this->validation->getValidTransitions($entity, $this->currentUser());
|
||||
|
||||
// Exclude self-transitions.
|
||||
$transitions = array_filter($transitions, function(ModerationStateTransition $transition) use ($current_state) {
|
||||
return $transition->getToState() != $current_state->id();
|
||||
});
|
||||
|
||||
$target_states = [];
|
||||
/** @var ModerationStateTransition $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->getToState()] = $transition->label();
|
||||
}
|
||||
|
||||
if (!count($target_states)) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
if ($current_state) {
|
||||
$form['current'] = [
|
||||
'#type' => 'item',
|
||||
'#title' => $this->t('Status'),
|
||||
'#markup' => $current_state->label(),
|
||||
];
|
||||
}
|
||||
|
||||
// Persist the entity so we can access it in the submit handler.
|
||||
$form_state->set('entity', $entity);
|
||||
|
||||
$form['new_state'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Moderate'),
|
||||
'#options' => $target_states,
|
||||
];
|
||||
|
||||
$form['revision_log'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Log message'),
|
||||
'#size' => 30,
|
||||
];
|
||||
|
||||
$form['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Apply'),
|
||||
];
|
||||
|
||||
$form['#theme'] = ['entity_moderation_form'];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
/** @var ContentEntityInterface $entity */
|
||||
$entity = $form_state->get('entity');
|
||||
|
||||
$new_state = $form_state->getValue('new_state');
|
||||
|
||||
// @todo should we just just be updating the content moderation state
|
||||
// entity? That would prevent setting the revision log.
|
||||
$entity->moderation_state->target_id = $new_state;
|
||||
$entity->revision_log = $form_state->getValue('revision_log');
|
||||
|
||||
$entity->save();
|
||||
|
||||
drupal_set_message($this->t('The moderation state has been updated.'));
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $state */
|
||||
$state = $this->entityTypeManager->getStorage('moderation_state')->load($new_state);
|
||||
|
||||
// The page we're on likely won't be visible if we just set the entity to
|
||||
// the default state, as we hide that latest-revision tab if there is no
|
||||
// forward revision. Redirect to the canonical URL instead, since that will
|
||||
// still exist.
|
||||
if ($state->isDefaultRevisionState()) {
|
||||
$form_state->setRedirectUrl($entity->toUrl('canonical'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Builds the form to delete Moderation state entities.
|
||||
*/
|
||||
class ModerationStateDeleteForm extends EntityConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return new Url('entity.moderation_state.collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->entity->delete();
|
||||
|
||||
drupal_set_message($this->t(
|
||||
'Moderation state %label deleted.',
|
||||
['%label' => $this->entity->label()]
|
||||
));
|
||||
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Class ModerationStateForm.
|
||||
*/
|
||||
class ModerationStateForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
/* @var \Drupal\content_moderation\ModerationStateInterface $moderation_state */
|
||||
$moderation_state = $this->entity;
|
||||
$form['label'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $moderation_state->label(),
|
||||
'#description' => $this->t('Label for the Moderation state.'),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
$form['id'] = array(
|
||||
'#type' => 'machine_name',
|
||||
'#default_value' => $moderation_state->id(),
|
||||
'#machine_name' => array(
|
||||
'exists' => [ModerationState::class, 'load'],
|
||||
),
|
||||
'#disabled' => !$moderation_state->isNew(),
|
||||
);
|
||||
|
||||
$form['published'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Published'),
|
||||
'#description' => $this->t('When content reaches this state it should be published.'),
|
||||
'#default_value' => $moderation_state->isPublishedState(),
|
||||
];
|
||||
|
||||
$form['default_revision'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Default revision'),
|
||||
'#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'),
|
||||
'#default_value' => $moderation_state->isDefaultRevisionState(),
|
||||
// @todo Add form #state to force "make default" on when "published" is
|
||||
// on for a state.
|
||||
// @see https://www.drupal.org/node/2645614
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$moderation_state = $this->entity;
|
||||
$status = $moderation_state->save();
|
||||
|
||||
switch ($status) {
|
||||
case SAVED_NEW:
|
||||
drupal_set_message($this->t('Created the %label Moderation state.', [
|
||||
'%label' => $moderation_state->label(),
|
||||
]));
|
||||
break;
|
||||
|
||||
default:
|
||||
drupal_set_message($this->t('Saved the %label Moderation state.', [
|
||||
'%label' => $moderation_state->label(),
|
||||
]));
|
||||
}
|
||||
$form_state->setRedirectUrl($moderation_state->toUrl('collection'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Builds the form to delete Moderation state transition entities.
|
||||
*/
|
||||
class ModerationStateTransitionDeleteForm extends EntityConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return new Url('entity.moderation_state_transition.collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->entity->delete();
|
||||
|
||||
drupal_set_message($this->t(
|
||||
'Moderation transition %label deleted.',
|
||||
['%label' => $this->entity->label()]
|
||||
));
|
||||
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Class ModerationStateTransitionForm.
|
||||
*
|
||||
* @package Drupal\content_moderation\Form
|
||||
*/
|
||||
class ModerationStateTransitionForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity query factory.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $queryFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateTransitionForm.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
|
||||
* The entity query factory.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->queryFactory = $query_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('entity_type.manager'), $container->get('entity.query'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
/* @var \Drupal\content_moderation\ModerationStateTransitionInterface $moderation_state_transition */
|
||||
$moderation_state_transition = $this->entity;
|
||||
$form['label'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $moderation_state_transition->label(),
|
||||
'#description' => $this->t('Label for the Moderation state transition.'),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
$form['id'] = [
|
||||
'#type' => 'machine_name',
|
||||
'#default_value' => $moderation_state_transition->id(),
|
||||
'#machine_name' => [
|
||||
'exists' => '\Drupal\content_moderation\Entity\ModerationStateTransition::load',
|
||||
],
|
||||
'#disabled' => !$moderation_state_transition->isNew(),
|
||||
];
|
||||
|
||||
$options = [];
|
||||
foreach ($this->entityTypeManager->getStorage('moderation_state')
|
||||
->loadMultiple() as $moderation_state) {
|
||||
$options[$moderation_state->id()] = $moderation_state->label();
|
||||
}
|
||||
|
||||
$form['container'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['container-inline'],
|
||||
],
|
||||
];
|
||||
|
||||
$form['container']['stateFrom'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Transition from'),
|
||||
'#options' => $options,
|
||||
'#required' => TRUE,
|
||||
'#empty_option' => $this->t('-- Select --'),
|
||||
'#default_value' => $moderation_state_transition->getFromState(),
|
||||
];
|
||||
|
||||
$form['container']['stateTo'] = [
|
||||
'#type' => 'select',
|
||||
'#options' => $options,
|
||||
'#required' => TRUE,
|
||||
'#title' => $this->t('Transition to'),
|
||||
'#empty_option' => $this->t('-- Select --'),
|
||||
'#default_value' => $moderation_state_transition->getToState(),
|
||||
];
|
||||
|
||||
// Make sure there's always at least a wide enough delta on weight to cover
|
||||
// the current value or the total number of transitions. That way we
|
||||
// never end up forcing a transition to change its weight needlessly.
|
||||
$num_transitions = $this->queryFactory->get('moderation_state_transition')
|
||||
->count()
|
||||
->execute();
|
||||
$delta = max(abs($moderation_state_transition->getWeight()), $num_transitions);
|
||||
|
||||
$form['weight'] = [
|
||||
'#type' => 'weight',
|
||||
'#delta' => $delta,
|
||||
'#options' => $options,
|
||||
'#title' => $this->t('Weight'),
|
||||
'#default_value' => $moderation_state_transition->getWeight(),
|
||||
'#description' => $this->t('Orders the transitions in moderation forms and the administrative listing. Heavier items will sink and the lighter items will be positioned nearer the top.'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$moderation_state_transition = $this->entity;
|
||||
$status = $moderation_state_transition->save();
|
||||
|
||||
switch ($status) {
|
||||
case SAVED_NEW:
|
||||
drupal_set_message($this->t('Created the %label Moderation state transition.', [
|
||||
'%label' => $moderation_state_transition->label(),
|
||||
]));
|
||||
break;
|
||||
|
||||
default:
|
||||
drupal_set_message($this->t('Saved the %label Moderation state transition.', [
|
||||
'%label' => $moderation_state_transition->label(),
|
||||
]));
|
||||
}
|
||||
$form_state->setRedirectUrl($moderation_state_transition->toUrl('collection'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* General service for moderation-related questions about Entity API.
|
||||
*/
|
||||
class ModerationInformation implements ModerationInformationInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationInformation instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isModeratedEntity(EntityInterface $entity) {
|
||||
if (!$entity instanceof ContentEntityInterface) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return $this->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canModerateEntitiesOfEntityType(EntityTypeInterface $entity_type) {
|
||||
return $entity_type->hasHandlerClass('moderation');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle) {
|
||||
if ($this->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
$bundle_entity = $this->entityTypeManager->getStorage($entity_type->getBundleEntityType())->load($bundle);
|
||||
if ($bundle_entity) {
|
||||
return $bundle_entity->getThirdPartySetting('content_moderation', 'enabled', FALSE);
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLatestRevision($entity_type_id, $entity_id) {
|
||||
if ($latest_revision_id = $this->getLatestRevisionId($entity_type_id, $entity_id)) {
|
||||
return $this->entityTypeManager->getStorage($entity_type_id)->loadRevision($latest_revision_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLatestRevisionId($entity_type_id, $entity_id) {
|
||||
if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) {
|
||||
$revision_ids = $storage->getQuery()
|
||||
->allRevisions()
|
||||
->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id)
|
||||
->sort($this->entityTypeManager->getDefinition($entity_type_id)->getKey('revision'), 'DESC')
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
if ($revision_ids) {
|
||||
return array_keys($revision_ids)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultRevisionId($entity_type_id, $entity_id) {
|
||||
if ($storage = $this->entityTypeManager->getStorage($entity_type_id)) {
|
||||
$revision_ids = $storage->getQuery()
|
||||
->condition($this->entityTypeManager->getDefinition($entity_type_id)->getKey('id'), $entity_id)
|
||||
->sort($this->entityTypeManager->getDefinition($entity_type_id)->getKey('revision'), 'DESC')
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
if ($revision_ids) {
|
||||
return array_keys($revision_ids)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLatestRevision(ContentEntityInterface $entity) {
|
||||
return $entity->getRevisionId() == $this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasForwardRevision(ContentEntityInterface $entity) {
|
||||
return $this->isModeratedEntity($entity)
|
||||
&& !($this->getLatestRevisionId($entity->getEntityTypeId(), $entity->id()) == $this->getDefaultRevisionId($entity->getEntityTypeId(), $entity->id()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isLiveRevision(ContentEntityInterface $entity) {
|
||||
return $this->isLatestRevision($entity)
|
||||
&& $entity->isDefaultRevision()
|
||||
&& $entity->moderation_state->entity
|
||||
&& $entity->moderation_state->entity->isPublishedState();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
|
||||
/**
|
||||
* Interface for moderation_information service.
|
||||
*/
|
||||
interface ModerationInformationInterface {
|
||||
|
||||
/**
|
||||
* Determines if an entity is moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity we may be moderating.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity is moderated, FALSE otherwise.
|
||||
*/
|
||||
public function isModeratedEntity(EntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if an entity type can have moderated entities.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* An entity type object.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity type can have moderated entities, FALSE otherwise.
|
||||
*/
|
||||
public function canModerateEntitiesOfEntityType(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Determines if an entity type/bundle entities should be moderated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition to check.
|
||||
* @param string $bundle
|
||||
* The bundle to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if an entity type/bundle entities should be moderated, FALSE
|
||||
* otherwise.
|
||||
*/
|
||||
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle);
|
||||
|
||||
/**
|
||||
* Loads the latest revision of a specific entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface|null
|
||||
* The latest entity revision or NULL, if the entity type / entity doesn't
|
||||
* exist.
|
||||
*/
|
||||
public function getLatestRevision($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Returns the revision ID of the latest revision of the given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return int
|
||||
* The revision ID of the latest revision for the specified entity, or
|
||||
* NULL if there is no such entity.
|
||||
*/
|
||||
public function getLatestRevisionId($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Returns the revision ID of the default revision for the specified entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*
|
||||
* @return int
|
||||
* The revision ID of the default revision, or NULL if the entity was
|
||||
* not found.
|
||||
*/
|
||||
public function getDefaultRevisionId($entity_type_id, $entity_id);
|
||||
|
||||
/**
|
||||
* Determines if an entity is a latest revision.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* A revisionable content entity.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the specified object is the latest revision of its entity,
|
||||
* FALSE otherwise.
|
||||
*/
|
||||
public function isLatestRevision(ContentEntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if a forward revision exists for the specified entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity which may or may not have a forward revision.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity has forward revisions available, FALSE otherwise.
|
||||
*/
|
||||
public function hasForwardRevision(ContentEntityInterface $entity);
|
||||
|
||||
/**
|
||||
* Determines if an entity is "live".
|
||||
*
|
||||
* A "live" entity revision is one whose latest revision is also the default,
|
||||
* and whose moderation state, if any, is a published state.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the specified entity is a live revision, FALSE otherwise.
|
||||
*/
|
||||
public function isLiveRevision(ContentEntityInterface $entity);
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
|
||||
/**
|
||||
* Access controller for the Moderation State entity.
|
||||
*
|
||||
* @see \Drupal\workbench_moderation\Entity\ModerationState.
|
||||
*/
|
||||
class ModerationStateAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
$admin_access = parent::checkAccess($entity, $operation, $account);
|
||||
|
||||
// Allow view with other permission.
|
||||
if ($operation === 'view') {
|
||||
return AccessResult::allowedIfHasPermission($account, 'view moderation states')->orIf($admin_access);
|
||||
}
|
||||
|
||||
return $admin_access;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for defining Moderation state entities.
|
||||
*/
|
||||
interface ModerationStateInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Determines if content updated to this state should be published.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if content updated to this state should be published.
|
||||
*/
|
||||
public function isPublishedState();
|
||||
|
||||
/**
|
||||
* Determines if content updated to this state should be the default revision.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if content in this state should be the default revision.
|
||||
*/
|
||||
public function isDefaultRevisionState();
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\DraggableListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Provides a listing of Moderation state entities.
|
||||
*/
|
||||
class ModerationStateListBuilder extends DraggableListBuilder {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'moderation_state_admin_overview_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['label'] = $this->t('Moderation state');
|
||||
$header['id'] = $this->t('Machine name');
|
||||
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
$row['label'] = $entity->label();
|
||||
$row['id']['#markup'] = $entity->id();
|
||||
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for defining Moderation state transition entities.
|
||||
*/
|
||||
interface ModerationStateTransitionInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Gets the from state for the given transition.
|
||||
*
|
||||
* @return string
|
||||
* The moderation state ID for the from state.
|
||||
*/
|
||||
public function getFromState();
|
||||
|
||||
/**
|
||||
* Gets the to state for the given transition.
|
||||
*
|
||||
* @return string
|
||||
* The moderation state ID for the to state.
|
||||
*/
|
||||
public function getToState();
|
||||
|
||||
/**
|
||||
* Gets the weight for the given transition.
|
||||
*
|
||||
* @return int
|
||||
* The weight of this transition.
|
||||
*/
|
||||
public function getWeight();
|
||||
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Config\Entity\DraggableListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\user\RoleStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a listing of Moderation state transition entities.
|
||||
*/
|
||||
class ModerationStateTransitionListBuilder extends DraggableListBuilder {
|
||||
|
||||
/**
|
||||
* Moderation state entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $stateStorage;
|
||||
|
||||
/**
|
||||
* The role storage.
|
||||
*
|
||||
* @var \Drupal\user\RoleStorageInterface
|
||||
*/
|
||||
protected $roleStorage;
|
||||
|
||||
/**
|
||||
* {@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('entity.manager')->getStorage('moderation_state'),
|
||||
$container->get('entity.manager')->getStorage('user_role')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateTransitionListBuilder.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* Entity Type.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $transition_storage
|
||||
* Moderation state transition entity storage.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $state_storage
|
||||
* Moderation state entity storage.
|
||||
* @param \Drupal\user\RoleStorageInterface $role_storage
|
||||
* The role storage.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $transition_storage, EntityStorageInterface $state_storage, RoleStorageInterface $role_storage) {
|
||||
parent::__construct($entity_type, $transition_storage);
|
||||
$this->stateStorage = $state_storage;
|
||||
$this->roleStorage = $role_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_moderation_transition_list';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['to'] = $this->t('To state');
|
||||
$header['label'] = $this->t('Button label');
|
||||
$header['roles'] = $this->t('Allowed roles');
|
||||
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
$row['to']['#markup'] = $this->stateStorage->load($entity->getToState())->label();
|
||||
$row['label'] = $entity->label();
|
||||
$row['roles']['#markup'] = implode(', ', user_role_names(FALSE, 'use ' . $entity->id() . ' transition'));
|
||||
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
$build = parent::render();
|
||||
|
||||
$build['item'] = [
|
||||
'#type' => 'item',
|
||||
'#markup' => $this->t('On this screen you can define <em>transitions</em>. Every time an entity is saved, it undergoes a transition. It is not possible to save an entity if it tries do a transition not defined here. Transitions do not necessarily mean a state change, it is possible to transition from a state to the same state but that transition needs to be defined here as well.'),
|
||||
'#weight' => -5,
|
||||
];
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$this->entities = $this->load();
|
||||
|
||||
// Get all the moderation states and sort them by weight.
|
||||
$states = $this->stateStorage->loadMultiple();
|
||||
uasort($states, array($this->entityType->getClass(), 'sort'));
|
||||
|
||||
/** @var \Drupal\content_moderation\ModerationStateTransitionInterface $entity */
|
||||
$groups = array_fill_keys(array_keys($states), []);
|
||||
foreach ($this->entities as $entity) {
|
||||
$groups[$entity->getFromState()][] = $entity;
|
||||
}
|
||||
|
||||
foreach ($groups as $group_name => $entities) {
|
||||
$form[$group_name] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('From @state to...', ['@state' => $states[$group_name]->label()]),
|
||||
// Make sure that the first group is always open.
|
||||
'#open' => $group_name === array_keys($groups)[0],
|
||||
];
|
||||
|
||||
$form[$group_name][$this->entitiesKey] = array(
|
||||
'#type' => 'table',
|
||||
'#header' => $this->buildHeader(),
|
||||
'#empty' => t('There is no @label yet.', array('@label' => $this->entityType->getLabel())),
|
||||
'#tabledrag' => array(
|
||||
array(
|
||||
'action' => 'order',
|
||||
'relationship' => 'sibling',
|
||||
'group' => 'weight',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$delta = 10;
|
||||
// Change the delta of the weight field if have more than 20 entities.
|
||||
if (!empty($this->weightKey)) {
|
||||
$count = count($this->entities);
|
||||
if ($count > 20) {
|
||||
$delta = ceil($count / 2);
|
||||
}
|
||||
}
|
||||
foreach ($entities as $entity) {
|
||||
$row = $this->buildRow($entity);
|
||||
if (isset($row['label'])) {
|
||||
$row['label'] = array('#markup' => $row['label']);
|
||||
}
|
||||
if (isset($row['weight'])) {
|
||||
$row['weight']['#delta'] = $delta;
|
||||
}
|
||||
$form[$group_name][$this->entitiesKey][$entity->id()] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$form['actions']['#type'] = 'actions';
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save order'),
|
||||
'#button_type' => 'primary',
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @todo: If the parent class is ever cleaned up to use EntityTypeManager
|
||||
* instead of Entity manager, this method will also need to be adjusted.
|
||||
*/
|
||||
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->hasForwardRevisionFlag($definition) || $this->isEditFormPage($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the route definition includes a forward-revision flag.
|
||||
*
|
||||
* This is a custom flag defined by the Content Moderation module to load
|
||||
* forward 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 forward revision flag is set, FALSE otherwise.
|
||||
*/
|
||||
protected function hasForwardRevisionFlag(array $definition) {
|
||||
return (isset($definition['load_forward_revision']) && $definition['load_forward_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 the entity type is translatable, ensure we return the proper
|
||||
// translation object for the current context.
|
||||
if ($latest_revision instanceof EntityInterface && $entity instanceof TranslatableInterface) {
|
||||
$latest_revision = $this->entityManager->getTranslationFromContext($latest_revision, NULL, array('operation' => 'entity_upcast'));
|
||||
}
|
||||
|
||||
if ($latest_revision->isRevisionTranslationAffected()) {
|
||||
$entity = $latest_revision;
|
||||
}
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
43
web/core/modules/content_moderation/src/Permissions.php
Normal file
43
web/core/modules/content_moderation/src/Permissions.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
|
||||
/**
|
||||
* Defines a class for dynamic permissions based on transitions.
|
||||
*/
|
||||
class Permissions {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Returns an array of transition permissions.
|
||||
*
|
||||
* @return array
|
||||
* The transition permissions.
|
||||
*/
|
||||
public function transitionPermissions() {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$perms = [];
|
||||
/* @var \Drupal\content_moderation\ModerationStateInterface[] $states */
|
||||
$states = ModerationState::loadMultiple();
|
||||
/* @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
|
||||
foreach (ModerationStateTransition::loadMultiple() as $id => $transition) {
|
||||
$perms['use ' . $id . ' transition'] = [
|
||||
'title' => $this->t('Use the %transition_name transition', [
|
||||
'%transition_name' => $transition->label(),
|
||||
]),
|
||||
'description' => $this->t('Move content from %from state to %to state.', [
|
||||
'%from' => $states[$transition->getFromState()]->label(),
|
||||
'%to' => $states[$transition->getToState()]->label(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return $perms;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Alternate action plugin that can opt-out of modifying moderated entities.
|
||||
*
|
||||
* @see \Drupal\node\Plugin\Action\PublishNode
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 execute($entity = NULL) {
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t('One or more entities were skipped as they are under moderation and may not be directly published or unpublished.'));
|
||||
return;
|
||||
}
|
||||
|
||||
parent::execute($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
$result = parent::access($object, $account, TRUE)
|
||||
->andif(AccessResult::forbiddenIf($this->moderationInfo->isModeratedEntity($object))->addCacheableDependency($object));
|
||||
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Alternate action plugin that can opt-out of modifying moderated entities.
|
||||
*
|
||||
* @see \Drupal\node\Plugin\Action\UnpublishNode
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 execute($entity = NULL) {
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t('One or more entities were skipped as they are under moderation and may not be directly published or unpublished.'));
|
||||
return;
|
||||
}
|
||||
|
||||
parent::execute($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
$result = parent::access($object, $account, TRUE)
|
||||
->andif(AccessResult::forbiddenIf($this->moderationInfo->isModeratedEntity($object))->addCacheableDependency($object));
|
||||
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Generates moderation-related local tasks.
|
||||
*/
|
||||
class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The base plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $basePluginId;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* Creates an FieldUiLocalTask object.
|
||||
*
|
||||
* @param string $base_plugin_id
|
||||
* The base plugin ID.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, ModerationInformationInterface $moderation_information) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->basePluginId = $base_plugin_id;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$base_plugin_id,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$this->derivatives = [];
|
||||
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
|
||||
$bundle_id = $entity_type->getBundleEntityType();
|
||||
$this->derivatives["$bundle_id.moderation_tab"] = [
|
||||
'route_name' => "entity.$bundle_id.moderation",
|
||||
'title' => $this->t('Manage moderation'),
|
||||
// @todo - are we sure they all have an edit_form?
|
||||
'base_route' => "entity.$bundle_id.edit_form",
|
||||
'weight' => 30,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
}
|
||||
|
||||
$latest_version_entities = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInfo->canModerateEntitiesOfEntityType($type) && $type->hasLinkTemplate('latest-version');
|
||||
});
|
||||
|
||||
foreach ($latest_version_entities as $entity_type_id => $entity_type) {
|
||||
$this->derivatives["$entity_type_id.latest_version_tab"] = [
|
||||
'route_name' => "entity.$entity_type_id.latest_version",
|
||||
'title' => $this->t('Latest version'),
|
||||
'base_route' => "entity.$entity_type_id.canonical",
|
||||
'weight' => 1,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget;
|
||||
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 Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'moderation_state_default' widget.
|
||||
*
|
||||
* @FieldWidget(
|
||||
* id = "moderation_state_default",
|
||||
* label = @Translation("Moderation state"),
|
||||
* field_types = {
|
||||
* "entity_reference"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ModerationStateWidget extends OptionsSelectWidget implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Current user service.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* Moderation state transition entity query.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryInterface
|
||||
*/
|
||||
protected $moderationStateTransitionEntityQuery;
|
||||
|
||||
/**
|
||||
* Moderation state storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $moderationStateStorage;
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformation
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Moderation state transition storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $moderationStateTransitionStorage;
|
||||
|
||||
/**
|
||||
* Moderation state transition validation service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* Constructs a new ModerationStateWidget object.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* Plugin id.
|
||||
* @param mixed $plugin_definition
|
||||
* Plugin definition.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* Field definition.
|
||||
* @param array $settings
|
||||
* Field settings.
|
||||
* @param array $third_party_settings
|
||||
* Third party settings.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* Current user service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_storage
|
||||
* Moderation state storage.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_transition_storage
|
||||
* Moderation state transition storage.
|
||||
* @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
|
||||
* Moderation transition entity query service.
|
||||
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
|
||||
* Moderation information service.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $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, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||
$this->moderationStateTransitionEntityQuery = $entity_query;
|
||||
$this->moderationStateTransitionStorage = $moderation_state_transition_storage;
|
||||
$this->moderationStateStorage = $moderation_state_storage;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->currentUser = $current_user;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$configuration['field_definition'],
|
||||
$configuration['settings'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->get('current_user'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_type.manager')->getStorage('moderation_state'),
|
||||
$container->get('entity_type.manager')->getStorage('moderation_state_transition'),
|
||||
$container->get('entity.query')->get('moderation_state_transition', 'AND'),
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('content_moderation.state_transition_validation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
|
||||
/** @var ContentEntityInterface $entity */
|
||||
$entity = $items->getEntity();
|
||||
|
||||
/* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle_entity */
|
||||
$bundle_entity = $this->entityTypeManager->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle());
|
||||
if (!$this->moderationInformation->isModeratedEntity($entity)) {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
return $element + ['#access' => FALSE];
|
||||
}
|
||||
|
||||
$default = $items->get($delta)->value ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE);
|
||||
/** @var \Drupal\content_moderation\ModerationStateInterface $default_state */
|
||||
$default_state = $this->entityTypeManager->getStorage('moderation_state')->load($default);
|
||||
if (!$default || !$default_state) {
|
||||
throw new \UnexpectedValueException(sprintf('The %s bundle has an invalid moderation state configuration, moderation states are enabled but no default is set.', $bundle_entity->label()));
|
||||
}
|
||||
|
||||
$transitions = $this->validator->getValidTransitions($entity, $this->currentUser);
|
||||
|
||||
$target_states = [];
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationStateTransition $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->getToState()] = $transition->label();
|
||||
}
|
||||
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$element += [
|
||||
'#access' => FALSE,
|
||||
'#type' => 'select',
|
||||
'#options' => $target_states,
|
||||
'#default_value' => $default,
|
||||
'#published' => $default ? $default_state->isPublishedState() : FALSE,
|
||||
'#key_column' => $this->column,
|
||||
];
|
||||
$element['#element_validate'][] = array(get_class($this), 'validateElement');
|
||||
|
||||
// Use the dropbutton.
|
||||
$element['#process'][] = [get_called_class(), 'processActions'];
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity builder updating the node moderation state with the submitted value.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type identifier.
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity updated with the submitted values.
|
||||
* @param array $form
|
||||
* The complete form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public static function updateStatus($entity_type_id, ContentEntityInterface $entity, array $form, FormStateInterface $form_state) {
|
||||
$element = $form_state->getTriggeringElement();
|
||||
if (isset($element['#moderation_state'])) {
|
||||
$entity->moderation_state->target_id = $element['#moderation_state'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process callback to alter action buttons.
|
||||
*/
|
||||
public static function processActions($element, FormStateInterface $form_state, array &$form) {
|
||||
|
||||
// We'll steal most of the button configuration from the default submit
|
||||
// button. However, NodeForm also hides that button for admins (as it adds
|
||||
// its own, too), so we have to restore it.
|
||||
$default_button = $form['actions']['submit'];
|
||||
$default_button['#access'] = TRUE;
|
||||
|
||||
// Add a custom button for each transition we're allowing. The #dropbutton
|
||||
// property tells FAPI to cluster them all together into a single widget.
|
||||
$options = $element['#options'];
|
||||
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
$translatable = !$entity->isNew() && $entity->isTranslatable();
|
||||
foreach ($options as $id => $label) {
|
||||
$button = [
|
||||
'#dropbutton' => 'save',
|
||||
'#moderation_state' => $id,
|
||||
'#weight' => -10,
|
||||
];
|
||||
|
||||
$button['#value'] = $translatable
|
||||
? t('Save and @transition (this translation)', ['@transition' => $label])
|
||||
: t('Save and @transition', ['@transition' => $label]);
|
||||
|
||||
$form['actions']['moderation_state_' . $id] = $button + $default_button;
|
||||
}
|
||||
|
||||
// Hide the default buttons, including the specialty ones added by
|
||||
// NodeForm.
|
||||
foreach (['publish', 'unpublish', 'submit'] as $key) {
|
||||
$form['actions'][$key]['#access'] = FALSE;
|
||||
unset($form['actions'][$key]['#dropbutton']);
|
||||
}
|
||||
|
||||
// Setup a callback to translate the button selection back into field
|
||||
// widget, so that it will get saved properly.
|
||||
$form['#entity_builders']['update_moderation_state'] = [get_called_class(), 'updateStatus'];
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition) {
|
||||
return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\Core\Field\EntityReferenceFieldItemList;
|
||||
|
||||
/**
|
||||
* A computed field that provides a content entity's moderation state.
|
||||
*
|
||||
* It links content entities to a moderation state configuration entity via a
|
||||
* moderation state content entity.
|
||||
*/
|
||||
class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
|
||||
|
||||
/**
|
||||
* Gets the moderation state entity linked to a content entity revision.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ModerationStateInterface|null
|
||||
* The moderation state configuration entity linked to a content entity
|
||||
* revision.
|
||||
*/
|
||||
protected function getModerationState() {
|
||||
$entity = $this->getEntity();
|
||||
|
||||
if (!\Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ($entity->id() && $entity->getRevisionId()) {
|
||||
$revisions = \Drupal::service('entity.query')->get('content_moderation_state')
|
||||
->condition('content_entity_type_id', $entity->getEntityTypeId())
|
||||
->condition('content_entity_id', $entity->id())
|
||||
->condition('content_entity_revision_id', $entity->getRevisionId())
|
||||
->allRevisions()
|
||||
->sort('revision_id', 'DESC')
|
||||
->execute();
|
||||
|
||||
if ($revision_to_load = key($revisions)) {
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = \Drupal::entityTypeManager()
|
||||
->getStorage('content_moderation_state')
|
||||
->loadRevision($revision_to_load);
|
||||
|
||||
// Return the correct translation.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$langcode = $entity->language()->getId();
|
||||
if (!$content_moderation_state->hasTranslation($langcode)) {
|
||||
$content_moderation_state->addTranslation($langcode);
|
||||
}
|
||||
if ($content_moderation_state->language()->getId() !== $langcode) {
|
||||
$content_moderation_state = $content_moderation_state->getTranslation($langcode);
|
||||
}
|
||||
}
|
||||
|
||||
return $content_moderation_state->get('moderation_state')->entity;
|
||||
}
|
||||
}
|
||||
// It is possible that the bundle does not exist at this point. For example,
|
||||
// the node type form creates a fake Node entity to get default values.
|
||||
// @see \Drupal\node\NodeTypeForm::form()
|
||||
$bundle_entity = \Drupal::entityTypeManager()
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())
|
||||
->load($entity->bundle());
|
||||
if ($bundle_entity && ($default = $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state'))) {
|
||||
return ModerationState::load($default);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get($index) {
|
||||
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->getModerationState();
|
||||
// Do not store NULL values in the static cache.
|
||||
if ($moderation_state) {
|
||||
$this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
104
web/core/modules/content_moderation/src/Plugin/Menu/EditTab.php
Normal file
104
web/core/modules/content_moderation/src/Plugin/Menu/EditTab.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Menu;
|
||||
|
||||
use Drupal\Core\Menu\LocalTaskDefault;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for making the edit tab use 'Edit draft' or 'New draft'.
|
||||
*/
|
||||
class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformation
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* The entity.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* Constructs a new EditTab object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* Plugin configuration.
|
||||
* @param string $plugin_id
|
||||
* Plugin ID.
|
||||
* @param mixed $plugin_definition
|
||||
* Plugin definition.
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation service.
|
||||
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, TranslationInterface $string_translation, ModerationInformation $moderation_information) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('string_translation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRouteParameters(RouteMatchInterface $route_match) {
|
||||
// Override the node here with the latest revision.
|
||||
$this->entity = $route_match->getParameter($this->pluginDefinition['entity_type_id']);
|
||||
return parent::getRouteParameters($route_match);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTitle() {
|
||||
if (!$this->moderationInfo->isModeratedEntity($this->entity)) {
|
||||
// Moderation isn't enabled.
|
||||
return parent::getTitle();
|
||||
}
|
||||
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
return $this->moderationInfo->isLiveRevision($this->entity)
|
||||
? $this->t('New draft')
|
||||
: $this->t('Edit draft');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheTags() {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
$tags = parent::getCacheTags();
|
||||
// Tab changes if node or node-type is modified.
|
||||
$tags = array_merge($tags, $this->entity->getCacheTags());
|
||||
$tags[] = $this->entity->getEntityType()->getBundleEntityType() . ':' . $this->entity->bundle();
|
||||
return $tags;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Verifies that nodes have a valid moderation state.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "ModerationState",
|
||||
* label = @Translation("Valid moderation state", context = "Validation")
|
||||
* )
|
||||
*/
|
||||
class ModerationStateConstraint extends Constraint {
|
||||
|
||||
public $message = 'Invalid state transition from %from to %to';
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\content_moderation\Entity\ModerationState as ModerationStateEntity;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Checks if a moderation state transition is valid.
|
||||
*/
|
||||
class ModerationStateConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The state transition validation.
|
||||
*
|
||||
* @var \Drupal\content_moderation\StateTransitionValidation
|
||||
*/
|
||||
protected $validation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
private $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation info.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationStateConstraintValidator instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\StateTransitionValidation $validation
|
||||
* The state transition validation.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, StateTransitionValidation $validation, ModerationInformationInterface $moderation_information) {
|
||||
$this->validation = $validation;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('content_moderation.state_transition_validation'),
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint) {
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
$entity = $value->getEntity();
|
||||
|
||||
// Ignore entities that are not subject to moderation anyway.
|
||||
if (!$this->moderationInformation->isModeratedEntity($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore entities that are being created for the first time.
|
||||
if ($entity->isNew()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore entities that are being moderated for the first time, such as
|
||||
// when they existed before moderation was enabled for this entity type.
|
||||
if ($this->isFirstTimeModeration($entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
|
||||
if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) {
|
||||
$original_entity = $original_entity->getTranslation($entity->language()->getId());
|
||||
}
|
||||
|
||||
if ($entity->moderation_state->target_id) {
|
||||
$new_state_id = $entity->moderation_state->target_id;
|
||||
}
|
||||
else {
|
||||
$new_state_id = $default = $this->entityTypeManager
|
||||
->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
|
||||
->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
}
|
||||
if ($new_state_id) {
|
||||
$new_state = ModerationStateEntity::load($new_state_id);
|
||||
}
|
||||
// @todo - what if $new_state_id references something that does not exist or
|
||||
// is null.
|
||||
if (!$this->validation->isTransitionAllowed($original_entity->moderation_state->entity, $new_state)) {
|
||||
$this->context->addViolation($constraint->message, ['%from' => $original_entity->moderation_state->entity->label(), '%to' => $new_state->label()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this entity is being moderated for the first time.
|
||||
*
|
||||
* If the previous version of the entity has no moderation state, we assume
|
||||
* that means it predates the presence of moderation states.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being moderated.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this is the entity's first time being moderated, FALSE otherwise.
|
||||
*/
|
||||
protected function isFirstTimeModeration(EntityInterface $entity) {
|
||||
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
|
||||
|
||||
$original_id = $original_entity->moderation_state->target_id;
|
||||
|
||||
return !($entity->moderation_state->target_id && $original_entity && $original_id);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\views\filter;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\views\Plugin\views\filter\FilterPluginBase;
|
||||
use Drupal\views\Plugin\ViewsHandlerManager;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Filter to show only the latest revision of an entity.
|
||||
*
|
||||
* @ingroup views_filter_handlers
|
||||
*
|
||||
* @ViewsFilter("latest_revision")
|
||||
*/
|
||||
class LatestRevision extends FilterPluginBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Entity Type Manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Views Handler Plugin Manager.
|
||||
*
|
||||
* @var \Drupal\views\Plugin\ViewsHandlerManager
|
||||
*/
|
||||
protected $joinHandler;
|
||||
|
||||
/**
|
||||
* Database Connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Constructs a new LatestRevision.
|
||||
*
|
||||
* @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
|
||||
* Entity Type Manager Service.
|
||||
* @param \Drupal\views\Plugin\ViewsHandlerManager $join_handler
|
||||
* Views Handler Plugin Manager.
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* Database Connection.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ViewsHandlerManager $join_handler, Connection $connection) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->joinHandler = $join_handler;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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('plugin.manager.views.join'),
|
||||
$container->get('database')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function adminSummary() {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function operatorForm(&$form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canExpose() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
// The table doesn't exist until a moderated node has been saved at least
|
||||
// once. Just in case, disable this filter until then. Note that this means
|
||||
// the view will still show all revisions, not just latest, but this is
|
||||
// sufficiently edge-case-y that it's probably not worth the time to
|
||||
// handle more robustly.
|
||||
if (!$this->connection->schema()->tableExists('content_revision_tracker')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $this->ensureMyTable();
|
||||
|
||||
/** @var \Drupal\views\Plugin\views\query\Sql $query */
|
||||
$query = $this->query;
|
||||
|
||||
$definition = $this->entityTypeManager->getDefinition($this->getEntityType());
|
||||
$keys = $definition->getKeys();
|
||||
|
||||
$definition = [
|
||||
'table' => 'content_revision_tracker',
|
||||
'type' => 'INNER',
|
||||
'field' => 'entity_id',
|
||||
'left_table' => $table,
|
||||
'left_field' => $keys['id'],
|
||||
'extra' => [
|
||||
['left_field' => $keys['langcode'], 'field' => 'langcode'],
|
||||
['left_field' => $keys['revision'], 'field' => 'revision_id'],
|
||||
['field' => 'entity_type', 'value' => $this->getEntityType()],
|
||||
],
|
||||
];
|
||||
|
||||
$join = $this->joinHandler->createInstance('standard', $definition);
|
||||
|
||||
$query->ensureTable('content_revision_tracker', $this->relationship, $join);
|
||||
}
|
||||
|
||||
}
|
152
web/core/modules/content_moderation/src/RevisionTracker.php
Normal file
152
web/core/modules/content_moderation/src/RevisionTracker.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Database\DatabaseExceptionWrapper;
|
||||
use Drupal\Core\Database\SchemaObjectExistsException;
|
||||
|
||||
/**
|
||||
* Tracks metadata about revisions across entities.
|
||||
*/
|
||||
class RevisionTracker implements RevisionTrackerInterface {
|
||||
|
||||
/**
|
||||
* The name of the SQL table we use for tracking.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Constructs a new RevisionTracker.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection.
|
||||
* @param string $table
|
||||
* The table that should be used for tracking.
|
||||
*/
|
||||
public function __construct(Connection $connection, $table = 'content_revision_tracker') {
|
||||
$this->connection = $connection;
|
||||
$this->tableName = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) {
|
||||
try {
|
||||
$this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
}
|
||||
catch (DatabaseExceptionWrapper $e) {
|
||||
$this->ensureTableExists();
|
||||
$this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the latest revision of a given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The machine name of the type of entity.
|
||||
* @param string $entity_id
|
||||
* The Entity ID in question.
|
||||
* @param string $langcode
|
||||
* The langcode of the revision we're saving. Each language has its own
|
||||
* effective tree of entity revisions, so in different languages
|
||||
* different revisions will be "latest".
|
||||
* @param int $revision_id
|
||||
* The revision ID that is now the latest revision.
|
||||
*
|
||||
* @return int
|
||||
* One of the valid returns from a merge query's execute method.
|
||||
*/
|
||||
protected function recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) {
|
||||
return $this->connection->merge($this->tableName)
|
||||
->keys([
|
||||
'entity_type' => $entity_type_id,
|
||||
'entity_id' => $entity_id,
|
||||
'langcode' => $langcode,
|
||||
])
|
||||
->fields([
|
||||
'revision_id' => $revision_id,
|
||||
])
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the table exists and create it if not.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the table was created, FALSE otherwise.
|
||||
*/
|
||||
protected function ensureTableExists() {
|
||||
try {
|
||||
if (!$this->connection->schema()->tableExists($this->tableName)) {
|
||||
$this->connection->schema()->createTable($this->tableName, $this->schemaDefinition());
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
catch (SchemaObjectExistsException $e) {
|
||||
// If another process has already created the table, attempting to
|
||||
// recreate it will throw an exception. In this case just catch the
|
||||
// exception and do nothing.
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the schema for the tracker table.
|
||||
*
|
||||
* @return array
|
||||
* The schema API definition for the SQL storage table.
|
||||
*/
|
||||
protected function schemaDefinition() {
|
||||
$schema = [
|
||||
'description' => 'Tracks the latest revision for any entity',
|
||||
'fields' => [
|
||||
'entity_type' => [
|
||||
'description' => 'The entity type',
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'entity_id' => [
|
||||
'description' => 'The entity ID',
|
||||
'type' => 'int',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
'langcode' => [
|
||||
'description' => 'The language of the entity revision',
|
||||
'type' => 'varchar',
|
||||
'length' => 12,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
],
|
||||
'revision_id' => [
|
||||
'description' => 'The latest revision ID for this entity',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
],
|
||||
],
|
||||
'primary key' => ['entity_type', 'entity_id', 'langcode'],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
/**
|
||||
* Tracks metadata about revisions across content entities.
|
||||
*/
|
||||
interface RevisionTrackerInterface {
|
||||
|
||||
/**
|
||||
* Sets the latest revision of a given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The machine name of the type of entity.
|
||||
* @param string $entity_id
|
||||
* The Entity ID in question.
|
||||
* @param string $langcode
|
||||
* The langcode of the revision we're saving. Each language has its own
|
||||
* effective tree of entity revisions, so in different languages
|
||||
* different revisions will be "latest".
|
||||
* @param int $revision_id
|
||||
* The revision ID that is now the latest revision.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id);
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Dynamic route provider for the Content moderation module.
|
||||
*
|
||||
* Provides the following routes:
|
||||
* - The latest version tab, showing the latest revision of an entity, not the
|
||||
* default one.
|
||||
*/
|
||||
class EntityModerationRouteProvider implements EntityRouteProviderInterface, EntityHandlerInterface {
|
||||
|
||||
/**
|
||||
* The entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* Constructs a new DefaultHtmlRouteProvider.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
*/
|
||||
public function __construct(EntityFieldManagerInterface $entity_manager) {
|
||||
$this->entityFieldManager = $entity_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_field.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoutes(EntityTypeInterface $entity_type) {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
if ($moderation_route = $this->getLatestVersionRoute($entity_type)) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$collection->add("entity.{$entity_type_id}.latest_version", $moderation_route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moderation-form route.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route|null
|
||||
* The generated route, if available.
|
||||
*/
|
||||
protected function getLatestVersionRoute(EntityTypeInterface $entity_type) {
|
||||
if ($entity_type->hasLinkTemplate('latest-version') && $entity_type->hasViewBuilderClass()) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$route = new Route($entity_type->getLinkTemplate('latest-version'));
|
||||
$route
|
||||
->addDefaults([
|
||||
'_entity_view' => "{$entity_type_id}.full",
|
||||
'_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title',
|
||||
])
|
||||
// If the entity type is a node, unpublished content will be visible
|
||||
// if the user has the "view all unpublished content" permission.
|
||||
->setRequirement('_entity_access', "{$entity_type_id}.view")
|
||||
->setRequirement('_permission', 'view latest version,view any unpublished content')
|
||||
->setRequirement('_content_moderation_latest_version', 'TRUE')
|
||||
->setOption('_content_moderation_entity_type', $entity_type_id)
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => [
|
||||
'type' => 'entity:' . $entity_type_id,
|
||||
'load_forward_revision' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
// Entity types with serial IDs can specify this in their route
|
||||
// requirements, improving the matching process.
|
||||
if ($this->getEntityTypeIdKeyType($entity_type) === 'integer') {
|
||||
$route->setRequirement($entity_type_id, '\d+');
|
||||
}
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of the ID key for a given entity type.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* An entity type.
|
||||
*
|
||||
* @return string|null
|
||||
* The type of the ID key for a given entity type, or NULL if the entity
|
||||
* type does not support fields.
|
||||
*/
|
||||
protected function getEntityTypeIdKeyType(EntityTypeInterface $entity_type) {
|
||||
if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
|
||||
return $field_storage_definitions[$entity_type->getKey('id')]->getType();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Routing;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\Routing\EntityRouteProviderInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Provides the moderation configuration routes for config entities.
|
||||
*/
|
||||
class EntityTypeModerationRouteProvider implements EntityRouteProviderInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoutes(EntityTypeInterface $entity_type) {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
if ($moderation_route = $this->getModerationFormRoute($entity_type)) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
$collection->add("entity.{$entity_type_id}.moderation", $moderation_route);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the moderation-form route.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\Route|null
|
||||
* The generated route, if available.
|
||||
*/
|
||||
protected function getModerationFormRoute(EntityTypeInterface $entity_type) {
|
||||
if ($entity_type->hasLinkTemplate('moderation-form') && $entity_type->getFormClass('moderation')) {
|
||||
$entity_type_id = $entity_type->id();
|
||||
|
||||
$route = new Route($entity_type->getLinkTemplate('moderation-form'));
|
||||
|
||||
// @todo Come up with a new permission.
|
||||
$route
|
||||
->setDefaults([
|
||||
'_entity_form' => "{$entity_type_id}.moderation",
|
||||
'_title' => 'Moderation',
|
||||
])
|
||||
->setRequirement('_permission', 'administer moderation states')
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
|
||||
]);
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
*/
|
||||
class StateTransitionValidation implements StateTransitionValidationInterface {
|
||||
|
||||
/**
|
||||
* Entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Entity query factory.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryFactory
|
||||
*/
|
||||
protected $queryFactory;
|
||||
|
||||
/**
|
||||
* Stores the possible state transitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $possibleTransitions = [];
|
||||
|
||||
/**
|
||||
* Constructs a new StateTransitionValidation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
|
||||
* The entity query factory.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->queryFactory = $query_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a mapping of possible transitions.
|
||||
*
|
||||
* This method is uncached and will recalculate the list on every request.
|
||||
* In most cases you want to use getPossibleTransitions() instead.
|
||||
*
|
||||
* @see static::getPossibleTransitions()
|
||||
*
|
||||
* @return array[]
|
||||
* An array containing all possible transitions. Each entry is keyed by the
|
||||
* "from" state, and the value is an array of all legal "to" states based
|
||||
* on the currently defined transition objects.
|
||||
*/
|
||||
protected function calculatePossibleTransitions() {
|
||||
$transitions = $this->transitionStorage()->loadMultiple();
|
||||
|
||||
$possible_transitions = [];
|
||||
/** @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
|
||||
foreach ($transitions as $transition) {
|
||||
$possible_transitions[$transition->getFromState()][] = $transition->getToState();
|
||||
}
|
||||
return $possible_transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mapping of possible transitions.
|
||||
*
|
||||
* @return array[]
|
||||
* An array containing all possible transitions. Each entry is keyed by the
|
||||
* "from" state, and the value is an array of all legal "to" states based
|
||||
* on the currently defined transition objects.
|
||||
*/
|
||||
protected function getPossibleTransitions() {
|
||||
if (empty($this->possibleTransitions)) {
|
||||
$this->possibleTransitions = $this->calculatePossibleTransitions();
|
||||
}
|
||||
return $this->possibleTransitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user) {
|
||||
$bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
|
||||
|
||||
$states_for_bundle = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
|
||||
$all_transitions = $this->getPossibleTransitions();
|
||||
$destination_ids = $all_transitions[$current_state->id()];
|
||||
|
||||
$destination_ids = array_intersect($states_for_bundle, $destination_ids);
|
||||
$destinations = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($destination_ids);
|
||||
|
||||
return array_filter($destinations, function(ModerationStateInterface $destination_state) use ($current_state, $user) {
|
||||
return $this->userMayTransition($current_state, $destination_state, $user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
|
||||
$bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
|
||||
|
||||
/** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
|
||||
$current_state = $entity->moderation_state->entity;
|
||||
$current_state_id = $current_state ? $current_state->id() : $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state');
|
||||
|
||||
// Determine the states that are legal on this bundle.
|
||||
$legal_bundle_states = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
|
||||
|
||||
// Legal transitions include those that are possible from the current state,
|
||||
// filtered by those whose target is legal on this bundle and that the
|
||||
// user has access to execute.
|
||||
$transitions = array_filter($this->getTransitionsFrom($current_state_id), function(ModerationStateTransition $transition) use ($legal_bundle_states, $user) {
|
||||
return in_array($transition->getToState(), $legal_bundle_states, TRUE)
|
||||
&& $user->hasPermission('use ' . $transition->id() . ' transition');
|
||||
});
|
||||
|
||||
return $transitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of possible transitions from a given state.
|
||||
*
|
||||
* This list is based only on those transitions that exist, not what
|
||||
* transitions are legal in a given context.
|
||||
*
|
||||
* @param string $state_name
|
||||
* The machine name of the state from which we are transitioning.
|
||||
*
|
||||
* @return ModerationStateTransition[]
|
||||
* A list of possible transitions from a given state.
|
||||
*/
|
||||
protected function getTransitionsFrom($state_name) {
|
||||
$result = $this->transitionStateQuery()
|
||||
->condition('stateFrom', $state_name)
|
||||
->sort('weight')
|
||||
->execute();
|
||||
|
||||
return $this->transitionStorage()->loadMultiple($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user) {
|
||||
if ($transition = $this->getTransitionFromStates($from, $to)) {
|
||||
return $user->hasPermission('use ' . $transition->id() . ' transition');
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transition object that transitions from one state to another.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
*
|
||||
* @return ModerationStateTransition|null
|
||||
* A transition object, or NULL if there is no such transition.
|
||||
*/
|
||||
protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) {
|
||||
$from = $this->transitionStateQuery()
|
||||
->condition('stateFrom', $from->id())
|
||||
->condition('stateTo', $to->id())
|
||||
->execute();
|
||||
|
||||
$transitions = $this->transitionStorage()->loadMultiple($from);
|
||||
|
||||
if ($transitions) {
|
||||
return current($transitions);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to) {
|
||||
$allowed_transitions = $this->calculatePossibleTransitions();
|
||||
if (isset($allowed_transitions[$from->id()])) {
|
||||
return in_array($to->id(), $allowed_transitions[$from->id()], TRUE);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a transition state entity query.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\Query\QueryInterface
|
||||
* A transition state entity query.
|
||||
*/
|
||||
protected function transitionStateQuery() {
|
||||
return $this->queryFactory->get('moderation_state_transition', 'AND');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transition entity storage service.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityStorageInterface
|
||||
* The transition state entity storage.
|
||||
*/
|
||||
protected function transitionStorage() {
|
||||
return $this->entityTypeManager->getStorage('moderation_state_transition');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state entity storage service.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityStorageInterface
|
||||
* The moderation state entity storage.
|
||||
*/
|
||||
protected function stateStorage() {
|
||||
return $this->entityTypeManager->getStorage('moderation_state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific bundle entity.
|
||||
*
|
||||
* @param string $bundle_entity_type_id
|
||||
* The bundle entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID.
|
||||
*
|
||||
* @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null
|
||||
* The specific bundle entity.
|
||||
*/
|
||||
protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) {
|
||||
return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
*/
|
||||
interface StateTransitionValidationInterface {
|
||||
|
||||
/**
|
||||
* Gets a list of states a user may transition an entity to.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to be transitioned.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The account that wants to perform a transition.
|
||||
*
|
||||
* @return \Drupal\content_moderation\Entity\ModerationState[]
|
||||
* Returns an array of States to which the specified user may transition the
|
||||
* entity.
|
||||
*/
|
||||
public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Gets a list of transitions that are legal for this user on this entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity to be transitioned.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The account that wants to perform a transition.
|
||||
*
|
||||
* @return \Drupal\content_moderation\Entity\ModerationStateTransition[]
|
||||
* The list of transitions that are legal for this user on this entity.
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Determines if a user is allowed to transition from one state to another.
|
||||
*
|
||||
* This method will also return FALSE if there is no transition between the
|
||||
* specified states at all.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
* @param \Drupal\Core\Session\AccountInterface $user
|
||||
* The user to validate.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the given user may transition between those two states.
|
||||
*/
|
||||
public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user);
|
||||
|
||||
/**
|
||||
* Determines a transition allowed.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $from
|
||||
* The origin state.
|
||||
* @param \Drupal\content_moderation\ModerationStateInterface $to
|
||||
* The destination state.
|
||||
*
|
||||
* @return bool
|
||||
* Is the transition allowed.
|
||||
*/
|
||||
public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to);
|
||||
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests the moderation form, specifically on nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationFormTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [
|
||||
'draft',
|
||||
'published',
|
||||
], 'draft');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the moderation form that shows on the latest version page.
|
||||
*
|
||||
* The latest version page only shows if there is a forward revision. There
|
||||
* is only a forward revision if a draft revision is created on a node where
|
||||
* the default revision is not a published moderation state.
|
||||
*
|
||||
* @see \Drupal\content_moderation\EntityOperations
|
||||
* @see \Drupal\content_moderation\Tests\ModerationStateBlockTest::testCustomBlockModeration
|
||||
*/
|
||||
public function testModerationForm() {
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'Some moderated content',
|
||||
'body[0][value]' => 'First version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
$node = $this->drupalGetNodeByTitle('Some moderated content');
|
||||
$canonical_path = sprintf('node/%d', $node->id());
|
||||
$edit_path = sprintf('node/%d/edit', $node->id());
|
||||
$latest_version_path = sprintf('node/%d/latest', $node->id());
|
||||
|
||||
$this->assertTrue($this->adminUser->hasPermission('edit any moderated_content content'));
|
||||
|
||||
// The latest version page should not show, because there is no forward
|
||||
// revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Update the draft.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Second version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// The latest version page should not show, because there is still no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Publish the draft.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Third version of the content.',
|
||||
], t('Save and Publish'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoText('Status', 'The node view page has no moderation form.');
|
||||
|
||||
// The latest version page should not show, because there is still no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Make a forward revision.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Fourth version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoText('Status', 'The node view page has no moderation form.');
|
||||
|
||||
// The latest version page should show the moderation form and have "Draft"
|
||||
// status, because the forward revision is in "Draft".
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertText('Status', 'Form text found on the latest-version page.');
|
||||
$this->assertText('Draft', 'Correct status found on the latest-version page.');
|
||||
|
||||
// Submit the moderation form to change status to published.
|
||||
$this->drupalPostForm($latest_version_path, [
|
||||
'new_state' => 'published',
|
||||
], t('Apply'));
|
||||
|
||||
// The latest version page should not show, because there is no
|
||||
// forward revision.
|
||||
$this->drupalGet($latest_version_path);
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Test content_moderation functionality with localization and translation.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationLocaleTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'locale',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests article translations can be moderated separately.
|
||||
*/
|
||||
public function testTranslateModeratedContent() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
// Enable moderation on Article node type.
|
||||
$this->createContentTypeFromUi(
|
||||
'Article',
|
||||
'article',
|
||||
TRUE,
|
||||
['draft', 'published', 'archived'],
|
||||
'draft'
|
||||
);
|
||||
|
||||
// Add French language.
|
||||
$edit = [
|
||||
'predefined_langcode' => 'fr',
|
||||
];
|
||||
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
|
||||
|
||||
// Enable content translation on articles.
|
||||
$this->drupalGet('admin/config/regional/content-language');
|
||||
$edit = [
|
||||
'entity_types[node]' => TRUE,
|
||||
'settings[node][article][translatable]' => TRUE,
|
||||
'settings[node][article][settings][language][language_alterable]' => TRUE,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
|
||||
// Adding languages requires a container rebuild in the test running
|
||||
// environment so that multilingual services are used.
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Create a published article in English.
|
||||
$edit = [
|
||||
'title[0][value]' => 'Published English node',
|
||||
'langcode[0][value]' => 'en',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Publish'));
|
||||
$this->assertText(t('Article Published English node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Published English node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'French node Draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
// Here the error has occurred "The website encountered an unexpected error.
|
||||
// Please try again later."
|
||||
// If the translation has got lost.
|
||||
$this->assertText(t('Article French node Draft has been updated.'));
|
||||
|
||||
// Create an article in English.
|
||||
$edit = [
|
||||
'title[0][value]' => 'English node',
|
||||
'langcode[0][value]' => 'en',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article English node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'French node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article French node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node', TRUE);
|
||||
|
||||
// Publish the English article and check that the translation stays
|
||||
// unpublished.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article English node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('English node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual('French node', $french_node->label());
|
||||
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'draft');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Create another article with its translation. This time we will publish
|
||||
// the translation first.
|
||||
$edit = [
|
||||
'title[0][value]' => 'Another node',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article Another node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node');
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'Translated node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article Translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
|
||||
// Publish the translation and check that the source language version stays
|
||||
// unpublished.
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article Translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'draft');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Now check that we can create a new draft of the translation.
|
||||
$edit = [
|
||||
'title[0][value]' => 'New draft of translated node',
|
||||
];
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', $edit, t('Save and Create New Draft (this translation)'));
|
||||
$this->assertText(t('Article New draft of translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($french_node->getTitle(), 'Translated node', 'The default revision of the published translation remains the same.');
|
||||
|
||||
// Publish the draft.
|
||||
$edit = [
|
||||
'new_state' => 'published',
|
||||
];
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/latest', $edit, t('Apply'));
|
||||
$this->assertText(t('The moderation state has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($french_node->getTitle(), 'New draft of translated node', 'The draft has replaced the published revision.');
|
||||
|
||||
// Publish the English article before testing the archive transition.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$this->assertText(t('Article Another node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'published');
|
||||
|
||||
// Archive the node and its translation.
|
||||
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)'));
|
||||
$this->assertText(t('Article Another node has been updated.'));
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)'));
|
||||
$this->assertText(t('Article New draft of translated node has been updated.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertEqual($english_node->moderation_state->target_id, 'archived');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->target_id, 'archived');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Create another article with its translation. This time publishing english
|
||||
// after creating a forward french revision.
|
||||
$edit = [
|
||||
'title[0][value]' => 'An english node',
|
||||
];
|
||||
$this->drupalPostForm('node/add/article', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('Article An english node has been created.'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Add a French translation.
|
||||
$this->drupalGet('node/' . $english_node->id() . '/translations');
|
||||
$this->clickLink(t('Add'));
|
||||
$edit = [
|
||||
'title[0][value]' => 'A french node',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save and Publish (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Create a forward revision
|
||||
$this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Create New Draft (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Publish the english node and the default french node not the latest
|
||||
// french node should be used.
|
||||
$this->drupalPostForm('/node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
|
||||
$english_node = $this->drupalGetNodeByTitle('An english node', TRUE);
|
||||
$french_node = $english_node->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
|
||||
/**
|
||||
* Tests general content moderation workflow for blocks.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateBlockTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create the "basic" block type.
|
||||
$bundle = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'basic',
|
||||
'revision' => FALSE,
|
||||
]);
|
||||
$bundle->save();
|
||||
|
||||
// Add the body field to it.
|
||||
block_content_add_body_field($bundle->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests moderating custom blocks.
|
||||
*
|
||||
* Blocks and any non-node-type-entities do not have a concept of
|
||||
* "published". As such, we must use the "default revision" to know what is
|
||||
* going to be "published", i.e. visible to the user.
|
||||
*
|
||||
* The one exception is a block that has never been "published". When a block
|
||||
* is first created, it becomes the "default revision". For each edit of the
|
||||
* block after that, Content Moderation checks the "default revision" to
|
||||
* see if it is set to a published moderation state. If it is not, the entity
|
||||
* being saved will become the "default revision".
|
||||
*
|
||||
* The test below is intended, in part, to make this behavior clear.
|
||||
*
|
||||
* @see \Drupal\content_moderation\EntityOperations::entityPresave
|
||||
* @see \Drupal\content_moderation\Tests\ModerationFormTest::testModerationForm
|
||||
*/
|
||||
public function testCustomBlockModeration() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
$this->drupalGet('admin/structure/block/block-content/types');
|
||||
$this->assertLinkByHref('admin/structure/block/block-content/manage/basic/moderation');
|
||||
$this->drupalGet('admin/structure/block/block-content/manage/basic');
|
||||
$this->assertLinkByHref('admin/structure/block/block-content/manage/basic/moderation');
|
||||
$this->drupalGet('admin/structure/block/block-content/manage/basic/moderation');
|
||||
|
||||
// Enable moderation for custom blocks at
|
||||
// admin/structure/block/block-content/manage/basic/moderation.
|
||||
$edit = [
|
||||
'enable_moderation_state' => TRUE,
|
||||
'allowed_moderation_states_unpublished[draft]' => TRUE,
|
||||
'allowed_moderation_states_published[published]' => TRUE,
|
||||
'default_moderation_state' => 'draft',
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
$this->assertText(t('Your settings have been saved.'));
|
||||
|
||||
// Create a custom block at block/add and save it as draft.
|
||||
$body = 'Body of moderated block';
|
||||
$edit = [
|
||||
'info[0][value]' => 'Moderated block',
|
||||
'body[0][value]' => $body,
|
||||
];
|
||||
$this->drupalPostForm('block/add', $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been created.'));
|
||||
|
||||
// Place the block in the Sidebar First region.
|
||||
$instance = array(
|
||||
'id' => 'moderated_block',
|
||||
'settings[label]' => $edit['info[0][value]'],
|
||||
'region' => 'sidebar_first',
|
||||
);
|
||||
$block = BlockContent::load(1);
|
||||
$url = 'admin/structure/block/add/block_content:' . $block->uuid() . '/' . $this->config('system.theme')->get('default');
|
||||
$this->drupalPostForm($url, $instance, t('Save block'));
|
||||
|
||||
// Navigate to home page and check that the block is visible. It should be
|
||||
// visible because it is the default revision.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($body);
|
||||
|
||||
// Update the block.
|
||||
$updated_body = 'This is the new body value';
|
||||
$edit = [
|
||||
'body[0][value]' => $updated_body,
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id(), $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been updated.'));
|
||||
|
||||
// Navigate to the home page and check that the block shows the updated
|
||||
// content. It should show the updated content because the block's default
|
||||
// revision is not a published moderation state.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($updated_body);
|
||||
|
||||
// Publish the block so we can create a forward revision.
|
||||
$this->drupalPostForm('block/' . $block->id(), [], t('Save and Publish'));
|
||||
|
||||
// Create a forward revision.
|
||||
$forward_revision_body = 'This is the forward revision body value';
|
||||
$edit = [
|
||||
'body[0][value]' => $forward_revision_body,
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id(), $edit, t('Save and Create New Draft'));
|
||||
$this->assertText(t('basic Moderated block has been updated.'));
|
||||
|
||||
// Navigate to home page and check that the forward revision doesn't show,
|
||||
// since it should not be set as the default revision.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($updated_body);
|
||||
|
||||
// Open the latest tab and publish the new draft.
|
||||
$edit = [
|
||||
'new_state' => 'published',
|
||||
];
|
||||
$this->drupalPostForm('block/' . $block->id() . '/latest', $edit, t('Apply'));
|
||||
$this->assertText(t('The moderation state has been updated.'));
|
||||
|
||||
// Navigate to home page and check that the forward revision is now the
|
||||
// default revision and therefore visible.
|
||||
$this->drupalGet('');
|
||||
$this->assertText($forward_revision_body);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\node\Entity\Node;
|
||||
|
||||
/**
|
||||
* Tests general content moderation workflow for nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateNodeTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi(
|
||||
'Moderated content',
|
||||
'moderated_content',
|
||||
TRUE,
|
||||
['draft', 'needs_review', 'published'],
|
||||
'draft'
|
||||
);
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests creating and deleting content.
|
||||
*/
|
||||
public function testCreatingContent() {
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], t('Save and Create New Draft'));
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
$node = reset($nodes);
|
||||
$this->assertEqual('draft', $node->moderation_state->target_id);
|
||||
|
||||
$path = 'node/' . $node->id() . '/edit';
|
||||
// Set up published revision.
|
||||
$this->drupalPostForm($path, [], t('Save and Publish'));
|
||||
\Drupal::entityTypeManager()->getStorage('node')->resetCache([$node->id()]);
|
||||
/* @var \Drupal\node\NodeInterface $node */
|
||||
$node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
|
||||
$this->assertTrue($node->isPublished());
|
||||
$this->assertEqual('published', $node->moderation_state->target_id);
|
||||
|
||||
// Verify that the state field is not shown.
|
||||
$this->assertNoText('Published');
|
||||
|
||||
// Delete the node.
|
||||
$this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
|
||||
$this->assertText(t('The Moderated content moderated content has been deleted.'));
|
||||
|
||||
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertFieldChecked('edit-enable-moderation-state');
|
||||
$this->drupalPostForm(NULL, ['enable_moderation_state' => FALSE], t('Save'));
|
||||
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertNoFieldChecked('edit-enable-moderation-state');
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'non-moderated content',
|
||||
], t('Save and publish'));
|
||||
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'non-moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Non-moderated test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
$node = reset($nodes);
|
||||
$this->assertEqual(NULL, $node->moderation_state->target_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests edit form destinations.
|
||||
*/
|
||||
public function testFormSaveDestination() {
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'Some moderated content',
|
||||
'body[0][value]' => 'First version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
$node = $this->drupalGetNodeByTitle('Some moderated content');
|
||||
$edit_path = sprintf('node/%d/edit', $node->id());
|
||||
|
||||
// After saving, we should be at the canonical URL and viewing the first
|
||||
// revision.
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('First version of the content.');
|
||||
|
||||
// Create a new draft; after saving, we should still be on the canonical
|
||||
// URL, but viewing the second revision.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Second version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('Second version of the content.');
|
||||
|
||||
// Make a new published revision; after saving, we should be at the
|
||||
// canonical URL.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Third version of the content.',
|
||||
], t('Save and Publish'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.canonical', ['node' => $node->id()]));
|
||||
$this->assertText('Third version of the content.');
|
||||
|
||||
// Make a new forward revision; after saving, we should be on the "Latest
|
||||
// version" tab.
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'body[0][value]' => 'Fourth version of the content.',
|
||||
], t('Save and Create New Draft'));
|
||||
$this->assertUrl(Url::fromRoute('entity.node.latest_version', ['node' => $node->id()]));
|
||||
$this->assertText('Fourth version of the content.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests pagers aren't broken by content_moderation.
|
||||
*/
|
||||
public function testPagers() {
|
||||
// Create 51 nodes to force the pager.
|
||||
foreach (range(1, 51) as $delta) {
|
||||
Node::create([
|
||||
'type' => 'moderated_content',
|
||||
'uid' => $this->adminUser->id(),
|
||||
'title' => 'Node ' . $delta,
|
||||
'status' => 1,
|
||||
'moderation_state' => 'published',
|
||||
])->save();
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/content');
|
||||
$element = $this->cssSelect('nav.pager li.is-active a');
|
||||
$url = (string) $element[0]['href'];
|
||||
$query = [];
|
||||
parse_str(parse_url($url, PHP_URL_QUERY), $query);
|
||||
$this->assertEqual(0, $query['page']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
|
||||
/**
|
||||
* Tests moderation state node type integration.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateNodeTypeTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* A node type without moderation state disabled.
|
||||
*/
|
||||
public function testNotModerated() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
|
||||
$this->assertText('The content type Not moderated has been added.');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
|
||||
$this->drupalGet('node/add/not_moderated');
|
||||
$this->assertRaw('Save as unpublished');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'Test',
|
||||
], t('Save and publish'));
|
||||
$this->assertText('Not moderated Test has been created.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests enabling moderation on an existing node-type, with content.
|
||||
*/
|
||||
public function testEnablingOnExistingContent() {
|
||||
// Create a node type that is not moderated.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated');
|
||||
|
||||
// Create content.
|
||||
$this->drupalGet('node/add/not_moderated');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'Test',
|
||||
], t('Save and publish'));
|
||||
$this->assertText('Not moderated Test has been created.');
|
||||
|
||||
// Now enable moderation state.
|
||||
$this->enableModerationThroughUi(
|
||||
'not_moderated',
|
||||
['draft', 'needs_review', 'published'],
|
||||
'draft'
|
||||
);
|
||||
|
||||
// And make sure it works.
|
||||
$nodes = \Drupal::entityTypeManager()->getStorage('node')
|
||||
->loadByProperties(['title' => 'Test']);
|
||||
if (empty($nodes)) {
|
||||
$this->fail('Could not load node with title Test');
|
||||
return;
|
||||
}
|
||||
$node = reset($nodes);
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
$this->assertResponse(200);
|
||||
$this->assertLinkByHref('node/' . $node->id() . '/edit');
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw('Save and Create New Draft');
|
||||
$this->assertNoRaw('Save and publish');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests moderation state config entity.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateStatesTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Tests route access/permissions.
|
||||
*/
|
||||
public function testAccess() {
|
||||
$paths = [
|
||||
'admin/config/workflow/moderation',
|
||||
'admin/config/workflow/moderation/states',
|
||||
'admin/config/workflow/moderation/states/add',
|
||||
'admin/config/workflow/moderation/states/draft',
|
||||
'admin/config/workflow/moderation/states/draft/delete',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// No access.
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// User has access.
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administration of moderation state entity.
|
||||
*/
|
||||
public function testStateAdministration() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/workflow/moderation');
|
||||
$this->assertLink('Moderation states');
|
||||
$this->assertLink('Moderation state transitions');
|
||||
$this->clickLink('Moderation states');
|
||||
$this->assertLink('Add moderation state');
|
||||
$this->assertText('Draft');
|
||||
// Edit the draft.
|
||||
$this->clickLink('Edit', 0);
|
||||
$this->assertFieldByName('label', 'Draft');
|
||||
$this->assertNoFieldChecked('edit-published');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Drafty',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Drafty Moderation state.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/draft');
|
||||
$this->assertFieldByName('label', 'Drafty');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Draft Moderation state.');
|
||||
$this->clickLink(t('Add moderation state'));
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Expired',
|
||||
'id' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Expired Moderation state.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/expired');
|
||||
$this->clickLink('Delete');
|
||||
$this->assertText('Are you sure you want to delete Expired?');
|
||||
$this->drupalPostForm(NULL, [], t('Delete'));
|
||||
$this->assertText('Moderation state Expired deleted');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
|
||||
/**
|
||||
* Defines a base class for moderation state tests.
|
||||
*/
|
||||
abstract class ModerationStateTestBase extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Profile to use.
|
||||
*/
|
||||
protected $profile = 'testing';
|
||||
|
||||
/**
|
||||
* Admin user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer moderation states',
|
||||
'administer moderation state transitions',
|
||||
'use draft_draft transition',
|
||||
'use draft_published transition',
|
||||
'use published_draft transition',
|
||||
'use published_archived transition',
|
||||
'access administration pages',
|
||||
'administer content types',
|
||||
'administer nodes',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'access content overview',
|
||||
];
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'block',
|
||||
'block_content',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* Sets the test up.
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->adminUser = $this->drupalCreateUser($this->permissions);
|
||||
$this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']);
|
||||
$this->drupalPlaceBlock('page_title_block');
|
||||
$this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a content-type from the UI.
|
||||
*
|
||||
* @param string $content_type_name
|
||||
* Content type human name.
|
||||
* @param string $content_type_id
|
||||
* Machine name.
|
||||
* @param bool $moderated
|
||||
* TRUE if should be moderated.
|
||||
* @param string[] $allowed_states
|
||||
* Array of allowed state IDs.
|
||||
* @param string $default_state
|
||||
* Default state.
|
||||
*/
|
||||
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) {
|
||||
$this->drupalGet('admin/structure/types');
|
||||
$this->clickLink('Add content type');
|
||||
$edit = [
|
||||
'name' => $content_type_name,
|
||||
'type' => $content_type_id,
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit, t('Save content type'));
|
||||
|
||||
if ($moderated) {
|
||||
$this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable moderation for a specified content type, using the UI.
|
||||
*
|
||||
* @param string $content_type_id
|
||||
* Machine name.
|
||||
* @param string[] $allowed_states
|
||||
* Array of allowed state IDs.
|
||||
* @param string $default_state
|
||||
* Default state.
|
||||
*/
|
||||
protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) {
|
||||
$this->drupalGet('admin/structure/types');
|
||||
$this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/' . $content_type_id);
|
||||
$this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation');
|
||||
$this->assertFieldByName('enable_moderation_state');
|
||||
$this->assertNoFieldChecked('edit-enable-moderation-state');
|
||||
|
||||
$edit['enable_moderation_state'] = 1;
|
||||
|
||||
/** @var ModerationState $state */
|
||||
foreach (ModerationState::loadMultiple() as $state) {
|
||||
$key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']';
|
||||
$edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE;
|
||||
}
|
||||
|
||||
$edit['default_moderation_state'] = $default_state;
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants given user permission to create content of given type.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* User to grant permission to.
|
||||
* @param string $content_type_id
|
||||
* Content type ID.
|
||||
*/
|
||||
protected function grantUserPermissionToCreateContentOfType(AccountInterface $account, $content_type_id) {
|
||||
$role_ids = $account->getRoles(TRUE);
|
||||
/* @var \Drupal\user\RoleInterface $role */
|
||||
$role_id = reset($role_ids);
|
||||
$role = Role::load($role_id);
|
||||
$role->grantPermission(sprintf('create %s content', $content_type_id));
|
||||
$role->grantPermission(sprintf('edit any %s content', $content_type_id));
|
||||
$role->grantPermission(sprintf('delete any %s content', $content_type_id));
|
||||
$role->save();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests moderation state transition config entity.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateTransitionsTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Tests route access/permissions.
|
||||
*/
|
||||
public function testAccess() {
|
||||
$paths = [
|
||||
'admin/config/workflow/moderation/transitions',
|
||||
'admin/config/workflow/moderation/transitions/add',
|
||||
'admin/config/workflow/moderation/transitions/draft_published',
|
||||
'admin/config/workflow/moderation/transitions/draft_published/delete',
|
||||
];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// No access.
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
$this->drupalLogin($this->adminUser);
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
// User has access.
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administration of moderation state transition entity.
|
||||
*/
|
||||
public function testTransitionAdministration() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
$this->drupalGet('admin/config/workflow/moderation');
|
||||
$this->clickLink('Moderation state transitions');
|
||||
$this->assertLink('Add moderation state transition');
|
||||
$this->assertText('Create New Draft');
|
||||
|
||||
// Edit the Draft » Draft review.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
|
||||
$this->assertFieldByName('label', 'Create New Draft');
|
||||
$this->assertFieldByName('stateFrom', 'draft');
|
||||
$this->assertFieldByName('stateTo', 'draft');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Create Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Create Draft Moderation state transition.');
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
|
||||
$this->assertFieldByName('label', 'Create Draft');
|
||||
// Now set it back.
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Create New Draft',
|
||||
], t('Save'));
|
||||
$this->assertText('Saved the Create New Draft Moderation state transition.');
|
||||
|
||||
// Add a new state.
|
||||
$this->drupalGet('admin/config/workflow/moderation/states/add');
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Expired',
|
||||
'id' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Expired Moderation state.');
|
||||
|
||||
// Add a new transition.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions');
|
||||
$this->clickLink(t('Add moderation state transition'));
|
||||
$this->drupalPostForm(NULL, [
|
||||
'label' => 'Published » Expired',
|
||||
'id' => 'published_expired',
|
||||
'stateFrom' => 'published',
|
||||
'stateTo' => 'expired',
|
||||
], t('Save'));
|
||||
$this->assertText('Created the Published » Expired Moderation state transition.');
|
||||
|
||||
// Delete the new transition.
|
||||
$this->drupalGet('admin/config/workflow/moderation/transitions/published_expired');
|
||||
$this->clickLink('Delete');
|
||||
$this->assertText('Are you sure you want to delete Published » Expired?');
|
||||
$this->drupalPostForm(NULL, [], t('Delete'));
|
||||
$this->assertText('Moderation transition Published » Expired deleted');
|
||||
}
|
||||
|
||||
}
|
108
web/core/modules/content_moderation/src/Tests/NodeAccessTest.php
Normal file
108
web/core/modules/content_moderation/src/Tests/NodeAccessTest.php
Normal file
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Tests;
|
||||
|
||||
/**
|
||||
* Tests permission access control around nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class NodeAccessTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi(
|
||||
'Moderated content',
|
||||
'moderated_content',
|
||||
TRUE,
|
||||
['draft', 'published'],
|
||||
'draft'
|
||||
);
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a non-admin user can still access the appropriate pages.
|
||||
*/
|
||||
public function testPageAccess() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Create a node to test with.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'moderated content',
|
||||
], t('Save and Create New Draft'));
|
||||
$nodes = \Drupal::entityTypeManager()
|
||||
->getStorage('node')
|
||||
->loadByProperties([
|
||||
'title' => 'moderated content',
|
||||
]);
|
||||
|
||||
if (!$nodes) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = reset($nodes);
|
||||
|
||||
$view_path = 'node/' . $node->id();
|
||||
$edit_path = 'node/' . $node->id() . '/edit';
|
||||
$latest_path = 'node/' . $node->id() . '/latest';
|
||||
|
||||
// Publish the node.
|
||||
$this->drupalPostForm($edit_path, [], t('Save and Publish'));
|
||||
|
||||
// Ensure access works correctly for anonymous users.
|
||||
$this->drupalLogout();
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Create a forward revision for the 'Latest revision' tab.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalPostForm($edit_path, [
|
||||
'title[0][value]' => 'moderated content revised',
|
||||
], t('Save and Create New Draft'));
|
||||
|
||||
// Now make a new user and verify that the new user's access is correct.
|
||||
$user = $this->createUser([
|
||||
'use draft_draft transition',
|
||||
'use published_draft transition',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(200);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Now make another user, who should not be able to see forward revisions.
|
||||
$user = $this->createUser([
|
||||
'use published_draft transition',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
|
||||
}
|
266
web/core/modules/content_moderation/src/ViewsData.php
Normal file
266
web/core/modules/content_moderation/src/ViewsData.php
Normal file
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Provides the content_moderation views integration.
|
||||
*/
|
||||
class ViewsData {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The moderation information.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* Creates a new ViewsData instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
|
||||
* The moderation information.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_information) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the views data.
|
||||
*
|
||||
* @return array
|
||||
* The views data.
|
||||
*/
|
||||
public function getViewsData() {
|
||||
$data = [];
|
||||
|
||||
$data['content_revision_tracker']['table']['group'] = $this->t('Content moderation (tracker)');
|
||||
|
||||
$data['content_revision_tracker']['entity_type'] = [
|
||||
'title' => $this->t('Entity type'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'string',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'string',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['entity_id'] = [
|
||||
'title' => $this->t('Entity ID'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['langcode'] = [
|
||||
'title' => $this->t('Entity language'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'language',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'language',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$data['content_revision_tracker']['revision_id'] = [
|
||||
'title' => $this->t('Latest revision ID'),
|
||||
'field' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
'filter' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'argument' => [
|
||||
'id' => 'numeric',
|
||||
],
|
||||
'sort' => [
|
||||
'id' => 'standard',
|
||||
],
|
||||
];
|
||||
|
||||
$entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInformation->canModerateEntitiesOfEntityType($type);
|
||||
});
|
||||
|
||||
// Add a join for each entity type to the content_revision_tracker table.
|
||||
foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) {
|
||||
/** @var \Drupal\views\EntityViewsDataInterface $views_data */
|
||||
// We need the views_data handler in order to get the table name later.
|
||||
if ($this->entityTypeManager->hasHandler($entity_type_id, 'views_data') && $views_data = $this->entityTypeManager->getHandler($entity_type_id, 'views_data')) {
|
||||
// Add a join from the entity base table to the revision tracker table.
|
||||
$base_table = $views_data->getViewsTableForEntityType($entity_type);
|
||||
$data['content_revision_tracker']['table']['join'][$base_table] = [
|
||||
'left_field' => $entity_type->getKey('id'),
|
||||
'field' => 'entity_id',
|
||||
'extra' => [
|
||||
[
|
||||
'field' => 'entity_type',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Some entity types might not be translatable.
|
||||
if ($entity_type->hasKey('langcode')) {
|
||||
$data['content_revision_tracker']['table']['join'][$base_table]['extra'][] = [
|
||||
'field' => 'langcode',
|
||||
'left_field' => $entity_type->getKey('langcode'),
|
||||
'operation' => '=',
|
||||
];
|
||||
}
|
||||
|
||||
// Add a relationship between the revision tracker table to the latest
|
||||
// revision on the entity revision table.
|
||||
$data['content_revision_tracker']['latest_revision__' . $entity_type_id] = [
|
||||
'title' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]),
|
||||
'group' => $this->t('@label revision', ['@label' => $entity_type->getLabel()]),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $this->getRevisionViewsTableForEntityType($entity_type),
|
||||
'base field' => $entity_type->getKey('revision'),
|
||||
'relationship field' => 'revision_id',
|
||||
'extra' => [
|
||||
[
|
||||
'left_field' => 'entity_type',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Some entity types might not be translatable.
|
||||
if ($entity_type->hasKey('langcode')) {
|
||||
$data['content_revision_tracker']['latest_revision__' . $entity_type_id]['relationship']['extra'][] = [
|
||||
'left_field' => 'langcode',
|
||||
'field' => $entity_type->getKey('langcode'),
|
||||
'operation' => '=',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provides a relationship from moderated entity to its moderation state
|
||||
// entity.
|
||||
$content_moderation_state_entity_type = \Drupal::entityTypeManager()->getDefinition('content_moderation_state');
|
||||
$content_moderation_state_entity_base_table = $content_moderation_state_entity_type->getDataTable() ?: $content_moderation_state_entity_type->getBaseTable();
|
||||
$content_moderation_state_entity_revision_base_table = $content_moderation_state_entity_type->getRevisionDataTable() ?: $content_moderation_state_entity_type->getRevisionTable();
|
||||
foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) {
|
||||
$table = $entity_type->getDataTable() ?: $entity_type->getBaseTable();
|
||||
|
||||
$data[$table]['moderation_state'] = [
|
||||
'title' => t('Moderation state'),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $content_moderation_state_entity_base_table,
|
||||
'base field' => 'content_entity_id',
|
||||
'relationship field' => $entity_type->getKey('id'),
|
||||
'join_extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'field' => 'content_entity_revision_id',
|
||||
'left_field' => $entity_type->getKey('revision'),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
|
||||
$data[$revision_table]['moderation_state'] = [
|
||||
'title' => t('Moderation state'),
|
||||
'relationship' => [
|
||||
'id' => 'standard',
|
||||
'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]),
|
||||
'base' => $content_moderation_state_entity_revision_base_table,
|
||||
'base field' => 'content_entity_revision_id',
|
||||
'relationship field' => $entity_type->getKey('revision'),
|
||||
'join_extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the table and field information from hook_views_data().
|
||||
*
|
||||
* @param array $data
|
||||
* An array of all information about Views tables and fields, collected from
|
||||
* hook_views_data(), passed by reference.
|
||||
*
|
||||
* @see hook_views_data()
|
||||
*/
|
||||
public function alterViewsData(array &$data) {
|
||||
$entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
|
||||
return $this->moderationInformation->canModerateEntitiesOfEntityType($type);
|
||||
});
|
||||
foreach ($entity_types_with_moderation as $type) {
|
||||
$data[$type->getRevisionTable()]['latest_revision'] = [
|
||||
'title' => t('Is Latest Revision'),
|
||||
'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'),
|
||||
'filter' => ['id' => 'latest_revision'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table of an entity type to be used as revision table in views.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type.
|
||||
*
|
||||
* @return string
|
||||
* The revision base table.
|
||||
*/
|
||||
protected function getRevisionViewsTableForEntityType(EntityTypeInterface $entity_type) {
|
||||
return $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{{ attach_library('content_moderation/entity-moderation-form') }}
|
||||
<ul class="entity-moderation-form">
|
||||
<li>{{ form.current }}</li>
|
||||
<li>{{ form.new_state }}</li>
|
||||
<li>{{ form.revision_log }}</li>
|
||||
<li>{{ form.submit }}</li>
|
||||
</ul>
|
||||
{{ form|without('current', 'new_state', 'revision_log', 'submit') }}
|
|
@ -0,0 +1,409 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- system.menu.main
|
||||
module:
|
||||
- content_moderation
|
||||
- user
|
||||
id: latest
|
||||
label: Latest
|
||||
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: full
|
||||
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: '‹ Previous'
|
||||
next: 'Next ›'
|
||||
first: '« First'
|
||||
last: 'Last »'
|
||||
quantity: 9
|
||||
style:
|
||||
type: table
|
||||
row:
|
||||
type: fields
|
||||
fields:
|
||||
nid:
|
||||
id: nid
|
||||
table: node_field_revision
|
||||
field: nid
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
label: 'Node ID'
|
||||
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: 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
|
||||
vid:
|
||||
id: vid
|
||||
table: node_field_revision
|
||||
field: vid
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
label: 'Revision ID'
|
||||
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: 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: vid
|
||||
plugin_id: field
|
||||
title:
|
||||
id: title
|
||||
table: node_field_revision
|
||||
field: title
|
||||
entity_type: node
|
||||
entity_field: title
|
||||
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: false
|
||||
plugin_id: field
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
label: Title
|
||||
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: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state
|
||||
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: target_id
|
||||
type: entity_reference_label
|
||||
settings:
|
||||
link: true
|
||||
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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
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
|
||||
sorts: { }
|
||||
title: Latest
|
||||
header: { }
|
||||
footer: { }
|
||||
empty: { }
|
||||
relationships:
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: node_field_revision
|
||||
field: moderation_state
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
arguments: { }
|
||||
display_extenders: { }
|
||||
cache_metadata:
|
||||
max-age: 0
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url.query_args
|
||||
- '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: latest
|
||||
menu:
|
||||
type: normal
|
||||
title: Drafts
|
||||
description: ''
|
||||
expanded: false
|
||||
parent: ''
|
||||
weight: 0
|
||||
context: '0'
|
||||
menu_name: main
|
||||
cache_metadata:
|
||||
max-age: 0
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url.query_args
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
|
@ -0,0 +1,406 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- content_moderation
|
||||
- node
|
||||
- user
|
||||
id: test_content_moderation_base_table_test
|
||||
label: test_content_moderation_base_table_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: 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_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: 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
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: content_moderation_state_field_data
|
||||
field: moderation_state
|
||||
relationship: moderation_state
|
||||
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: 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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
moderation_state_1:
|
||||
id: moderation_state_1
|
||||
table: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state
|
||||
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: 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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
moderation_state_2:
|
||||
id: moderation_state_2
|
||||
table: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state_1
|
||||
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: target_id
|
||||
type: entity_reference_entity_id
|
||||
settings: { }
|
||||
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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
filters: { }
|
||||
sorts:
|
||||
created:
|
||||
id: created
|
||||
table: node_field_data
|
||||
field: created
|
||||
order: DESC
|
||||
entity_type: node
|
||||
entity_field: created
|
||||
plugin_id: date
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
exposed: false
|
||||
expose:
|
||||
label: ''
|
||||
granularity: second
|
||||
vid:
|
||||
id: vid
|
||||
table: node_field_data
|
||||
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:
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: node_field_data
|
||||
field: moderation_state
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
moderation_state_1:
|
||||
id: moderation_state_1
|
||||
table: node_field_revision
|
||||
field: moderation_state
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state (revision)'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
arguments: { }
|
||||
display_extenders: { }
|
||||
cache_metadata:
|
||||
max-age: -1
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url.query_args
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
|
@ -0,0 +1,447 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- node
|
||||
- user
|
||||
id: test_content_moderation_latest_revision
|
||||
label: test_content_moderation_latest_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: 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_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: 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
|
||||
revision_id:
|
||||
id: revision_id
|
||||
table: content_revision_tracker
|
||||
field: revision_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
|
||||
title:
|
||||
id: title
|
||||
table: node_field_revision
|
||||
field: title
|
||||
relationship: latest_revision__node
|
||||
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: string
|
||||
settings:
|
||||
link_to_entity: 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: title
|
||||
plugin_id: field
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state
|
||||
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: target_id
|
||||
type: entity_reference_entity_id
|
||||
settings: { }
|
||||
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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
moderation_state_1:
|
||||
id: moderation_state_1
|
||||
table: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state_1
|
||||
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: target_id
|
||||
type: entity_reference_entity_id
|
||||
settings: { }
|
||||
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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
filters: { }
|
||||
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:
|
||||
latest_revision__node:
|
||||
id: latest_revision__node
|
||||
table: content_revision_tracker
|
||||
field: latest_revision__node
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content latest revision'
|
||||
required: false
|
||||
plugin_id: standard
|
||||
moderation_state_1:
|
||||
id: moderation_state_1
|
||||
table: node_field_revision
|
||||
field: moderation_state
|
||||
relationship: latest_revision__node
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state (latest revision)'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: node_field_revision
|
||||
field: moderation_state
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
arguments: { }
|
||||
display_extenders: { }
|
||||
rendering_language: '***LANGUAGE_entity_default***'
|
||||
cache_metadata:
|
||||
max-age: -1
|
||||
contexts:
|
||||
- 'languages:language_interface'
|
||||
- url.query_args
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
|
@ -0,0 +1,315 @@
|
|||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- user
|
||||
id: test_content_moderation_revision_test
|
||||
label: test_content_moderation_revision_test
|
||||
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:
|
||||
vid:
|
||||
id: vid
|
||||
table: node_field_revision
|
||||
field: vid
|
||||
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: vid
|
||||
plugin_id: field
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: content_moderation_state_field_revision
|
||||
field: moderation_state
|
||||
relationship: moderation_state
|
||||
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: target_id
|
||||
type: entity_reference_entity_id
|
||||
settings: { }
|
||||
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: content_moderation_state
|
||||
entity_field: moderation_state
|
||||
plugin_id: field
|
||||
revision_id:
|
||||
id: revision_id
|
||||
table: content_moderation_state_field_revision
|
||||
field: revision_id
|
||||
relationship: moderation_state
|
||||
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: content_moderation_state
|
||||
entity_field: revision_id
|
||||
plugin_id: field
|
||||
filters: { }
|
||||
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:
|
||||
moderation_state:
|
||||
id: moderation_state
|
||||
table: node_field_revision
|
||||
field: moderation_state
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: 'Content moderation state'
|
||||
required: false
|
||||
entity_type: node
|
||||
plugin_id: standard
|
||||
arguments: { }
|
||||
display_extenders: { }
|
||||
cache_metadata:
|
||||
max-age: -1
|
||||
contexts:
|
||||
- 'languages:language_content'
|
||||
- 'languages:language_interface'
|
||||
- url.query_args
|
||||
- 'user.node_grants:view'
|
||||
- user.permissions
|
||||
tags: { }
|
|
@ -0,0 +1,10 @@
|
|||
name: 'Content moderation test views'
|
||||
type: module
|
||||
description: 'Provides default views for views Content moderation tests.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- content_moderation
|
||||
- node
|
||||
- views
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the "Latest Revision" views filter.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class LatestRevisionViewsFilterTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation_test_views',
|
||||
'content_moderation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests view shows the correct node IDs.
|
||||
*/
|
||||
public function testViewShowsCorrectNids() {
|
||||
$node_type = $this->createNodeType('Test', 'test');
|
||||
|
||||
$permissions = [
|
||||
'access content',
|
||||
'view all revisions',
|
||||
];
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
$this->drupalLogin($editor1);
|
||||
|
||||
// Make a pre-moderation node.
|
||||
/** @var Node $node_0 */
|
||||
$node_0 = Node::create([
|
||||
'type' => 'test',
|
||||
'title' => 'Node 0 - Rev 1',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_0->save();
|
||||
|
||||
// Now enable moderation for subsequent nodes.
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
|
||||
// Make a node that is only ever in Draft.
|
||||
/** @var Node $node_1 */
|
||||
$node_1 = Node::create([
|
||||
'type' => 'test',
|
||||
'title' => 'Node 1 - Rev 1',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_1->moderation_state->target_id = 'draft';
|
||||
$node_1->save();
|
||||
|
||||
// Make a node that is in Draft, then Published.
|
||||
/** @var Node $node_2 */
|
||||
$node_2 = Node::create([
|
||||
'type' => 'test',
|
||||
'title' => 'Node 2 - Rev 1',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_2->moderation_state->target_id = 'draft';
|
||||
$node_2->save();
|
||||
|
||||
$node_2->setTitle('Node 2 - Rev 2');
|
||||
$node_2->moderation_state->target_id = 'published';
|
||||
$node_2->save();
|
||||
|
||||
// Make a node that is in Draft, then Published, then Draft.
|
||||
/** @var Node $node_3 */
|
||||
$node_3 = Node::create([
|
||||
'type' => 'test',
|
||||
'title' => 'Node 3 - Rev 1',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_3->moderation_state->target_id = 'draft';
|
||||
$node_3->save();
|
||||
|
||||
$node_3->setTitle('Node 3 - Rev 2');
|
||||
$node_3->moderation_state->target_id = 'published';
|
||||
$node_3->save();
|
||||
|
||||
$node_3->setTitle('Node 3 - Rev 3');
|
||||
$node_3->moderation_state->target_id = 'draft';
|
||||
$node_3->save();
|
||||
|
||||
// Now show the View, and confirm that only the correct titles are showing.
|
||||
$this->drupalGet('/latest');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertEquals(200, $this->getSession()->getStatusCode());
|
||||
$this->assertTrue($page->hasContent('Node 1 - Rev 1'));
|
||||
$this->assertTrue($page->hasContent('Node 2 - Rev 2'));
|
||||
$this->assertTrue($page->hasContent('Node 3 - Rev 3'));
|
||||
$this->assertFalse($page->hasContent('Node 2 - Rev 1'));
|
||||
$this->assertFalse($page->hasContent('Node 3 - Rev 1'));
|
||||
$this->assertFalse($page->hasContent('Node 3 - Rev 2'));
|
||||
$this->assertFalse($page->hasContent('Node 0 - Rev 1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node type.
|
||||
*
|
||||
* @param string $label
|
||||
* The human-readable label of the type to create.
|
||||
* @param string $machine_name
|
||||
* The machine name of the type to create.
|
||||
*
|
||||
* @return NodeType
|
||||
* The node type just created.
|
||||
*/
|
||||
protected function createNodeType($label, $machine_name) {
|
||||
/** @var NodeType $node_type */
|
||||
$node_type = NodeType::create([
|
||||
'type' => $machine_name,
|
||||
'label' => $label,
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
return $node_type;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the view access control handler for moderation state entities.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateAccessTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation_test_views',
|
||||
'content_moderation',
|
||||
];
|
||||
|
||||
/**
|
||||
* Test the view operation access handler with the view permission.
|
||||
*/
|
||||
public function testViewShowsCorrectStates() {
|
||||
$node_type_id = 'test';
|
||||
$this->createNodeType('Test', $node_type_id);
|
||||
|
||||
$permissions = [
|
||||
'access content',
|
||||
'view all revisions',
|
||||
'view moderation states',
|
||||
];
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($editor1);
|
||||
|
||||
$node_1 = Node::create([
|
||||
'type' => $node_type_id,
|
||||
'title' => 'Draft node',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_1->moderation_state->target_id = 'draft';
|
||||
$node_1->save();
|
||||
|
||||
$node_2 = Node::create([
|
||||
'type' => $node_type_id,
|
||||
'title' => 'Published node',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_2->moderation_state->target_id = 'published';
|
||||
$node_2->save();
|
||||
|
||||
// Resave the node with a new state.
|
||||
$node_2->setTitle('Archived node');
|
||||
$node_2->moderation_state->target_id = 'archived';
|
||||
$node_2->save();
|
||||
|
||||
// Now show the View, and confirm that the state labels are showing.
|
||||
$this->drupalGet('/latest');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertTrue($page->hasLink('Draft'));
|
||||
$this->assertTrue($page->hasLink('Archived'));
|
||||
$this->assertFalse($page->hasLink('Published'));
|
||||
|
||||
// Now log in as an admin and test the same thing.
|
||||
$permissions = [
|
||||
'access content',
|
||||
'view all revisions',
|
||||
'administer moderation states',
|
||||
];
|
||||
$admin1 = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($admin1);
|
||||
|
||||
$this->drupalGet('/latest');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertEquals(200, $this->getSession()->getStatusCode());
|
||||
$this->assertTrue($page->hasLink('Draft'));
|
||||
$this->assertTrue($page->hasLink('Archived'));
|
||||
$this->assertFalse($page->hasLink('Published'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node type.
|
||||
*
|
||||
* @param string $label
|
||||
* The human-readable label of the type to create.
|
||||
* @param string $machine_name
|
||||
* The machine name of the type to create.
|
||||
*
|
||||
* @return NodeType
|
||||
* The node type just created.
|
||||
*/
|
||||
protected function createNodeType($label, $machine_name) {
|
||||
/** @var NodeType $node_type */
|
||||
$node_type = NodeType::create([
|
||||
'type' => $machine_name,
|
||||
'label' => $label,
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
|
||||
return $node_type;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
use Drupal\config\Tests\SchemaCheckTestTrait;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\content_moderation\Entity\ModerationStateTransition;
|
||||
|
||||
/**
|
||||
* Ensures that content moderation schema is correct.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationSchemaTest extends KernelTestBase {
|
||||
|
||||
use SchemaCheckTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
'user',
|
||||
'block_content',
|
||||
'system',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests content moderation default schema.
|
||||
*/
|
||||
public function testContentModerationDefaultConfig() {
|
||||
$this->installConfig(['content_moderation']);
|
||||
$typed_config = \Drupal::service('config.typed');
|
||||
$moderation_states = ModerationState::loadMultiple();
|
||||
foreach ($moderation_states as $moderation_state) {
|
||||
$this->assertConfigSchema($typed_config, $moderation_state->getEntityType()->getConfigPrefix() . '.' . $moderation_state->id(), $moderation_state->toArray());
|
||||
}
|
||||
$moderation_state_transitions = ModerationStateTransition::loadMultiple();
|
||||
foreach ($moderation_state_transitions as $moderation_state_transition) {
|
||||
$this->assertConfigSchema($typed_config, $moderation_state_transition->getEntityType()->getConfigPrefix() . '.' . $moderation_state_transition->id(), $moderation_state_transition->toArray());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests content moderation third party schema for node types.
|
||||
*/
|
||||
public function testContentModerationNodeTypeConfig() {
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installConfig(['content_moderation']);
|
||||
$typed_config = \Drupal::service('config.typed');
|
||||
$moderation_states = ModerationState::loadMultiple();
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states));
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', '');
|
||||
$node_type->save();
|
||||
$this->assertConfigSchema($typed_config, $node_type->getEntityType()->getConfigPrefix() . '.' . $node_type->id(), $node_type->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests content moderation third party schema for block content types.
|
||||
*/
|
||||
public function testContentModerationBlockContentTypeConfig() {
|
||||
$this->installEntitySchema('block_content');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installConfig(['content_moderation']);
|
||||
$typed_config = \Drupal::service('config.typed');
|
||||
$moderation_states = ModerationState::loadMultiple();
|
||||
$block_content_type = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'basic',
|
||||
'revision' => TRUE,
|
||||
]);
|
||||
$block_content_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$block_content_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states));
|
||||
$block_content_type->setThirdPartySetting('content_moderation', 'default_moderation_state', '');
|
||||
$block_content_type->save();
|
||||
$this->assertConfigSchema($typed_config, $block_content_type->getEntityType()->getConfigPrefix() . '.' . $block_content_type->id(), $block_content_type->toArray());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\entity_test\Entity\EntityTestBundle;
|
||||
use Drupal\entity_test\Entity\EntityTestWithBundle;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\node\NodeInterface;
|
||||
|
||||
/**
|
||||
* Tests links between a content entity and a content_moderation_state entity.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationStateTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'entity_test',
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
'content_translation',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('entity_test_with_bundle');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests basic monolingual content moderation through the API.
|
||||
*/
|
||||
public function testBasicModeration() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$node_type->save();
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->save();
|
||||
$node = $this->reloadNode($node);
|
||||
$this->assertEquals('draft', $node->moderation_state->entity->id());
|
||||
|
||||
$published = ModerationState::load('published');
|
||||
$node->moderation_state->entity = $published;
|
||||
$node->save();
|
||||
|
||||
$node = $this->reloadNode($node);
|
||||
$this->assertEquals('published', $node->moderation_state->entity->id());
|
||||
|
||||
// Change the state without saving the node.
|
||||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
$content_moderation_state->save();
|
||||
|
||||
$node = $this->reloadNode($node, 3);
|
||||
$this->assertEquals('draft', $node->moderation_state->entity->id());
|
||||
$this->assertFalse($node->isPublished());
|
||||
|
||||
// Get the default revision.
|
||||
$node = $this->reloadNode($node);
|
||||
$this->assertTrue($node->isPublished());
|
||||
$this->assertEquals(2, $node->getRevisionId());
|
||||
|
||||
$node->moderation_state->target_id = 'published';
|
||||
$node->save();
|
||||
|
||||
$node = $this->reloadNode($node, 4);
|
||||
$this->assertEquals('published', $node->moderation_state->entity->id());
|
||||
|
||||
// Get the default revision.
|
||||
$node = $this->reloadNode($node);
|
||||
$this->assertTrue($node->isPublished());
|
||||
$this->assertEquals(4, $node->getRevisionId());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests basic multilingual content moderation through the API.
|
||||
*/
|
||||
public function testMultilingualModeration() {
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$node_type->save();
|
||||
$english_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
// Revision 1 (en).
|
||||
$english_node
|
||||
->setPublished(FALSE)
|
||||
->save();
|
||||
$this->assertEquals('draft', $english_node->moderation_state->entity->id());
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Create a French translation.
|
||||
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
|
||||
$french_node->setPublished(FALSE);
|
||||
// Revision 1 (fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->entity->id());
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Move English node to create another draft.
|
||||
$english_node = $this->reloadNode($english_node);
|
||||
$english_node->moderation_state->target_id = 'draft';
|
||||
// Revision 2 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadNode($english_node);
|
||||
$this->assertEquals('draft', $english_node->moderation_state->entity->id());
|
||||
|
||||
// French node should still be in draft.
|
||||
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->entity->id());
|
||||
|
||||
// Publish the French node.
|
||||
$french_node->moderation_state->target_id = 'published';
|
||||
// Revision 3 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadNode($french_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEquals('published', $french_node->moderation_state->entity->id());
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$english_node = $french_node->getTranslation('en');
|
||||
$this->assertEquals('draft', $english_node->moderation_state->entity->id());
|
||||
|
||||
// Publish the English node.
|
||||
$english_node->moderation_state->target_id = 'published';
|
||||
// Revision 4 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadNode($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
|
||||
// Move the French node back to draft.
|
||||
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$french_node->moderation_state->target_id = 'draft';
|
||||
// Revision 5 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadNode($english_node, 5)->getTranslation('fr');
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
$this->assertTrue($french_node->getTranslation('en')->isPublished());
|
||||
|
||||
// Republish the French node.
|
||||
$french_node->moderation_state->target_id = 'published';
|
||||
// Revision 6 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
|
||||
// Change the EN state without saving the node.
|
||||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
// Revision 7 (en, fr).
|
||||
$content_moderation_state->save();
|
||||
$english_node = $this->reloadNode($french_node, $french_node->getRevisionId() + 1);
|
||||
|
||||
$this->assertEquals('draft', $english_node->moderation_state->entity->id());
|
||||
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('published', $french_node->moderation_state->entity->id());
|
||||
|
||||
// This should unpublish the French node.
|
||||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state = $content_moderation_state->getTranslation('fr');
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
// Revision 8 (en, fr).
|
||||
$content_moderation_state->save();
|
||||
|
||||
$english_node = $this->reloadNode($english_node, $english_node->getRevisionId());
|
||||
$this->assertEquals('draft', $english_node->moderation_state->entity->id());
|
||||
$french_node = $this->reloadNode($english_node, '8')->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->entity->id());
|
||||
// Switching the moderation state to an unpublished state should update the
|
||||
// entity.
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Get the default english node.
|
||||
$english_node = $this->reloadNode($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEquals(6, $english_node->getRevisionId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a non-translatable entity type with a langcode can be moderated.
|
||||
*/
|
||||
public function testNonTranslatableEntityTypeModeration() {
|
||||
// Make the 'entity_test_with_bundle' entity type revisionable.
|
||||
$entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
|
||||
$keys = $entity_type->getKeys();
|
||||
$keys['revision'] = 'revision_id';
|
||||
$entity_type->set('entity_keys', $keys);
|
||||
\Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type);
|
||||
\Drupal::entityDefinitionUpdateManager()->applyUpdates();
|
||||
|
||||
// Create a test bundle.
|
||||
$entity_test_bundle = EntityTestBundle::create([
|
||||
'id' => 'example',
|
||||
]);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [
|
||||
'draft',
|
||||
'published'
|
||||
]);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$entity_test_bundle->save();
|
||||
|
||||
// Check that the tested entity type is not translatable.
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
|
||||
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
|
||||
|
||||
// Create a test entity.
|
||||
$entity_test_with_bundle = EntityTestWithBundle::create([
|
||||
'type' => 'example'
|
||||
]);
|
||||
$entity_test_with_bundle->save();
|
||||
$this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id());
|
||||
|
||||
$entity_test_with_bundle->moderation_state->target_id = 'published';
|
||||
$entity_test_with_bundle->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a non-translatable entity type without a langcode can be
|
||||
* moderated.
|
||||
*/
|
||||
public function testNonLangcodeEntityTypeModeration() {
|
||||
// Make the 'entity_test_with_bundle' entity type revisionable and unset
|
||||
// the langcode entity key.
|
||||
$entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
|
||||
$keys = $entity_type->getKeys();
|
||||
$keys['revision'] = 'revision_id';
|
||||
unset($keys['langcode']);
|
||||
$entity_type->set('entity_keys', $keys);
|
||||
\Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type);
|
||||
\Drupal::entityDefinitionUpdateManager()->applyUpdates();
|
||||
|
||||
// Create a test bundle.
|
||||
$entity_test_bundle = EntityTestBundle::create([
|
||||
'id' => 'example',
|
||||
]);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [
|
||||
'draft',
|
||||
'published'
|
||||
]);
|
||||
$entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$entity_test_bundle->save();
|
||||
|
||||
// Check that the tested entity type is not translatable.
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
|
||||
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
|
||||
|
||||
// Create a test entity.
|
||||
$entity_test_with_bundle = EntityTestWithBundle::create([
|
||||
'type' => 'example'
|
||||
]);
|
||||
$entity_test_with_bundle->save();
|
||||
$this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id());
|
||||
|
||||
$entity_test_with_bundle->moderation_state->target_id = 'published';
|
||||
$entity_test_with_bundle->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the node after clearing the static cache.
|
||||
*
|
||||
* @param \Drupal\node\NodeInterface $node
|
||||
* The node to reload.
|
||||
* @param int|false $revision_id
|
||||
* The specific revision ID to load. Defaults FALSE and just loads the
|
||||
* default revision.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface
|
||||
* The reloaded node.
|
||||
*/
|
||||
protected function reloadNode(NodeInterface $node, $revision_id = FALSE) {
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$storage->resetCache([$node->id()]);
|
||||
if ($revision_id) {
|
||||
return $storage->loadRevision($revision_id);
|
||||
}
|
||||
return $storage->load($node->id());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\EntityOperations
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityOperationsTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
'user',
|
||||
'system',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installEntitySchema('node');
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$this->createNodeType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a page node type to test with, ensuring that it's moderated.
|
||||
*/
|
||||
protected function createNodeType() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'page',
|
||||
'label' => 'Page',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the process of saving forward-revisions works as expected.
|
||||
*/
|
||||
public function testForwardRevisions() {
|
||||
// Create a new node in draft.
|
||||
$page = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'A',
|
||||
]);
|
||||
$page->moderation_state->target_id = 'draft';
|
||||
$page->save();
|
||||
|
||||
$id = $page->id();
|
||||
|
||||
// Verify the entity saved correctly, and that the presence of forward
|
||||
// revisions doesn't affect the default node load.
|
||||
/** @var Node $page */
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('A', $page->getTitle());
|
||||
$this->assertTrue($page->isDefaultRevision());
|
||||
$this->assertFalse($page->isPublished());
|
||||
|
||||
// Moderate the entity to published.
|
||||
$page->setTitle('B');
|
||||
$page->moderation_state->target_id = 'published';
|
||||
$page->save();
|
||||
|
||||
// Verify the entity is now published and public.
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('B', $page->getTitle());
|
||||
$this->assertTrue($page->isDefaultRevision());
|
||||
$this->assertTrue($page->isPublished());
|
||||
|
||||
// Make a new forward-revision in Draft.
|
||||
$page->setTitle('C');
|
||||
$page->moderation_state->target_id = 'draft';
|
||||
$page->save();
|
||||
|
||||
// Verify normal loads return the still-default previous version.
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('B', $page->getTitle());
|
||||
|
||||
// Verify we can load the forward revision, even if the mechanism is kind
|
||||
// of gross. Note: revisionIds() is only available on NodeStorageInterface,
|
||||
// so this won't work for non-nodes. We'd need to use entity queries. This
|
||||
// is a core bug that should get fixed.
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$revision_ids = $storage->revisionIds($page);
|
||||
sort($revision_ids);
|
||||
$latest = end($revision_ids);
|
||||
$page = $storage->loadRevision($latest);
|
||||
$this->assertEquals('C', $page->getTitle());
|
||||
|
||||
$page->setTitle('D');
|
||||
$page->moderation_state->target_id = 'published';
|
||||
$page->save();
|
||||
|
||||
// Verify normal loads return the still-default previous version.
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('D', $page->getTitle());
|
||||
$this->assertTrue($page->isDefaultRevision());
|
||||
$this->assertTrue($page->isPublished());
|
||||
|
||||
// Now check that we can immediately add a new published revision over it.
|
||||
$page->setTitle('E');
|
||||
$page->moderation_state->target_id = 'published';
|
||||
$page->save();
|
||||
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('E', $page->getTitle());
|
||||
$this->assertTrue($page->isDefaultRevision());
|
||||
$this->assertTrue($page->isPublished());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a newly-created node can go straight to published.
|
||||
*/
|
||||
public function testPublishedCreation() {
|
||||
// Create a new node in draft.
|
||||
$page = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'A',
|
||||
]);
|
||||
$page->moderation_state->target_id = 'published';
|
||||
$page->save();
|
||||
|
||||
$id = $page->id();
|
||||
|
||||
// Verify the entity saved correctly.
|
||||
/** @var Node $page */
|
||||
$page = Node::load($id);
|
||||
$this->assertEquals('A', $page->getTitle());
|
||||
$this->assertTrue($page->isDefaultRevision());
|
||||
$this->assertTrue($page->isPublished());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an unpublished state may be made the default revision.
|
||||
*/
|
||||
public function testArchive() {
|
||||
$published_id = $this->randomMachineName();
|
||||
$published_state = ModerationState::create([
|
||||
'id' => $published_id,
|
||||
'label' => $this->randomString(),
|
||||
'published' => TRUE,
|
||||
'default_revision' => TRUE,
|
||||
]);
|
||||
$published_state->save();
|
||||
|
||||
$archived_id = $this->randomMachineName();
|
||||
$archived_state = ModerationState::create([
|
||||
'id' => $archived_id,
|
||||
'label' => $this->randomString(),
|
||||
'published' => FALSE,
|
||||
'default_revision' => TRUE,
|
||||
]);
|
||||
$archived_state->save();
|
||||
|
||||
$page = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => $this->randomString(),
|
||||
]);
|
||||
$page->moderation_state->target_id = $published_id;
|
||||
$page->save();
|
||||
|
||||
$id = $page->id();
|
||||
|
||||
// The newly-created page should already be published.
|
||||
$page = Node::load($id);
|
||||
$this->assertTrue($page->isPublished());
|
||||
|
||||
// When the page is moderated to the archived state, then the latest
|
||||
// revision should be the default revision, and it should be unpublished.
|
||||
$page->moderation_state->target_id = $archived_id;
|
||||
$page->save();
|
||||
$new_revision_id = $page->getRevisionId();
|
||||
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('node');
|
||||
$new_revision = $storage->loadRevision($new_revision_id);
|
||||
$this->assertFalse($new_revision->isPublished());
|
||||
$this->assertTrue($new_revision->isDefaultRevision());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityRevisionConverterTest extends KernelTestBase {
|
||||
|
||||
public static $modules = [
|
||||
'user',
|
||||
'entity_test',
|
||||
'system',
|
||||
'content_moderation',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'article',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
|
||||
$revision_ids = [];
|
||||
$node = Node::create([
|
||||
'title' => 'test',
|
||||
'type' => 'article',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$revision_ids[] = $node->getRevisionId();
|
||||
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
$revision_ids[] = $node->getRevisionId();
|
||||
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->isDefaultRevision(FALSE);
|
||||
$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());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityStateChangeValidationTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
'content_translation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test valid transitions.
|
||||
*
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testValidTransition() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->moderation_state->target_id = 'draft';
|
||||
$node->save();
|
||||
|
||||
$node->moderation_state->target_id = 'published';
|
||||
$this->assertCount(0, $node->validate());
|
||||
$node->save();
|
||||
|
||||
$this->assertEquals('published', $node->moderation_state->entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invalid transitions.
|
||||
*
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testInvalidTransition() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->moderation_state->target_id = 'draft';
|
||||
$node->save();
|
||||
|
||||
$node->moderation_state->target_id = 'archived';
|
||||
$violations = $node->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
|
||||
$this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that content without prior moderation information can be moderated.
|
||||
*/
|
||||
public function testLegacyContent() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$nid = $node->id();
|
||||
|
||||
// Enable moderation for our node type.
|
||||
/** @var NodeType $node_type */
|
||||
$node_type = NodeType::load('example');
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$node_type->save();
|
||||
|
||||
$node = Node::load($nid);
|
||||
|
||||
// Having no previous state should not break validation.
|
||||
$violations = $node->validate();
|
||||
|
||||
$this->assertCount(0, $violations);
|
||||
|
||||
// Having no previous state should not break saving the node.
|
||||
$node->setTitle('New');
|
||||
$node->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that content without prior moderation information can be translated.
|
||||
*/
|
||||
public function testLegacyMultilingualContent() {
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
/** @var \Drupal\node\NodeInterface $node */
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
'langcode' => 'en',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$nid = $node->id();
|
||||
|
||||
$node = Node::load($nid);
|
||||
|
||||
// Creating a translation shouldn't break, even though there's no previous
|
||||
// moderated revision for the new language.
|
||||
$node_fr = $node->addTranslation('fr');
|
||||
$node_fr->setTitle('Francais');
|
||||
$node_fr->save();
|
||||
|
||||
// Enable moderation for our node type.
|
||||
/** @var NodeType $node_type */
|
||||
$node_type = NodeType::load('example');
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$node_type->save();
|
||||
|
||||
// Reload the French version of the node.
|
||||
$node = Node::load($nid);
|
||||
$node_fr = $node->getTranslation('fr');
|
||||
|
||||
/** @var \Drupal\node\NodeInterface $node_fr */
|
||||
$node_fr->setTitle('Nouveau');
|
||||
$node_fr->save();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\content_moderation\EntityTypeInfo;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\EntityTypeInfo
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityTypeInfoTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type info class.
|
||||
*
|
||||
* @var \Drupal\content_moderation\EntityTypeInfo
|
||||
*/
|
||||
protected $entityTypeInfo;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->entityTypeInfo = $this->container->get('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class);
|
||||
$this->entityTypeManager = $this->container->get('entity_type.manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::entityBaseFieldInfo
|
||||
*/
|
||||
public function testEntityBaseFieldInfo() {
|
||||
$definition = $this->entityTypeManager->getDefinition('entity_test');
|
||||
$definition->setHandlerClass('moderation', ModerationHandler::class);
|
||||
|
||||
$base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
|
||||
|
||||
$this->assertFalse($base_fields['moderation_state']->isReadOnly());
|
||||
$this->assertTrue($base_fields['moderation_state']->isComputed());
|
||||
$this->assertTrue($base_fields['moderation_state']->isTranslatable());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\content_moderation\Entity\ModerationState;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Entity\ModerationState
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateEntityTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['content_moderation'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('moderation_state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify moderation state methods based on entity properties.
|
||||
*
|
||||
* @covers ::isPublishedState
|
||||
* @covers ::isDefaultRevisionState
|
||||
*
|
||||
* @dataProvider moderationStateProvider
|
||||
*/
|
||||
public function testModerationStateProperties($published, $default_revision, $is_published, $is_default) {
|
||||
$moderation_state_id = $this->randomMachineName();
|
||||
$moderation_state = ModerationState::create([
|
||||
'id' => $moderation_state_id,
|
||||
'label' => $this->randomString(),
|
||||
'published' => $published,
|
||||
'default_revision' => $default_revision,
|
||||
]);
|
||||
$moderation_state->save();
|
||||
|
||||
$moderation_state = ModerationState::load($moderation_state_id);
|
||||
$this->assertEquals($is_published, $moderation_state->isPublishedState());
|
||||
$this->assertEquals($is_default, $moderation_state->isDefaultRevisionState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testModerationStateProperties.
|
||||
*/
|
||||
public function moderationStateProvider() {
|
||||
return [
|
||||
// Draft, Needs review; should not touch the default revision.
|
||||
[FALSE, FALSE, FALSE, FALSE],
|
||||
// Published; this state should update and publish the default revision.
|
||||
[TRUE, TRUE, TRUE, TRUE],
|
||||
// Archive; this state should update but not publish the default revision.
|
||||
[FALSE, TRUE, FALSE, TRUE],
|
||||
// We try to prevent creating this state via the UI, but when a moderation
|
||||
// state is a published state, it should also become the default revision.
|
||||
[TRUE, FALSE, TRUE, TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateFieldItemListTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $testNode;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft']);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
|
||||
$node_type->save();
|
||||
$this->testNode = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$this->testNode->save();
|
||||
\Drupal::entityTypeManager()->getStorage('node')->resetCache();
|
||||
$this->testNode = Node::load($this->testNode->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the field item list when accessing an index.
|
||||
*/
|
||||
public function testArrayIndex() {
|
||||
$this->assertEquals('draft', $this->testNode->moderation_state[0]->entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the field item list when iterating.
|
||||
*/
|
||||
public function testArrayIteration() {
|
||||
$states = [];
|
||||
foreach ($this->testNode->moderation_state as $item) {
|
||||
$states[] = $item->entity->id();
|
||||
}
|
||||
$this->assertEquals(['draft'], $states);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Tests the views integration of content_moderation.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ViewsDataIntegrationTest extends ViewsKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation_test_views',
|
||||
'node',
|
||||
'content_moderation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE) {
|
||||
parent::setUp($import_test_views);
|
||||
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installConfig('content_moderation_test_views');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'page',
|
||||
]);
|
||||
$node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
|
||||
$node_type->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests content_moderation_views_data().
|
||||
*
|
||||
* @see content_moderation_views_data()
|
||||
*/
|
||||
public function testViewsData() {
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'Test title first revision',
|
||||
]);
|
||||
$node->moderation_state->target_id = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->target_id = 'draft';
|
||||
$revision->save();
|
||||
|
||||
$view = Views::getView('test_content_moderation_latest_revision');
|
||||
$view->execute();
|
||||
|
||||
// Ensure that the content_revision_tracker contains the right latest
|
||||
// revision ID.
|
||||
// Also ensure that the relationship back to the revision table contains the
|
||||
// right latest revision.
|
||||
$expected_result = [
|
||||
[
|
||||
'nid' => $node->id(),
|
||||
'revision_id' => $revision->getRevisionId(),
|
||||
'title' => $revision->label(),
|
||||
'moderation_state_1' => 'draft',
|
||||
'moderation_state' => 'published',
|
||||
],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'content_revision_tracker_revision_id' => 'revision_id', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the join from the revision data table to the moderation state table.
|
||||
*/
|
||||
public function testContentModerationStateRevisionJoin() {
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'Test title first revision',
|
||||
]);
|
||||
$node->moderation_state->target_id = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->target_id = 'draft';
|
||||
$revision->save();
|
||||
|
||||
$view = Views::getView('test_content_moderation_revision_test');
|
||||
$view->execute();
|
||||
|
||||
$expected_result = [
|
||||
[
|
||||
'revision_id' => $node->getRevisionId(),
|
||||
'moderation_state' => 'published',
|
||||
],
|
||||
[
|
||||
'revision_id' => $revision->getRevisionId(),
|
||||
'moderation_state' => 'draft',
|
||||
],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, ['revision_id' => 'revision_id', 'moderation_state' => 'moderation_state']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the join from the data table to the moderation state table.
|
||||
*/
|
||||
public function testContentModerationStateBaseJoin() {
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'Test title first revision',
|
||||
]);
|
||||
$node->moderation_state->target_id = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->target_id = 'draft';
|
||||
$revision->save();
|
||||
|
||||
$view = Views::getView('test_content_moderation_base_table_test');
|
||||
$view->execute();
|
||||
|
||||
$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',
|
||||
// Joins from the revision table to the default revision of the
|
||||
// content_moderation.
|
||||
'moderation_state_1' => 'draft',
|
||||
// Joins from the revision table to the revision of the
|
||||
// content_moderation.
|
||||
'moderation_state_2' => 'published',
|
||||
],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1', 'moderation_state_2' => 'moderation_state_2']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\content_moderation\ContentPreprocess;
|
||||
use Drupal\Core\Routing\CurrentRouteMatch;
|
||||
use Drupal\node\Entity\Node;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\ContentPreprocess
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentPreprocessTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* @covers ::isLatestVersionPage
|
||||
* @dataProvider routeNodeProvider
|
||||
*/
|
||||
public function testIsLatestVersionPage($route_name, $route_nid, $check_nid, $result, $message) {
|
||||
$content_preprocess = new ContentPreprocess($this->setupCurrentRouteMatch($route_name, $route_nid));
|
||||
$node = $this->setupNode($check_nid);
|
||||
$this->assertEquals($result, $content_preprocess->isLatestVersionPage($node), $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for self::testIsLatestVersionPage().
|
||||
*/
|
||||
public function routeNodeProvider() {
|
||||
return [
|
||||
['entity.node.canonical', 1, 1, FALSE, 'Not on the latest version tab route.'],
|
||||
['entity.node.latest_version', 1, 1, TRUE, 'On the latest version tab route, with the route node.'],
|
||||
['entity.node.latest_version', 1, 2, FALSE, 'On the latest version tab route, with a different node.'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the current route matching object.
|
||||
*
|
||||
* @param string $route_name
|
||||
* The route to mock.
|
||||
* @param int $nid
|
||||
* The node ID for mocking.
|
||||
*
|
||||
* @return \Drupal\Core\Routing\CurrentRouteMatch
|
||||
* The mocked current route match object.
|
||||
*/
|
||||
protected function setupCurrentRouteMatch($route_name, $nid) {
|
||||
$route_match = $this->prophesize(CurrentRouteMatch::class);
|
||||
$route_match->getRouteName()->willReturn($route_name);
|
||||
$route_match->getParameter('node')->willReturn($this->setupNode($nid));
|
||||
|
||||
return $route_match->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock a node object.
|
||||
*
|
||||
* @param int $nid
|
||||
* The node ID to mock.
|
||||
*
|
||||
* @return \Drupal\node\Entity\Node
|
||||
* The mocked node.
|
||||
*/
|
||||
protected function setupNode($nid) {
|
||||
$node = $this->prophesize(Node::class);
|
||||
$node->id()->willReturn($nid);
|
||||
|
||||
return $node->reveal();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\Core\Access\AccessResultAllowed;
|
||||
use Drupal\Core\Access\AccessResultForbidden;
|
||||
use Drupal\Core\Routing\RouteMatch;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\content_moderation\Access\LatestRevisionCheck;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Access\LatestRevisionCheck
|
||||
* @group content_moderation
|
||||
*/
|
||||
class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* Test the access check of the LatestRevisionCheck service.
|
||||
*
|
||||
* @param string $entity_class
|
||||
* The class of the entity to mock.
|
||||
* @param string $entity_type
|
||||
* The machine name of the entity to mock.
|
||||
* @param bool $has_forward
|
||||
* Whether this entity should have a forward revision in the system.
|
||||
* @param string $result_class
|
||||
* The AccessResult class that should result. One of AccessResultAllowed,
|
||||
* AccessResultForbidden, AccessResultNeutral.
|
||||
*
|
||||
* @dataProvider accessSituationProvider
|
||||
*/
|
||||
public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, $result_class) {
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
$entity = $this->prophesize($entity_class);
|
||||
$entity->getCacheContexts()->willReturn([]);
|
||||
$entity->getCacheTags()->willReturn([]);
|
||||
$entity->getCacheMaxAge()->willReturn(0);
|
||||
|
||||
/** @var \Drupal\content_moderation\ModerationInformation $mod_info */
|
||||
$mod_info = $this->prophesize(ModerationInformation::class);
|
||||
$mod_info->hasForwardRevision($entity->reveal())->willReturn($has_forward);
|
||||
|
||||
$route = $this->prophesize(Route::class);
|
||||
|
||||
$route->getOption('_content_moderation_entity_type')->willReturn($entity_type);
|
||||
|
||||
$route_match = $this->prophesize(RouteMatch::class);
|
||||
$route_match->getParameter($entity_type)->willReturn($entity->reveal());
|
||||
|
||||
$lrc = new LatestRevisionCheck($mod_info->reveal());
|
||||
|
||||
/** @var \Drupal\Core\Access\AccessResult $result */
|
||||
$result = $lrc->access($route->reveal(), $route_match->reveal());
|
||||
|
||||
$this->assertInstanceOf($result_class, $result);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testLastAccessPermissions().
|
||||
*/
|
||||
public function accessSituationProvider() {
|
||||
return [
|
||||
[Node::class, 'node', TRUE, AccessResultAllowed::class],
|
||||
[Node::class, 'node', FALSE, AccessResultForbidden::class],
|
||||
[BlockContent::class, 'block_content', TRUE, AccessResultAllowed::class],
|
||||
[BlockContent::class, 'block_content', FALSE, AccessResultForbidden::class],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityType;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\ModerationInformation
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationInformationTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* Builds a mock user.
|
||||
*
|
||||
* @return AccountInterface
|
||||
* The mocked user.
|
||||
*/
|
||||
protected function getUser() {
|
||||
return $this->prophesize(AccountInterface::class)->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mock Entity Type Manager.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_bundle_storage
|
||||
* Entity bundle storage.
|
||||
*
|
||||
* @return EntityTypeManagerInterface
|
||||
* The mocked entity type manager.
|
||||
*/
|
||||
protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_storage) {
|
||||
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$entity_type_manager->getStorage('entity_test_bundle')->willReturn($entity_bundle_storage);
|
||||
return $entity_type_manager->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up content moderation and entity manager mocking.
|
||||
*
|
||||
* @param bool $status
|
||||
* TRUE if content_moderation should be enabled, FALSE if not.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
* The mocked entity type manager.
|
||||
*/
|
||||
public function setupModerationEntityManager($status) {
|
||||
$bundle = $this->prophesize(ConfigEntityInterface::class);
|
||||
$bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)->willReturn($status);
|
||||
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$entity_storage->load('test_bundle')->willReturn($bundle->reveal());
|
||||
|
||||
return $this->getEntityTypeManager($entity_storage->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerBoolean
|
||||
* @covers ::isModeratedEntity
|
||||
*/
|
||||
public function testIsModeratedEntity($status) {
|
||||
$moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser());
|
||||
|
||||
$entity_type = new ContentEntityType([
|
||||
'id' => 'test_entity_type',
|
||||
'bundle_entity_type' => 'entity_test_bundle',
|
||||
'handlers' => ['moderation' => ModerationHandler::class],
|
||||
]);
|
||||
$entity = $this->prophesize(ContentEntityInterface::class);
|
||||
$entity->getEntityType()->willReturn($entity_type);
|
||||
$entity->bundle()->willReturn('test_bundle');
|
||||
|
||||
$this->assertEquals($status, $moderation_information->isModeratedEntity($entity->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isModeratedEntity
|
||||
*/
|
||||
public function testIsModeratedEntityForNonBundleEntityType() {
|
||||
$entity_type = new ContentEntityType([
|
||||
'id' => 'test_entity_type',
|
||||
]);
|
||||
$entity = $this->prophesize(ContentEntityInterface::class);
|
||||
$entity->getEntityType()->willReturn($entity_type);
|
||||
$entity->bundle()->willReturn('test_entity_type');
|
||||
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$entity_type_manager = $this->getEntityTypeManager($entity_storage->reveal());
|
||||
$moderation_information = new ModerationInformation($entity_type_manager, $this->getUser());
|
||||
|
||||
$this->assertEquals(FALSE, $moderation_information->isModeratedEntity($entity->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerBoolean
|
||||
* @covers ::shouldModerateEntitiesOfBundle
|
||||
*/
|
||||
public function testShouldModerateEntities($status) {
|
||||
$entity_type = new ContentEntityType([
|
||||
'id' => 'test_entity_type',
|
||||
'bundle_entity_type' => 'entity_test_bundle',
|
||||
'handlers' => ['moderation' => ModerationHandler::class],
|
||||
]);
|
||||
|
||||
$moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser());
|
||||
|
||||
$this->assertEquals($status, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for several tests.
|
||||
*/
|
||||
public function providerBoolean() {
|
||||
return [
|
||||
[FALSE],
|
||||
[TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,297 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Query\QueryFactory;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\ModerationStateInterface;
|
||||
use Drupal\content_moderation\ModerationStateTransitionInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\StateTransitionValidation
|
||||
* @group content_moderation
|
||||
*/
|
||||
class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* Builds a mock storage object for Transitions.
|
||||
*
|
||||
* @return EntityStorageInterface
|
||||
* The mocked storage object for Transitions.
|
||||
*/
|
||||
protected function setupTransitionStorage() {
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
|
||||
$list = $this->setupTransitionEntityList();
|
||||
$entity_storage->loadMultiple()->willReturn($list);
|
||||
$entity_storage->loadMultiple(Argument::type('array'))->will(function ($args) use ($list) {
|
||||
$keys = $args[0];
|
||||
if (empty($keys)) {
|
||||
return $list;
|
||||
}
|
||||
|
||||
$return = array_map(function($key) use ($list) {
|
||||
return $list[$key];
|
||||
}, $keys);
|
||||
|
||||
return $return;
|
||||
});
|
||||
return $entity_storage->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of mocked Transition objects.
|
||||
*
|
||||
* @return ModerationStateTransitionInterface[]
|
||||
* An array of mocked Transition objects.
|
||||
*/
|
||||
protected function setupTransitionEntityList() {
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('draft__needs_review');
|
||||
$transition->getFromState()->willReturn('draft');
|
||||
$transition->getToState()->willReturn('needs_review');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('needs_review__staging');
|
||||
$transition->getFromState()->willReturn('needs_review');
|
||||
$transition->getToState()->willReturn('staging');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('staging__published');
|
||||
$transition->getFromState()->willReturn('staging');
|
||||
$transition->getToState()->willReturn('published');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('needs_review__draft');
|
||||
$transition->getFromState()->willReturn('needs_review');
|
||||
$transition->getToState()->willReturn('draft');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('draft__draft');
|
||||
$transition->getFromState()->willReturn('draft');
|
||||
$transition->getToState()->willReturn('draft');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('needs_review__needs_review');
|
||||
$transition->getFromState()->willReturn('needs_review');
|
||||
$transition->getToState()->willReturn('needs_review');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
$transition = $this->prophesize(ModerationStateTransitionInterface::class);
|
||||
$transition->id()->willReturn('published__published');
|
||||
$transition->getFromState()->willReturn('published');
|
||||
$transition->getToState()->willReturn('published');
|
||||
$list[$transition->reveal()->id()] = $transition->reveal();
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a mock storage object for States.
|
||||
*
|
||||
* @return EntityStorageInterface
|
||||
* The mocked storage object for States.
|
||||
*/
|
||||
protected function setupStateStorage() {
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
|
||||
$state = $this->prophesize(ModerationStateInterface::class);
|
||||
$state->id()->willReturn('draft');
|
||||
$state->label()->willReturn('Draft');
|
||||
$state->isPublishedState()->willReturn(FALSE);
|
||||
$state->isDefaultRevisionState()->willReturn(FALSE);
|
||||
$states['draft'] = $state->reveal();
|
||||
|
||||
$state = $this->prophesize(ModerationStateInterface::class);
|
||||
$state->id()->willReturn('needs_review');
|
||||
$state->label()->willReturn('Needs Review');
|
||||
$state->isPublishedState()->willReturn(FALSE);
|
||||
$state->isDefaultRevisionState()->willReturn(FALSE);
|
||||
$states['needs_review'] = $state->reveal();
|
||||
|
||||
$state = $this->prophesize(ModerationStateInterface::class);
|
||||
$state->id()->willReturn('staging');
|
||||
$state->label()->willReturn('Staging');
|
||||
$state->isPublishedState()->willReturn(FALSE);
|
||||
$state->isDefaultRevisionState()->willReturn(FALSE);
|
||||
$states['staging'] = $state->reveal();
|
||||
|
||||
$state = $this->prophesize(ModerationStateInterface::class);
|
||||
$state->id()->willReturn('published');
|
||||
$state->label()->willReturn('Published');
|
||||
$state->isPublishedState()->willReturn(TRUE);
|
||||
$state->isDefaultRevisionState()->willReturn(TRUE);
|
||||
$states['published'] = $state->reveal();
|
||||
|
||||
$state = $this->prophesize(ModerationStateInterface::class);
|
||||
$state->id()->willReturn('archived');
|
||||
$state->label()->willReturn('Archived');
|
||||
$state->isPublishedState()->willReturn(TRUE);
|
||||
$state->isDefaultRevisionState()->willReturn(TRUE);
|
||||
$states['archived'] = $state->reveal();
|
||||
|
||||
$entity_storage->loadMultiple()->willReturn($states);
|
||||
|
||||
foreach ($states as $id => $state) {
|
||||
$entity_storage->load($id)->willReturn($state);
|
||||
}
|
||||
|
||||
return $entity_storage->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a mocked Entity Type Manager.
|
||||
*
|
||||
* @return EntityTypeManagerInterface
|
||||
* The mocked Entity Type Manager.
|
||||
*/
|
||||
protected function setupEntityTypeManager(EntityStorageInterface $storage) {
|
||||
$entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$entityTypeManager->getStorage('moderation_state')->willReturn($storage);
|
||||
$entityTypeManager->getStorage('moderation_state_transition')->willReturn($this->setupTransitionStorage());
|
||||
|
||||
return $entityTypeManager->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a mocked query factory that does nothing.
|
||||
*
|
||||
* @return QueryFactory
|
||||
* The mocked query factory that does nothing.
|
||||
*/
|
||||
protected function setupQueryFactory() {
|
||||
$factory = $this->prophesize(QueryFactory::class);
|
||||
|
||||
return $factory->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isTransitionAllowed
|
||||
* @covers ::calculatePossibleTransitions
|
||||
*
|
||||
* @dataProvider providerIsTransitionAllowedWithValidTransition
|
||||
*/
|
||||
public function testIsTransitionAllowedWithValidTransition($from_id, $to_id) {
|
||||
$storage = $this->setupStateStorage();
|
||||
$state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
|
||||
$this->assertTrue($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for self::testIsTransitionAllowedWithValidTransition().
|
||||
*/
|
||||
public function providerIsTransitionAllowedWithValidTransition() {
|
||||
return [
|
||||
['draft', 'draft'],
|
||||
['draft', 'needs_review'],
|
||||
['needs_review', 'needs_review'],
|
||||
['needs_review', 'staging'],
|
||||
['staging', 'published'],
|
||||
['needs_review', 'draft'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::isTransitionAllowed
|
||||
* @covers ::calculatePossibleTransitions
|
||||
*
|
||||
* @dataProvider providerIsTransitionAllowedWithInValidTransition
|
||||
*/
|
||||
public function testIsTransitionAllowedWithInValidTransition($from_id, $to_id) {
|
||||
$storage = $this->setupStateStorage();
|
||||
$state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
|
||||
$this->assertFalse($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for self::testIsTransitionAllowedWithInValidTransition().
|
||||
*/
|
||||
public function providerIsTransitionAllowedWithInValidTransition() {
|
||||
return [
|
||||
['published', 'needs_review'],
|
||||
['published', 'staging'],
|
||||
['staging', 'needs_review'],
|
||||
['staging', 'staging'],
|
||||
['needs_review', 'published'],
|
||||
['published', 'archived'],
|
||||
['archived', 'published'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies user-aware transition validation.
|
||||
*
|
||||
* @param string $from_id
|
||||
* The state to transition from.
|
||||
* @param string $to_id
|
||||
* The state to transition to.
|
||||
* @param string $permission
|
||||
* The permission to give the user, or not.
|
||||
* @param bool $allowed
|
||||
* Whether or not to grant a user this permission.
|
||||
* @param bool $result
|
||||
* Whether userMayTransition() is expected to return TRUE or FALSE.
|
||||
*
|
||||
* @dataProvider userTransitionsProvider
|
||||
*/
|
||||
public function testUserSensitiveValidTransitions($from_id, $to_id, $permission, $allowed, $result) {
|
||||
$user = $this->prophesize(AccountInterface::class);
|
||||
// The one listed permission will be returned as instructed; Any others are
|
||||
// always denied.
|
||||
$user->hasPermission($permission)->willReturn($allowed);
|
||||
$user->hasPermission(Argument::type('string'))->willReturn(FALSE);
|
||||
|
||||
$storage = $this->setupStateStorage();
|
||||
$validator = new Validator($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
|
||||
|
||||
$this->assertEquals($result, $validator->userMayTransition($storage->load($from_id), $storage->load($to_id), $user->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the user transition test.
|
||||
*/
|
||||
public function userTransitionsProvider() {
|
||||
// The user has the right permission, so let it through.
|
||||
$ret[] = ['draft', 'draft', 'use draft__draft transition', TRUE, TRUE];
|
||||
|
||||
// The user doesn't have the right permission, block it.
|
||||
$ret[] = ['draft', 'draft', 'use draft__draft transition', FALSE, FALSE];
|
||||
|
||||
// The user has some other permission that doesn't matter.
|
||||
$ret[] = ['draft', 'draft', 'use draft__needs_review transition', TRUE, FALSE];
|
||||
|
||||
// The user has permission, but the transition isn't allowed anyway.
|
||||
$ret[] = ['published', 'needs_review', 'use published__needs_review transition', TRUE, FALSE];
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Testable subclass for selected tests.
|
||||
*
|
||||
* EntityQuery is beyond untestable, so we have to subclass and override the
|
||||
* method that uses it.
|
||||
*/
|
||||
class Validator extends StateTransitionValidation {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) {
|
||||
if ($from->id() === 'draft' && $to->id() === 'draft') {
|
||||
return $this->transitionStorage()->loadMultiple(['draft__draft'])[0];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue