updated core and modules
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- content_moderation
|
||||
id: editorial
|
||||
label: 'Editorial workflow'
|
||||
states:
|
||||
archived:
|
||||
label: Archived
|
||||
weight: 5
|
||||
draft:
|
||||
label: Draft
|
||||
weight: -5
|
||||
published:
|
||||
label: Published
|
||||
weight: 0
|
||||
transitions:
|
||||
archive:
|
||||
label: Archive
|
||||
from:
|
||||
- published
|
||||
to: archived
|
||||
weight: 2
|
||||
archived_draft:
|
||||
label: 'Restore to Draft'
|
||||
from:
|
||||
- archived
|
||||
to: draft
|
||||
weight: 3
|
||||
archived_published:
|
||||
label: Restore
|
||||
from:
|
||||
- archived
|
||||
to: published
|
||||
weight: 4
|
||||
create_new_draft:
|
||||
label: 'Create New Draft'
|
||||
from:
|
||||
- draft
|
||||
- published
|
||||
to: draft
|
||||
weight: 0
|
||||
publish:
|
||||
label: Publish
|
||||
from:
|
||||
- draft
|
||||
- published
|
||||
to: published
|
||||
weight: 1
|
||||
type: content_moderation
|
||||
type_settings:
|
||||
states:
|
||||
archived:
|
||||
published: false
|
||||
default_revision: true
|
||||
draft:
|
||||
published: false
|
||||
default_revision: false
|
||||
published:
|
||||
published: true
|
||||
default_revision: true
|
||||
entity_types: { }
|
||||
@@ -0,0 +1,33 @@
|
||||
views.filter.latest_revision:
|
||||
type: views_filter
|
||||
label: 'Latest revision'
|
||||
mapping:
|
||||
value:
|
||||
type: string
|
||||
label: 'Value'
|
||||
|
||||
workflow.type_settings.content_moderation:
|
||||
type: mapping
|
||||
mapping:
|
||||
states:
|
||||
type: sequence
|
||||
label: 'Additional state configuration for content moderation'
|
||||
sequence:
|
||||
type: mapping
|
||||
label: 'States'
|
||||
mapping:
|
||||
published:
|
||||
type: boolean
|
||||
label: 'Is published'
|
||||
default_revision:
|
||||
type: boolean
|
||||
label: 'Is default revision'
|
||||
entity_types:
|
||||
type: sequence
|
||||
label: 'Entity types'
|
||||
sequence:
|
||||
type: sequence
|
||||
label: 'Bundles'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Bundle ID'
|
||||
@@ -0,0 +1,15 @@
|
||||
name: 'Content Moderation'
|
||||
type: module
|
||||
description: 'Provides moderation states for content'
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
package: Core (Experimental)
|
||||
configure: entity.workflow.collection
|
||||
dependencies:
|
||||
- workflows
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
@@ -0,0 +1,7 @@
|
||||
content_moderation:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/content_moderation.module.css: {}
|
||||
theme:
|
||||
css/content_moderation.theme.css: {}
|
||||
@@ -0,0 +1,3 @@
|
||||
content_moderation.workflows:
|
||||
deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks'
|
||||
weight: 100
|
||||
@@ -0,0 +1,236 @@
|
||||
<?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\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\workflows\WorkflowInterface;
|
||||
use Drupal\node\NodeInterface;
|
||||
use Drupal\node\Plugin\Action\PublishNode;
|
||||
use Drupal\node\Plugin\Action\UnpublishNode;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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 moderation for content by applying workflows to content. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Configuring workflows') . '</dt>';
|
||||
$output .= '<dd>' . t('Enable the Workflow UI module to create, edit and delete content moderation workflows.') . '</p>';
|
||||
$output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
|
||||
$output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '</p>';
|
||||
$output .= '</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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_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) {
|
||||
/** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
|
||||
$transition_validation = \Drupal::service('content_moderation.state_transition_validation');
|
||||
|
||||
$valid_transition_targets = $transition_validation->getValidTransitions($node, $account);
|
||||
$access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
|
||||
|
||||
$access_result->addCacheableDependency($node);
|
||||
$access_result->addCacheableDependency($account);
|
||||
$workflow = \Drupal::service('content_moderation.moderation_information')->getWorkflowForEntity($node);
|
||||
$access_result->addCacheableDependency($workflow);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_bundle_info_alter().
|
||||
*/
|
||||
function content_moderation_entity_bundle_info_alter(&$bundles) {
|
||||
/** @var \Drupal\workflows\WorkflowInterface $workflow */
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
|
||||
$plugin = $workflow->getTypePlugin();
|
||||
foreach ($plugin->getEntityTypes() as $entity_type_id) {
|
||||
foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
|
||||
if (isset($bundles[$entity_type_id][$bundle_id])) {
|
||||
$bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_insert().
|
||||
*/
|
||||
function content_moderation_workflow_insert(WorkflowInterface $entity) {
|
||||
// Clear bundle cache so workflow gets added or removed from the bundle
|
||||
// information.
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
// Clear field cache so extra field is added or removed.
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_update().
|
||||
*/
|
||||
function content_moderation_workflow_update(WorkflowInterface $entity) {
|
||||
content_moderation_workflow_insert($entity);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
view any unpublished content:
|
||||
title: 'View any unpublished content'
|
||||
description: 'This permission is necessary for any users that may moderate content.'
|
||||
|
||||
view content moderation:
|
||||
title: 'View content moderation'
|
||||
description: 'View content moderation.'
|
||||
|
||||
'administer content moderation':
|
||||
title: 'Administer content moderation'
|
||||
description: 'Administer workflows on content entities.'
|
||||
'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,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: ['@content_moderation.moderation_information']
|
||||
content_moderation.moderation_information:
|
||||
class: Drupal\content_moderation\ModerationInformation
|
||||
arguments: ['@entity_type.manager', '@entity_type.bundle.info']
|
||||
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,19 @@
|
||||
/**
|
||||
* @file
|
||||
* Component styles for the content_moderation module.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
ul.entity-moderation-form input[type=submit] {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @file
|
||||
* Theme styles for the content_moderation module.
|
||||
*/
|
||||
ul.entity-moderation-form {
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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 Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
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.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The current user account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityAccessCheck
|
||||
*/
|
||||
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
|
||||
// This tab should not show up unless there's a reason to show it.
|
||||
$entity = $this->loadEntity($route, $route_match);
|
||||
if ($this->moderationInfo->hasForwardRevision($entity)) {
|
||||
// Check the global permissions first.
|
||||
$access_result = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view any unpublished content']);
|
||||
if (!$access_result->isAllowed()) {
|
||||
// Check entity owner access.
|
||||
$owner_access = AccessResult::allowedIfHasPermissions($account, ['view latest version', 'view own unpublished content']);
|
||||
$owner_access = $owner_access->andIf((AccessResult::allowedIf($entity instanceof EntityOwnerInterface && ($entity->getOwnerId() == $account->id()))));
|
||||
$access_result = $access_result->orIf($owner_access);
|
||||
}
|
||||
|
||||
return $access_result->addCacheableDependency($entity);
|
||||
}
|
||||
|
||||
return 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,114 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\workflows\StateInterface;
|
||||
|
||||
/**
|
||||
* A value object representing a workflow state for content moderation.
|
||||
*/
|
||||
class ContentModerationState implements StateInterface {
|
||||
|
||||
/**
|
||||
* The vanilla state object from the Workflow module.
|
||||
*
|
||||
* @var \Drupal\workflows\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* If entities should be published if in this state.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $published;
|
||||
|
||||
/**
|
||||
* If entities should be the default revision if in this state.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defaultRevision;
|
||||
|
||||
/**
|
||||
* ContentModerationState constructor.
|
||||
*
|
||||
* Decorates state objects to add methods to determine if an entity should be
|
||||
* published or made the default revision.
|
||||
*
|
||||
* @param \Drupal\workflows\StateInterface $state
|
||||
* The vanilla state object from the Workflow module.
|
||||
* @param bool $published
|
||||
* (optional) TRUE if entities should be published if in this state, FALSE
|
||||
* if not. Defaults to FALSE.
|
||||
* @param bool $default_revision
|
||||
* (optional) TRUE if entities should be the default revision if in this
|
||||
* state, FALSE if not. Defaults to FALSE.
|
||||
*/
|
||||
public function __construct(StateInterface $state, $published = FALSE, $default_revision = FALSE) {
|
||||
$this->state = $state;
|
||||
$this->published = $published;
|
||||
$this->defaultRevision = $default_revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if entities should be published if in this state.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPublishedState() {
|
||||
return $this->published;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if entities should be the default revision if in this state.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefaultRevisionState() {
|
||||
return $this->defaultRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function id() {
|
||||
return $this->state->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->state->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function weight() {
|
||||
return $this->state->weight();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canTransitionTo($to_state_id) {
|
||||
return $this->state->canTransitionTo($to_state_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTransitionTo($to_state_id) {
|
||||
return $this->state->getTransitionTo($to_state_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTransitions() {
|
||||
return $this->state->getTransitions();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* The access control handler for the content_moderation_state entity type.
|
||||
*
|
||||
* @see \Drupal\content_moderation\Entity\ContentModerationState
|
||||
*/
|
||||
class ContentModerationStateAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
// ContentModerationState is an internal entity type. Access is denied for
|
||||
// viewing, updating, and deleting. In order to update an entity's
|
||||
// moderation state use its moderation_state field.
|
||||
return AccessResult::forbidden('ContentModerationState is an internal entity type.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
|
||||
// ContentModerationState is an internal entity type. Access is denied for
|
||||
// creating. In order to update an entity's moderation state use its
|
||||
// moderation_state field.
|
||||
return AccessResult::forbidden('ContentModerationState is an internal entity type.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,38 @@
|
||||
<?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 unique keys to guarantee the integrity of the entity and to make
|
||||
// the lookup in ModerationStateFieldItemList::getModerationState() fast.
|
||||
$unique_keys = [
|
||||
'content_entity_type_id',
|
||||
'content_entity_id',
|
||||
'content_entity_revision_id',
|
||||
'workflow',
|
||||
'langcode',
|
||||
];
|
||||
$schema['content_moderation_state_field_data']['unique keys'] += [
|
||||
'content_moderation_state__lookup' => $unique_keys,
|
||||
];
|
||||
$schema['content_moderation_state_field_revision']['unique keys'] += [
|
||||
'content_moderation_state__lookup' => $unique_keys,
|
||||
];
|
||||
|
||||
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,182 @@
|
||||
<?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",
|
||||
* "access" = "Drupal\content_moderation\ContentModerationStateAccessControlHandler",
|
||||
* },
|
||||
* 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['workflow'] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('Workflow'))
|
||||
->setDescription(t('The workflow the moderation state is in.'))
|
||||
->setSetting('target_type', 'workflow')
|
||||
->setRequired(TRUE)
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$fields['moderation_state'] = BaseFieldDefinition::create('string')
|
||||
->setLabel(t('Moderation state'))
|
||||
->setDescription(t('The moderation state of the referenced content.'))
|
||||
->setRequired(TRUE)
|
||||
->setTranslatable(TRUE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$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)
|
||||
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH)
|
||||
->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);
|
||||
|
||||
$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 [\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 = $this->moderation_state;
|
||||
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,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityHandlerInterface;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
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);
|
||||
|
||||
// Update publishing status if it can be updated and if it needs updating.
|
||||
if (($entity instanceof EntityPublishedInterface) && $entity->isPublished() !== $published_state) {
|
||||
$published_state ? $entity->setPublished() : $entity->setUnpublished();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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,57 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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,64 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Entity\Handler;
|
||||
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Customizations for node entities.
|
||||
*/
|
||||
class NodeModerationHandler extends ModerationHandler {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* NodeModerationHandler constructor.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 ($this->moderationInfo->getWorkflowForEntity($entity)) {
|
||||
// Force the revision checkbox on.
|
||||
$form['workflow']['options']['#default_value']['revision'] = 'revision';
|
||||
$form['workflow']['options']['revision']['#disabled'] = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\TypedData\TranslatableInterface;
|
||||
use Drupal\content_moderation\Form\EntityModerationForm;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The entity bundle information service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
|
||||
* The entity bundle information service.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->formBuilder = $form_builder;
|
||||
$this->tracker = $tracker;
|
||||
$this->bundleInfo = $bundle_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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'),
|
||||
$container->get('entity_type.bundle.info')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->value) {
|
||||
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
|
||||
/** @var \Drupal\content_moderation\ContentModerationState $current_state */
|
||||
$current_state = $workflow->getState($entity->moderation_state->value);
|
||||
|
||||
// This entity is default if it is new, a new translation, the default
|
||||
// revision, or the default revision is not published.
|
||||
$update_default_revision = $entity->isNew()
|
||||
|| $entity->isNewTranslation()
|
||||
|| $current_state->isDefaultRevisionState()
|
||||
|| !$this->isDefaultRevisionPublished($entity, $workflow);
|
||||
|
||||
// Fire per-entity-type logic for handling the save process.
|
||||
$this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->value;
|
||||
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
if (!$moderation_state) {
|
||||
$moderation_state = $workflow->getTypePlugin()->getInitialState($workflow, $entity)->id();
|
||||
}
|
||||
|
||||
// @todo what if $entity->moderation_state 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,
|
||||
'workflow' => $workflow->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,
|
||||
]);
|
||||
$content_moderation_state->workflow->target_id = $workflow->id();
|
||||
}
|
||||
elseif ($content_moderation_state->content_entity_revision_id->value != $entity_revision_id) {
|
||||
// If a new revision of the content has been created, add a new content
|
||||
// moderation state revision.
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
}
|
||||
|
||||
// Sync translations.
|
||||
if ($entity->getEntityType()->hasKey('langcode')) {
|
||||
$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);
|
||||
ContentModerationStateEntity::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;
|
||||
}
|
||||
if ($this->moderationInfo->isLiveRevision($entity)) {
|
||||
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, check if any of the
|
||||
* translations are published.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* The workflow being applied to the entity.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the default revision is published. FALSE otherwise.
|
||||
*/
|
||||
protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) {
|
||||
$default_revision = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id());
|
||||
|
||||
// Ensure we are checking all translations of the default revision.
|
||||
if ($default_revision instanceof TranslatableInterface && $default_revision->isTranslatable()) {
|
||||
// Loop through each language that has a translation.
|
||||
foreach ($default_revision->getTranslationLanguages() as $language) {
|
||||
// Load the translated revision.
|
||||
$language_revision = $default_revision->getTranslation($language->getId());
|
||||
// Return TRUE if a translation with a published state is found.
|
||||
if ($workflow->getState($language_revision->moderation_state->value)->isPublishedState()) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
<?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\EntityTypeBundleInfoInterface;
|
||||
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 bundle information service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
|
||||
* Bundle information service.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* Current user.
|
||||
*/
|
||||
public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user) {
|
||||
$this->stringTranslation = $translation;
|
||||
$this->moderationInfo = $moderation_information;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->bundleInfo = $bundle_info;
|
||||
$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('entity_type.bundle.info'),
|
||||
$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 ($entity_types as $entity_type_id => $entity_type) {
|
||||
// The ContentModerationState entity type should never be moderated.
|
||||
if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
|
||||
$entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
|
||||
// Add additional moderation support to entity types whose bundles are
|
||||
// managed by a config entity type.
|
||||
if ($entity_type->getBundleEntityType()) {
|
||||
$entity_types[$entity_type->getBundleEntityType()] = $this->addModerationToBundleEntityType($entity_types[$entity_type->getBundleEntityType()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 addModerationToEntityType(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 addModerationToBundleEntityType(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 content moderation') && $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() {
|
||||
$entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
|
||||
foreach ($entity_types as $type_name => $type) {
|
||||
foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
|
||||
if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
|
||||
yield ['entity' => $type_name, 'bundle' => $bundle_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('string')
|
||||
->setLabel(t('Moderation state'))
|
||||
->setDescription(t('The moderation state of this piece of content.'))
|
||||
->setComputed(TRUE)
|
||||
->setClass(ModerationStateFieldItemList::class)
|
||||
->setSetting('target_type', 'moderation_state')
|
||||
->setDisplayOptions('view', [
|
||||
'label' => 'hidden',
|
||||
'region' => '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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\content_moderation\Plugin\WorkflowType\ContentModeration;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
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\ConfigEntityInterface $bundle */
|
||||
$bundle = $this->getEntity();
|
||||
$bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle->getEntityType()->getBundleOf());
|
||||
/* @var \Drupal\workflows\WorkflowInterface[] $workflows */
|
||||
$workflows = $this->entityTypeManager->getStorage('workflow')->loadMultiple();
|
||||
|
||||
$options = array_map(function (WorkflowInterface $workflow) {
|
||||
return $workflow->label();
|
||||
}, array_filter($workflows, function (WorkflowInterface $workflow) {
|
||||
return $workflow->status() && $workflow->getTypePlugin() instanceof ContentModeration;
|
||||
}));
|
||||
|
||||
$selected_workflow = array_reduce($workflows, function ($carry, WorkflowInterface $workflow) use ($bundle_of_entity_type, $bundle) {
|
||||
$plugin = $workflow->getTypePlugin();
|
||||
if ($plugin instanceof ContentModeration && $plugin->appliesToEntityTypeAndBundle($bundle_of_entity_type->id(), $bundle->id())) {
|
||||
return $workflow->id();
|
||||
}
|
||||
return $carry;
|
||||
});
|
||||
$form['workflow'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Select the workflow to apply'),
|
||||
'#default_value' => $selected_workflow,
|
||||
'#options' => $options,
|
||||
'#required' => FALSE,
|
||||
'#empty_value' => '',
|
||||
];
|
||||
|
||||
$form['original_workflow'] = [
|
||||
'#type' => 'value',
|
||||
'#value' => $selected_workflow,
|
||||
];
|
||||
|
||||
$form['bundle'] = [
|
||||
'#type' => 'value',
|
||||
'#value' => $bundle->id(),
|
||||
];
|
||||
|
||||
$form['entity_type'] = [
|
||||
'#type' => 'value',
|
||||
'#value' => $bundle_of_entity_type->id(),
|
||||
];
|
||||
|
||||
// Add a special message when moderation is being disabled.
|
||||
if ($selected_workflow) {
|
||||
$form['enable_workflow_note'] = [
|
||||
'#type' => 'item',
|
||||
'#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
|
||||
'#access' => !empty($selected_workflow)
|
||||
];
|
||||
}
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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.
|
||||
drupal_set_message($this->t('Your settings have been saved.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$entity_type_id = $form_state->getValue('entity_type');
|
||||
$bundle_id = $form_state->getValue('bundle');
|
||||
$new_workflow_id = $form_state->getValue('workflow');
|
||||
$original_workflow_id = $form_state->getValue('original_workflow');
|
||||
if ($new_workflow_id === $original_workflow_id) {
|
||||
// Nothing to do.
|
||||
return;
|
||||
}
|
||||
if ($original_workflow_id) {
|
||||
/* @var \Drupal\workflows\WorkflowInterface $workflow */
|
||||
$workflow = $this->entityTypeManager->getStorage('workflow')->load($original_workflow_id);
|
||||
$workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$workflow->save();
|
||||
}
|
||||
if ($new_workflow_id) {
|
||||
/* @var \Drupal\workflows\WorkflowInterface $workflow */
|
||||
$workflow = $this->entityTypeManager->getStorage('workflow')->load($new_workflow_id);
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$workflow->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function actions(array $form, FormStateInterface $form_state) {
|
||||
$actions['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
'#submit' => ['::submitForm', '::save'],
|
||||
];
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Form;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\RevisionLogInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Drupal\workflows\Transition;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
$this->validation = $validation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('content_moderation.moderation_information'),
|
||||
$container->get('content_moderation.state_transition_validation')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'content_moderation_entity_moderation_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) {
|
||||
$current_state = $entity->moderation_state->value;
|
||||
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
|
||||
|
||||
/** @var \Drupal\workflows\Transition[] $transitions */
|
||||
$transitions = $this->validation->getValidTransitions($entity, $this->currentUser());
|
||||
|
||||
// Exclude self-transitions.
|
||||
$transitions = array_filter($transitions, function(Transition $transition) use ($current_state) {
|
||||
return $transition->to()->id() != $current_state;
|
||||
});
|
||||
|
||||
$target_states = [];
|
||||
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->to()->id()] = $transition->to()->label();
|
||||
}
|
||||
|
||||
if (!count($target_states)) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
if ($current_state) {
|
||||
$form['current'] = [
|
||||
'#type' => 'item',
|
||||
'#title' => $this->t('Status'),
|
||||
'#markup' => $workflow->getState($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');
|
||||
|
||||
$entity->set('moderation_state', $new_state);
|
||||
|
||||
if ($entity instanceof RevisionLogInterface) {
|
||||
$entity->setRevisionLogMessage($form_state->getValue('revision_log'));
|
||||
$entity->setRevisionUserId($this->currentUser()->id());
|
||||
}
|
||||
$entity->save();
|
||||
|
||||
drupal_set_message($this->t('The moderation state has been updated.'));
|
||||
|
||||
$new_state = $this->moderationInfo->getWorkflowForEntity($entity)->getState($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 ($new_state->isDefaultRevisionState()) {
|
||||
$form_state->setRedirectUrl($entity->toUrl('canonical'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The bundle information service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $bundleInfo;
|
||||
|
||||
/**
|
||||
* Creates a new ModerationInformation instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
|
||||
* The bundle information service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->bundleInfo = $bundle_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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)) {
|
||||
$bundles = $this->bundleInfo->getBundleInfo($entity_type->id());
|
||||
return isset($bundles[$bundle]['workflow']);
|
||||
}
|
||||
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) {
|
||||
$workflow = $this->getWorkflowForEntity($entity);
|
||||
return $this->isLatestRevision($entity)
|
||||
&& $entity->isDefaultRevision()
|
||||
&& $entity->moderation_state->value
|
||||
&& $workflow->getState($entity->moderation_state->value)->isPublishedState();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getWorkflowForEntity(ContentEntityInterface $entity) {
|
||||
$bundles = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
|
||||
if (isset($bundles[$entity->bundle()]['workflow'])) {
|
||||
return $this->entityTypeManager->getStorage('workflow')->load($bundles[$entity->bundle()]['workflow']);
|
||||
};
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?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);
|
||||
|
||||
/**
|
||||
* Gets the workflow for the given content entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The content entity to get the workflow for.
|
||||
*
|
||||
* @return \Drupal\workflows\WorkflowInterface|null
|
||||
* The workflow entity. NULL if there is no workflow.
|
||||
*/
|
||||
public function getWorkflowForEntity(ContentEntityInterface $entity);
|
||||
|
||||
}
|
||||
@@ -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, ['operation' => 'entity_upcast']);
|
||||
}
|
||||
|
||||
if ($latest_revision instanceof EntityInterface && $latest_revision->isRevisionTranslationAffected()) {
|
||||
$entity = $latest_revision;
|
||||
}
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$permissions = [];
|
||||
/** @var \Drupal\workflows\WorkflowInterface $workflow */
|
||||
foreach (Workflow::loadMultipleByType('content_moderation') as $id => $workflow) {
|
||||
foreach ($workflow->getTransitions() as $transition) {
|
||||
$permissions['use ' . $workflow->id() . ' transition ' . $transition->id()] = [
|
||||
'title' => $this->t('Use %transition transition from %workflow workflow.', [
|
||||
'%transition' => $transition->label(),
|
||||
'%workflow' => $workflow->label(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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 access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
/** @var \Drupal\node\NodeInterface $entity */
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly published.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
|
||||
$result = AccessResult::forbidden();
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
return parent::access($entity, $account, $return_as_object);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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 access($entity, AccountInterface $account = NULL, $return_as_object = FALSE) {
|
||||
/** @var \Drupal\node\NodeInterface $entity */
|
||||
if ($entity && $this->moderationInfo->isModeratedEntity($entity)) {
|
||||
drupal_set_message($this->t("@bundle @label were skipped as they are under moderation and may not be directly unpublished.", ['@bundle' => node_get_type_label($entity), '@label' => $entity->getEntityType()->getPluralLabel()]), 'warning');
|
||||
$result = AccessResult::forbidden();
|
||||
return $return_as_object ? $result : $result->isAllowed();
|
||||
}
|
||||
return parent::access($entity, $account, $return_as_object);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FormatterBase;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'content_moderation_state' formatter.
|
||||
*
|
||||
* @FieldFormatter(
|
||||
* id = "content_moderation_state",
|
||||
* label = @Translation("Content moderation state"),
|
||||
* field_types = {
|
||||
* "string",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ContentModerationStateFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* Create an instance of ContentModerationStateFormatter.
|
||||
*/
|
||||
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, ModerationInformationInterface $moderation_information) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
|
||||
$this->moderationInformation = $moderation_information;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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['label'],
|
||||
$configuration['view_mode'],
|
||||
$configuration['third_party_settings'],
|
||||
$container->get('content_moderation.moderation_information')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$elements = [];
|
||||
$workflow = $this->moderationInformation->getWorkflowForEntity($items->getEntity());
|
||||
foreach ($items as $delta => $item) {
|
||||
$elements[$delta] = [
|
||||
'#markup' => $workflow->getState($item->value)->label(),
|
||||
];
|
||||
}
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition) {
|
||||
return $field_definition->getName() === 'moderation_state' && $field_definition->getTargetEntityTypeId() !== 'content_moderation_state';
|
||||
}
|
||||
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
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 = {
|
||||
* "string"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class ModerationStateWidget extends OptionsSelectWidget implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Current user service.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* Moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformation
|
||||
*/
|
||||
protected $moderationInformation;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* 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\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, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
|
||||
$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('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();
|
||||
|
||||
if (!$this->moderationInformation->isModeratedEntity($entity)) {
|
||||
// @todo https://www.drupal.org/node/2779933 write a test for this.
|
||||
return $element + ['#access' => FALSE];
|
||||
}
|
||||
|
||||
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
|
||||
$default = $items->get($delta)->value ? $workflow->getState($items->get($delta)->value) : $workflow->getTypePlugin()->getInitialState($workflow, $entity);
|
||||
|
||||
/** @var \Drupal\workflows\Transition[] $transitions */
|
||||
$transitions = $this->validator->getValidTransitions($entity, $this->currentUser);
|
||||
|
||||
$target_states = [];
|
||||
foreach ($transitions as $transition) {
|
||||
$target_states[$transition->to()->id()] = $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->id(),
|
||||
'#published' => $default->isPublishedState(),
|
||||
'#key_column' => $this->column,
|
||||
];
|
||||
$element['#element_validate'][] = [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->value = $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 $field_definition->getName() === 'moderation_state';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Field;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Field\FieldItemList;
|
||||
|
||||
/**
|
||||
* 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 FieldItemList {
|
||||
|
||||
/**
|
||||
* Gets the moderation state ID linked to a content entity revision.
|
||||
*
|
||||
* @return string|null
|
||||
* The moderation state ID linked to a content entity revision.
|
||||
*/
|
||||
protected function getModerationStateId() {
|
||||
$entity = $this->getEntity();
|
||||
|
||||
/** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
|
||||
$moderation_info = \Drupal::service('content_moderation.moderation_information');
|
||||
if (!$moderation_info->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Existing entities will have a corresponding content_moderation_state
|
||||
// entity associated with them.
|
||||
if (!$entity->isNew() && $content_moderation_state = $this->loadContentModerationStateRevision($entity)) {
|
||||
return $content_moderation_state->moderation_state->value;
|
||||
}
|
||||
|
||||
// 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()
|
||||
$workflow = $moderation_info->getWorkFlowForEntity($entity);
|
||||
return $workflow ? $workflow->getTypePlugin()->getInitialState($workflow, $entity)->id() : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the content moderation state revision associated with an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
|
||||
* The entity the content moderation state entity will be loaded from.
|
||||
*
|
||||
* @return \Drupal\content_moderation\ContentModerationStateInterface|null
|
||||
* The content_moderation_state revision or FALSE if none exists.
|
||||
*/
|
||||
protected function loadContentModerationStateRevision(ContentEntityInterface $entity) {
|
||||
$moderation_info = \Drupal::service('content_moderation.moderation_information');
|
||||
$content_moderation_storage = \Drupal::entityTypeManager()->getStorage('content_moderation_state');
|
||||
|
||||
$revisions = \Drupal::service('entity.query')->get('content_moderation_state')
|
||||
->condition('content_entity_type_id', $entity->getEntityTypeId())
|
||||
->condition('content_entity_id', $entity->id())
|
||||
// Ensure the correct revision is loaded in scenarios where a revision is
|
||||
// being reverted.
|
||||
->condition('content_entity_revision_id', $entity->isNewRevision() ? $entity->getLoadedRevisionId() : $entity->getRevisionId())
|
||||
->condition('workflow', $moderation_info->getWorkflowForEntity($entity)->id())
|
||||
->allRevisions()
|
||||
->sort('revision_id', 'DESC')
|
||||
->execute();
|
||||
if (empty($revisions)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
|
||||
$content_moderation_state = $content_moderation_storage->loadRevision(key($revisions));
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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->getModerationStateId();
|
||||
// Do not store NULL values in the static cache.
|
||||
if ($moderation_state) {
|
||||
$this->list[$index] = $this->createItem($index, $moderation_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -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';
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
$workflow = $this->moderationInformation->getWorkflowForEntity($entity);
|
||||
$new_state = $workflow->getState($entity->moderation_state->value) ?: $workflow->getInitialState();
|
||||
$original_state = $workflow->getState($original_entity->moderation_state->value);
|
||||
// @todo - what if $new_state references something that does not exist or
|
||||
// is null.
|
||||
if (!$original_state->canTransitionTo($new_state->id())) {
|
||||
$this->context->addViolation($constraint->message, ['%from' => $original_state->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());
|
||||
|
||||
if ($original_entity) {
|
||||
$original_id = $original_entity->moderation_state;
|
||||
}
|
||||
|
||||
return !($entity->moderation_state && $original_entity && $original_id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\Plugin\WorkflowType;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\content_moderation\ContentModerationState;
|
||||
use Drupal\workflows\Plugin\WorkflowTypeBase;
|
||||
use Drupal\workflows\StateInterface;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Attaches workflows to content entity types and their bundles.
|
||||
*
|
||||
* @WorkflowType(
|
||||
* id = "content_moderation",
|
||||
* label = @Translation("Content moderation"),
|
||||
* required_states = {
|
||||
* "draft",
|
||||
* "published",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class ContentModeration extends WorkflowTypeBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Creates an instance of the ContentModeration WorkflowType plugin.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function initializeWorkflow(WorkflowInterface $workflow) {
|
||||
$workflow
|
||||
->addState('draft', $this->t('Draft'))
|
||||
->setStateWeight('draft', -5)
|
||||
->addState('published', $this->t('Published'))
|
||||
->setStateWeight('published', 0)
|
||||
->addTransition('create_new_draft', $this->t('Create New Draft'), ['draft', 'published'], 'draft')
|
||||
->addTransition('publish', $this->t('Publish'), ['draft', 'published'], 'published');
|
||||
return $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
|
||||
if ($operation === 'view') {
|
||||
return AccessResult::allowedIfHasPermission($account, 'view content moderation');
|
||||
}
|
||||
return parent::checkWorkflowAccess($entity, $operation, $account);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decorateState(StateInterface $state) {
|
||||
if (isset($this->configuration['states'][$state->id()])) {
|
||||
$state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']);
|
||||
}
|
||||
else {
|
||||
$state = new ContentModerationState($state);
|
||||
}
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
|
||||
/** @var \Drupal\content_moderation\ContentModerationState $state */
|
||||
$is_required_state = isset($state) ? in_array($state->id(), $this->getRequiredStates(), TRUE) : FALSE;
|
||||
|
||||
$form = [];
|
||||
$form['published'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Published'),
|
||||
'#description' => $this->t('When content reaches this state it should be published.'),
|
||||
'#default_value' => isset($state) ? $state->isPublishedState() : FALSE,
|
||||
'#disabled' => $is_required_state,
|
||||
];
|
||||
|
||||
$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' => isset($state) ? $state->isDefaultRevisionState() : FALSE,
|
||||
'#disabled' => $is_required_state,
|
||||
// @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity types the workflow is applied to.
|
||||
*
|
||||
* @return string[]
|
||||
* The entity types the workflow is applied to.
|
||||
*/
|
||||
public function getEntityTypes() {
|
||||
return array_keys($this->configuration['entity_types']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any bundles the workflow is applied to for the given entity type.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to get the bundles for.
|
||||
*
|
||||
* @return string[]
|
||||
* The bundles of the entity type the workflow is applied to or an empty
|
||||
* array if the entity type is not applied to the workflow.
|
||||
*/
|
||||
public function getBundlesForEntityType($entity_type_id) {
|
||||
return isset($this->configuration['entity_types'][$entity_type_id]) ? $this->configuration['entity_types'][$entity_type_id] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the workflow applies to the supplied entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to check.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workflow applies to the supplied entity type ID and bundle
|
||||
* ID. FALSE if not.
|
||||
*/
|
||||
public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
return in_array($bundle_id, $this->getBundlesForEntityType($entity_type_id), TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an entity type ID / bundle ID from the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to remove.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to remove.
|
||||
*/
|
||||
public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
$key = array_search($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE);
|
||||
if ($key !== FALSE) {
|
||||
unset($this->configuration['entity_types'][$entity_type_id][$key]);
|
||||
if (empty($this->configuration['entity_types'][$entity_type_id])) {
|
||||
unset($this->configuration['entity_types'][$entity_type_id]);
|
||||
}
|
||||
else {
|
||||
$this->configuration['entity_types'][$entity_type_id] = array_values($this->configuration['entity_types'][$entity_type_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entity type ID / bundle ID to the workflow.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to add. It is responsibility of the caller to provide
|
||||
* a valid entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID to add. It is responsibility of the caller to provide a
|
||||
* valid bundle ID.
|
||||
*/
|
||||
public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
|
||||
if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
|
||||
$this->configuration['entity_types'][$entity_type_id][] = $bundle_id;
|
||||
sort($this->configuration['entity_types'][$entity_type_id]);
|
||||
ksort($this->configuration['entity_types']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
// This plugin does not store anything per transition.
|
||||
return [
|
||||
'states' => [
|
||||
'draft' => [
|
||||
'published' => FALSE,
|
||||
'default_revision' => FALSE,
|
||||
],
|
||||
'published' => [
|
||||
'published' => TRUE,
|
||||
'default_revision' => TRUE,
|
||||
],
|
||||
],
|
||||
'entity_types' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
$dependencies = parent::calculateDependencies();
|
||||
foreach ($this->getEntityTypes() as $entity_type_id) {
|
||||
$entity_definition = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
foreach ($this->getBundlesForEntityType($entity_type_id) as $bundle) {
|
||||
$dependency = $entity_definition->getBundleConfigDependency($bundle);
|
||||
$dependencies[$dependency['type']][] = $dependency['name'];
|
||||
}
|
||||
}
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onDependencyRemoval(array $dependencies) {
|
||||
$changed = parent::onDependencyRemoval($dependencies);
|
||||
|
||||
// When bundle config entities are removed, ensure they are cleaned up from
|
||||
// the workflow.
|
||||
foreach ($dependencies['config'] as $removed_config) {
|
||||
if ($entity_type_id = $removed_config->getEntityType()->getBundleOf()) {
|
||||
$bundle_id = $removed_config->id();
|
||||
$this->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
// When modules that provide entity types are removed, ensure they are also
|
||||
// removed from the workflow.
|
||||
if (!empty($dependencies['module'])) {
|
||||
// Gather all entity definitions provided by the dependent modules which
|
||||
// are being removed.
|
||||
$module_entity_definitions = [];
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_definition) {
|
||||
if (in_array($entity_definition->getProvider(), $dependencies['module'])) {
|
||||
$module_entity_definitions[] = $entity_definition;
|
||||
}
|
||||
}
|
||||
|
||||
// For all entity types provided by the uninstalled modules, remove any
|
||||
// configuration for those types.
|
||||
foreach ($module_entity_definitions as $module_entity_definition) {
|
||||
foreach ($this->getBundlesForEntityType($module_entity_definition->id()) as $bundle) {
|
||||
$this->removeEntityTypeAndBundle($module_entity_definition->id(), $bundle);
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfiguration() {
|
||||
$configuration = parent::getConfiguration();
|
||||
// Ensure that states and entity types are ordered consistently.
|
||||
ksort($configuration['states']);
|
||||
ksort($configuration['entity_types']);
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInitialState(WorkflowInterface $workflow, $entity = NULL) {
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
return $workflow->getState($entity->isPublished() && !$entity->isNew() ? 'published' : 'draft');
|
||||
}
|
||||
return parent::getInitialState($workflow);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,121 @@
|
||||
<?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('_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->entityClassImplements(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 content moderation')
|
||||
->setOption('parameters', [
|
||||
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
|
||||
]);
|
||||
|
||||
return $route;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workflows\Transition;
|
||||
|
||||
/**
|
||||
* Validates whether a certain state transition is allowed.
|
||||
*/
|
||||
class StateTransitionValidation implements StateTransitionValidationInterface {
|
||||
|
||||
/**
|
||||
* The moderation information service.
|
||||
*
|
||||
* @var \Drupal\content_moderation\ModerationInformationInterface
|
||||
*/
|
||||
protected $moderationInfo;
|
||||
|
||||
/**
|
||||
* Stores the possible state transitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $possibleTransitions = [];
|
||||
|
||||
/**
|
||||
* Constructs a new StateTransitionValidation.
|
||||
*
|
||||
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
|
||||
* The moderation information service.
|
||||
*/
|
||||
public function __construct(ModerationInformationInterface $moderation_info) {
|
||||
$this->moderationInfo = $moderation_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
|
||||
$workflow = $this->moderationInfo->getWorkflowForEntity($entity);
|
||||
$current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getTypePlugin()->getInitialState($workflow, $entity);
|
||||
|
||||
return array_filter($current_state->getTransitions(), function(Transition $transition) use ($workflow, $user) {
|
||||
return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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 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\workflows\Transition[]
|
||||
* The list of transitions that are legal for this user on this entity.
|
||||
*/
|
||||
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?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'),
|
||||
'extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
'field' => ['default_formatter' => 'content_moderation_state'],
|
||||
];
|
||||
|
||||
$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'),
|
||||
'extra' => [
|
||||
[
|
||||
'field' => 'content_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
'field' => ['default_formatter' => 'content_moderation_state'],
|
||||
];
|
||||
}
|
||||
|
||||
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/content_moderation') }}
|
||||
<ul class="entity-moderation-form">
|
||||
<li>{{ form.current }}</li>
|
||||
<li>{{ form.new_state }}</li>
|
||||
<li>{{ form.revision_log }}</li>
|
||||
<li>{{ form.submit }}</li>
|
||||
</ul>
|
||||
{{ form|without('current', 'new_state', 'revision_log', 'submit') }}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
name: 'Content moderation test local task'
|
||||
type: module
|
||||
description: 'Provides a local task for testing.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
dependencies:
|
||||
- content_moderation
|
||||
- node
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
entity.node.test_local_task_without_upcast_node:
|
||||
route_name: entity.node.test_local_task_without_upcast_node
|
||||
base_route: entity.node.canonical
|
||||
title: 'Task Without Upcast Node'
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
entity.node.test_local_task_without_upcast_node:
|
||||
path: '/node/{node}/task-without-upcast-node'
|
||||
defaults:
|
||||
_title: 'Page Without Upcast Node'
|
||||
_controller: '\Drupal\content_moderation_test_local_task\Controller\TestLocalTaskController::methodWithoutUpcastNode'
|
||||
requirements:
|
||||
_access: 'TRUE'
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\content_moderation_test_local_task\Controller;
|
||||
|
||||
/**
|
||||
* A test controller.
|
||||
*/
|
||||
class TestLocalTaskController {
|
||||
|
||||
/**
|
||||
* A method which does not hint the node parameter to avoid upcasting.
|
||||
*/
|
||||
public function methodWithoutUpcastNode($node) {
|
||||
return ['#markup' => 'It works!'];
|
||||
}
|
||||
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
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: content_moderation_state
|
||||
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: { }
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
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: content_moderation_state
|
||||
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: content_moderation_state
|
||||
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: string
|
||||
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: { }
|
||||
+447
@@ -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: string
|
||||
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: string
|
||||
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: { }
|
||||
+315
@@ -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: string
|
||||
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: { }
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
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
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-08-16
|
||||
version: '8.3.7'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1502903957
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test the workflow type plugin in the content_moderation module.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationWorkflowTypeTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$admin = $this->drupalCreateUser([
|
||||
'administer workflows',
|
||||
]);
|
||||
$this->drupalLogin($admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a new workflow using the content moderation plugin.
|
||||
*/
|
||||
public function testNewWorkflow() {
|
||||
$entity_bundle_info = \Drupal::service('entity_type.bundle.info');
|
||||
|
||||
$this->drupalPostForm('admin/config/workflow/workflows/add', [
|
||||
'label' => 'Test Workflow',
|
||||
'id' => 'test_workflow',
|
||||
'workflow_type' => 'content_moderation',
|
||||
], 'Save');
|
||||
|
||||
// Make sure the test workflow includes the default states and transitions.
|
||||
$this->assertSession()->pageTextContains('Draft');
|
||||
$this->assertSession()->pageTextContains('Published');
|
||||
$this->assertSession()->pageTextContains('Create New Draft');
|
||||
$this->assertSession()->pageTextContains('Publish');
|
||||
|
||||
// Ensure after a workflow is created, the bundle information can be
|
||||
// refreshed.
|
||||
$entity_bundle_info->clearCachedBundles();
|
||||
$this->assertNotEmpty($entity_bundle_info->getAllBundleInfo());
|
||||
|
||||
$this->clickLink('Add a new state');
|
||||
$this->submitForm([
|
||||
'label' => 'Test State',
|
||||
'id' => 'test_state',
|
||||
'type_settings[content_moderation][published]' => TRUE,
|
||||
'type_settings[content_moderation][default_revision]' => FALSE,
|
||||
], 'Save');
|
||||
$this->assertSession()->pageTextContains('Created Test State state.');
|
||||
|
||||
// Ensure that the published settings cannot be changed.
|
||||
$this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/state/published');
|
||||
$this->assertSession()->fieldDisabled('type_settings[content_moderation][published]');
|
||||
$this->assertSession()->fieldDisabled('type_settings[content_moderation][default_revision]');
|
||||
|
||||
// Ensure that the draft settings cannot be changed.
|
||||
$this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/state/draft');
|
||||
$this->assertSession()->fieldDisabled('type_settings[content_moderation][published]');
|
||||
$this->assertSession()->fieldDisabled('type_settings[content_moderation][default_revision]');
|
||||
}
|
||||
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$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.
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test');
|
||||
$workflow->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->value = '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->value = 'draft';
|
||||
$node_2->save();
|
||||
|
||||
$node_2->setTitle('Node 2 - Rev 2');
|
||||
$node_2->moderation_state->value = '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->value = 'draft';
|
||||
$node_3->save();
|
||||
|
||||
$node_3->setTitle('Node 3 - Rev 2');
|
||||
$node_3->moderation_state->value = 'published';
|
||||
$node_3->save();
|
||||
|
||||
$node_3->setTitle('Node 3 - Rev 3');
|
||||
$node_3->moderation_state->value = '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,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\simpletest\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Test the content moderation actions.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationActionsTest extends BrowserTestBase {
|
||||
|
||||
use ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
'views',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$moderated_bundle = $this->createContentType(['type' => 'moderated_bundle']);
|
||||
$moderated_bundle->save();
|
||||
$standard_bundle = $this->createContentType(['type' => 'standard_bundle']);
|
||||
$standard_bundle->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
|
||||
$workflow->save();
|
||||
|
||||
$admin = $this->drupalCreateUser([
|
||||
'access content overview',
|
||||
'administer nodes',
|
||||
'bypass node access',
|
||||
]);
|
||||
$this->drupalLogin($admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the node status actions report moderation status to users correctly.
|
||||
*
|
||||
* @dataProvider nodeStatusActionsTestCases
|
||||
*/
|
||||
public function testNodeStatusActions($action, $bundle, $warning_appears, $starting_status, $final_status) {
|
||||
// Create and run an action on a node.
|
||||
$node = Node::create([
|
||||
'type' => $bundle,
|
||||
'title' => $this->randomString(),
|
||||
'status' => $starting_status,
|
||||
]);
|
||||
if ($bundle == 'moderated_bundle') {
|
||||
$node->moderation_state->value = $starting_status ? 'published' : 'draft';
|
||||
}
|
||||
$node->save();
|
||||
|
||||
$this->drupalPostForm('admin/content', [
|
||||
'node_bulk_form[0]' => TRUE,
|
||||
'action' => $action,
|
||||
], 'Apply to selected items');
|
||||
|
||||
if ($warning_appears) {
|
||||
if ($action == 'node_publish_action') {
|
||||
$this->assertSession()
|
||||
->elementContains('css', '.messages--warning', node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly published.');
|
||||
}
|
||||
else {
|
||||
$this->assertSession()
|
||||
->elementContains('css', '.messages--warning', node_get_type_label($node) . ' content items were skipped as they are under moderation and may not be directly unpublished.');
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->assertSession()->elementNotExists('css', '.messages--warning');
|
||||
}
|
||||
|
||||
// Ensure after the action has run, the node matches the expected status.
|
||||
$node = Node::load($node->id());
|
||||
$this->assertEquals($node->isPublished(), $final_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::testNodeStatusActions.
|
||||
*
|
||||
* @return array
|
||||
* An array of test cases.
|
||||
*/
|
||||
public function nodeStatusActionsTestCases() {
|
||||
return [
|
||||
'Moderated bundle shows warning (publish action)' => [
|
||||
'node_publish_action',
|
||||
'moderated_bundle',
|
||||
TRUE,
|
||||
// If the node starts out unpublished, the action should not work.
|
||||
FALSE,
|
||||
FALSE,
|
||||
],
|
||||
'Moderated bundle shows warning (unpublish action)' => [
|
||||
'node_unpublish_action',
|
||||
'moderated_bundle',
|
||||
TRUE,
|
||||
// If the node starts out published, the action should not work.
|
||||
TRUE,
|
||||
TRUE,
|
||||
],
|
||||
'Normal bundle works (publish action)' => [
|
||||
'node_publish_action',
|
||||
'standard_bundle',
|
||||
FALSE,
|
||||
// If the node starts out unpublished, the action should work.
|
||||
FALSE,
|
||||
TRUE,
|
||||
],
|
||||
'Normal bundle works (unpublish action)' => [
|
||||
'node_unpublish_action',
|
||||
'standard_bundle',
|
||||
FALSE,
|
||||
// If the node starts out published, the action should work.
|
||||
TRUE,
|
||||
FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$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\Tests\content_moderation\Functional\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 canonical view should have a moderation form, because it is not the
|
||||
// live revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertField('edit-new-state', 'The node view page has a moderation form.');
|
||||
|
||||
// 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 canonical view should have a moderation form, because it is not the
|
||||
// live revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertField('edit-new-state', 'The node view page has a moderation form.');
|
||||
|
||||
// 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
|
||||
// live revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoField('edit-new-state', '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
|
||||
// live revision.
|
||||
$this->drupalGet($canonical_path);
|
||||
$this->assertResponse(200);
|
||||
$this->assertNoField('edit-new-state', '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->assertField('edit-new-state', 'The latest-version page has a moderation form.');
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test moderation non-bundle entity type.
|
||||
*/
|
||||
public function testNonBundleModerationForm() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
|
||||
$workflow->save();
|
||||
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('entity_test_mulrevpub/add', [], t('Save and Create New Draft'));
|
||||
|
||||
// The latest version page should not show, because there is no forward
|
||||
// revision.
|
||||
$this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Update the draft.
|
||||
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
|
||||
|
||||
// The latest version page should not show, because there is still no
|
||||
// forward revision.
|
||||
$this->drupalGet('/entity_test_mulrevpub/manage/1/latest');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Publish the draft.
|
||||
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Publish'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet('entity_test_mulrevpub/manage/1');
|
||||
$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('entity_test_mulrevpub/manage/1/latest');
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Make a forward revision.
|
||||
$this->drupalPostForm('entity_test_mulrevpub/manage/1/edit', [], t('Save and Create New Draft'));
|
||||
|
||||
// The published view should not have a moderation form, because it is the
|
||||
// default revision.
|
||||
$this->drupalGet('entity_test_mulrevpub/manage/1');
|
||||
$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('entity_test_mulrevpub/manage/1/latest');
|
||||
$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('entity_test_mulrevpub/manage/1/latest', [
|
||||
'new_state' => 'published',
|
||||
], t('Apply'));
|
||||
|
||||
// The latest version page should not show, because there is no
|
||||
// forward revision.
|
||||
$this->drupalGet('entity_test_mulrevpub/manage/1/latest');
|
||||
$this->assertResponse(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the revision author is updated when the moderation form is used.
|
||||
*/
|
||||
public function testModerationFormSetsRevisionAuthor() {
|
||||
// Create new moderated content in published.
|
||||
$node = $this->createNode(['type' => 'moderated_content', 'moderation_state' => 'published']);
|
||||
// Make a forward revision.
|
||||
$node->title = $this->randomMachineName();
|
||||
$node->moderation_state->value = 'draft';
|
||||
$node->save();
|
||||
|
||||
$another_user = $this->drupalCreateUser($this->permissions);
|
||||
$this->grantUserPermissionToCreateContentOfType($another_user, 'moderated_content');
|
||||
$this->drupalLogin($another_user);
|
||||
$this->drupalPostForm(sprintf('node/%d/latest', $node->id()), [
|
||||
'new_state' => 'published',
|
||||
], t('Apply'));
|
||||
|
||||
$this->drupalGet(sprintf('node/%d/revisions', $node->id()));
|
||||
$this->assertText('by ' . $another_user->getAccountName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// 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->value, 'published');
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->value, '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->value, 'published');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEqual($english_node->moderation_state->value, '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->value, '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->value, '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->value, '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->value, 'archived');
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
$this->assertEqual($french_node->moderation_state->value, '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,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\simpletest\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Test revision revert.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationRevisionRevertTest extends BrowserTestBase {
|
||||
|
||||
use ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$moderated_bundle = $this->createContentType(['type' => 'moderated_bundle']);
|
||||
$moderated_bundle->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'moderated_bundle');
|
||||
$workflow->save();
|
||||
|
||||
$admin = $this->drupalCreateUser([
|
||||
'access content overview',
|
||||
'administer nodes',
|
||||
'bypass node access',
|
||||
'view all revisions',
|
||||
'view content moderation',
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
]);
|
||||
$this->drupalLogin($admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that reverting a revision works.
|
||||
*/
|
||||
public function testEditingAfterRevertRevision() {
|
||||
// Create a draft.
|
||||
$this->drupalPostForm('node/add/moderated_bundle', ['title[0][value]' => 'First draft node'], t('Save and Create New Draft'));
|
||||
|
||||
// Now make it published.
|
||||
$this->drupalPostForm('node/1/edit', ['title[0][value]' => 'Published node'], t('Save and Publish'));
|
||||
|
||||
// Check the editing form that show the published title.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$this->assertSession()
|
||||
->pageTextContains('Published node');
|
||||
|
||||
// Revert the first revision.
|
||||
$revision_url = 'node/1/revisions/1/revert';
|
||||
$this->drupalGet($revision_url);
|
||||
$this->assertSession()->elementExists('css', '.form-submit');
|
||||
$this->click('.form-submit');
|
||||
|
||||
// Check that it reverted.
|
||||
$this->drupalGet('node/1/edit');
|
||||
$this->assertSession()
|
||||
->pageTextContains('First draft node');
|
||||
// Try to save the node.
|
||||
$this->click('.moderation-state-draft > input');
|
||||
|
||||
// Check if the submission passed the EntityChangedConstraintValidator.
|
||||
$this->assertSession()
|
||||
->pageTextNotContains('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
|
||||
|
||||
// Check the node has been saved.
|
||||
$this->assertSession()
|
||||
->pageTextContains('moderated_bundle First draft node has been updated');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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 content moderation',
|
||||
];
|
||||
$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->value = 'draft';
|
||||
$node_1->save();
|
||||
|
||||
$node_2 = Node::create([
|
||||
'type' => $node_type_id,
|
||||
'title' => 'Published node',
|
||||
'uid' => $editor1->id(),
|
||||
]);
|
||||
$node_2->moderation_state->value = 'published';
|
||||
$node_2->save();
|
||||
|
||||
// Resave the node with a new state.
|
||||
$node_2->setTitle('Archived node');
|
||||
$node_2->moderation_state->value = '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->hasContent('Draft'));
|
||||
$this->assertTrue($page->hasContent('Archived'));
|
||||
$this->assertFalse($page->hasContent('Published'));
|
||||
|
||||
// Now log in as an admin and test the same thing.
|
||||
$permissions = [
|
||||
'access content',
|
||||
'view all revisions',
|
||||
'administer content moderation',
|
||||
];
|
||||
$admin1 = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($admin1);
|
||||
|
||||
$this->drupalGet('/latest');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertEquals(200, $this->getSession()->getStatusCode());
|
||||
$this->assertTrue($page->hasContent('Draft'));
|
||||
$this->assertTrue($page->hasContent('Archived'));
|
||||
$this->assertFalse($page->hasContent('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->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', $machine_name);
|
||||
$workflow->save();
|
||||
return $node_type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
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 = ['workflow' => 'editorial'];
|
||||
$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 = [
|
||||
'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,141 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
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);
|
||||
$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'));
|
||||
$node = $this->getNodeByTitle('moderated content');
|
||||
if (!$node) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
}
|
||||
$this->assertEqual('draft', $node->moderation_state->value);
|
||||
|
||||
$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->value);
|
||||
|
||||
// Verify that the state field is not shown.
|
||||
$this->assertNoText('Published');
|
||||
|
||||
// Delete the node.
|
||||
$this->drupalPostForm('node/' . $node->id() . '/delete', [], t('Delete'));
|
||||
$this->assertText(t('The Moderated content moderated content has been deleted.'));
|
||||
|
||||
// Disable content moderation.
|
||||
$this->drupalPostForm('admin/structure/types/manage/moderated_content/moderation', ['workflow' => ''], t('Save'));
|
||||
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
|
||||
$this->assertOptionSelected('edit-workflow', '');
|
||||
// Ensure the parent environment is up-to-date.
|
||||
// @see content_moderation_workflow_insert()
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
|
||||
// Create a new node.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'non-moderated content',
|
||||
], t('Save and publish'));
|
||||
|
||||
$node = $this->getNodeByTitle('non-moderated content');
|
||||
if (!$node) {
|
||||
$this->fail('Non-moderated test node was not saved correctly.');
|
||||
}
|
||||
$this->assertEqual(NULL, $node->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = $element[0]->getAttribute('href');
|
||||
$query = [];
|
||||
parse_str(parse_url($url, PHP_URL_QUERY), $query);
|
||||
$this->assertEqual(0, $query['page']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
$editor_permissions = [
|
||||
'administer content moderation',
|
||||
'access administration pages',
|
||||
'administer content types',
|
||||
'administer nodes',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'access content overview',
|
||||
'use editorial transition create_new_draft',
|
||||
];
|
||||
$publish_permissions = array_merge($editor_permissions, ['use editorial transition publish']);
|
||||
$editor = $this->drupalCreateUser($editor_permissions);
|
||||
$editor_with_publish = $this->drupalCreateUser($publish_permissions);
|
||||
|
||||
// Create a node type that is not moderated.
|
||||
$this->drupalLogin($editor);
|
||||
$this->createContentTypeFromUi('Not moderated', 'not_moderated');
|
||||
$this->grantUserPermissionToCreateContentOfType($editor, 'not_moderated');
|
||||
$this->grantUserPermissionToCreateContentOfType($editor_with_publish, '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, ensuring all the expected links and tabs are
|
||||
// present.
|
||||
$this->drupalGet('admin/structure/types');
|
||||
$this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/not_moderated');
|
||||
$this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
|
||||
$this->drupalGet('admin/structure/types/manage/not_moderated/moderation');
|
||||
$this->assertOptionSelected('edit-workflow', '');
|
||||
$this->assertNoLink('Delete');
|
||||
$edit['workflow'] = 'editorial';
|
||||
$this->drupalPostForm(NULL, $edit, t('Save'));
|
||||
|
||||
// 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');
|
||||
|
||||
$this->drupalLogin($editor_with_publish);
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertResponse(200);
|
||||
$this->assertRaw('Save and Create New Draft');
|
||||
$this->assertRaw('Save and Publish');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
|
||||
/**
|
||||
* Defines a base class for moderation state tests.
|
||||
*/
|
||||
abstract class ModerationStateTestBase extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* 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 content moderation',
|
||||
'access administration pages',
|
||||
'administer content types',
|
||||
'administer nodes',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'access content overview',
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
];
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'block',
|
||||
'block_content',
|
||||
'node',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the permission machine name for a transition.
|
||||
*
|
||||
* @param string $workflow_id
|
||||
* The workflow ID.
|
||||
* @param string $transition_id
|
||||
* The transition ID.
|
||||
*
|
||||
* @return string
|
||||
* The permission machine name for a transition.
|
||||
*/
|
||||
protected function getWorkflowTransitionPermission($workflow_id, $transition_id) {
|
||||
return 'use ' . $workflow_id . ' transition ' . $transition_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $workflow_id
|
||||
* The workflow to attach to the bundle.
|
||||
*/
|
||||
protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'editorial') {
|
||||
$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, $workflow_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable moderation for a specified content type, using the UI.
|
||||
*
|
||||
* @param string $content_type_id
|
||||
* Machine name.
|
||||
* @param string $workflow_id
|
||||
* The workflow to attach to the bundle.
|
||||
*/
|
||||
protected function enableModerationThroughUi($content_type_id, $workflow_id = 'editorial') {
|
||||
$edit['workflow'] = $workflow_id;
|
||||
$this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save'));
|
||||
// Ensure the parent environment is up-to-date.
|
||||
// @see content_moderation_workflow_insert()
|
||||
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
|
||||
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,138 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
/**
|
||||
* Tests permission access control around nodes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class NodeAccessTest extends ModerationStateTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'block',
|
||||
'block_content',
|
||||
'node',
|
||||
'node_access_test_empty',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permissions to grant admin user.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'administer content moderation',
|
||||
'access administration pages',
|
||||
'administer content types',
|
||||
'administer nodes',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'access content overview',
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
'bypass node access',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
|
||||
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
|
||||
|
||||
// Rebuild permissions because hook_node_grants() is implemented by the
|
||||
// node_access_test_empty module.
|
||||
node_access_rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
$node = $this->getNodeByTitle('moderated content');
|
||||
if (!$node) {
|
||||
$this->fail('Test node was not saved correctly.');
|
||||
}
|
||||
|
||||
$view_path = 'node/' . $node->id();
|
||||
$edit_path = 'node/' . $node->id() . '/edit';
|
||||
$latest_path = 'node/' . $node->id() . '/latest';
|
||||
|
||||
// Now make a new user and verify that the new user's access is correct.
|
||||
$user = $this->createUser([
|
||||
'use editorial transition create_new_draft',
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
|
||||
// Publish the node.
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$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'));
|
||||
|
||||
$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 editorial transition create_new_draft',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet($edit_path);
|
||||
$this->assertResponse(403);
|
||||
|
||||
$this->drupalGet($latest_path);
|
||||
$this->assertResponse(403);
|
||||
$this->drupalGet($view_path);
|
||||
$this->assertResponse(200);
|
||||
}
|
||||
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Permissions;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Test to ensure content moderation permissions are generated correctly.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationPermissionsTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'workflows',
|
||||
'content_moderation',
|
||||
'workflow_type_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installEntitySchema('workflow');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test permissions generated by content moderation.
|
||||
*
|
||||
* @dataProvider permissionsTestCases
|
||||
*/
|
||||
public function testPermissions($workflow, $permissions) {
|
||||
Workflow::create($workflow)->save();
|
||||
$this->assertEquals($permissions, (new Permissions())->transitionPermissions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::testPermissions
|
||||
*
|
||||
* @return array
|
||||
* Content moderation permissions based test cases.
|
||||
*/
|
||||
public function permissionsTestCases() {
|
||||
return [
|
||||
'Simple Content Moderation Workflow' => [
|
||||
[
|
||||
'id' => 'simple_workflow',
|
||||
'label' => 'Simple Workflow',
|
||||
'type' => 'content_moderation',
|
||||
'transitions' => [
|
||||
'publish' => [
|
||||
'label' => 'Publish',
|
||||
'from' => ['draft'],
|
||||
'to' => 'published',
|
||||
'weight' => 0,
|
||||
],
|
||||
'unpublish' => [
|
||||
'label' => 'Unpublish',
|
||||
'from' => ['published'],
|
||||
'to' => 'draft',
|
||||
'weight' => 0,
|
||||
],
|
||||
],
|
||||
'states' => [
|
||||
'draft' => [
|
||||
'label' => 'Draft',
|
||||
'weight' => -5,
|
||||
],
|
||||
'published' => [
|
||||
'label' => 'Published',
|
||||
'weight' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'use simple_workflow transition publish' => [
|
||||
'title' => 'Use <em class="placeholder">Publish</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
|
||||
],
|
||||
'use simple_workflow transition unpublish' => [
|
||||
'title' => 'Use <em class="placeholder">Unpublish</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'Non Content Moderation Workflow' => [
|
||||
[
|
||||
'id' => 'morning',
|
||||
'label' => 'Morning',
|
||||
'type' => 'workflow_type_test',
|
||||
'transitions' => [
|
||||
'drink_coffee' => [
|
||||
'label' => 'Drink Coffee',
|
||||
'from' => ['tired'],
|
||||
'to' => 'awake',
|
||||
'weight' => 0,
|
||||
],
|
||||
],
|
||||
'states' => [
|
||||
'awake' => [
|
||||
'label' => 'Awake',
|
||||
'weight' => -5,
|
||||
],
|
||||
'tired' => [
|
||||
'label' => 'Tired',
|
||||
'weight' => -0,
|
||||
],
|
||||
],
|
||||
],
|
||||
[]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\ContentModerationStateAccessControlHandler
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationStateAccessControlHandlerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'workflows',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The content_moderation_state access control handler.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
|
||||
*/
|
||||
protected $accessControlHandler;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installEntitySchema('user');
|
||||
$this->accessControlHandler = $this->container->get('entity_type.manager')->getAccessControlHandler('content_moderation_state');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::checkAccess
|
||||
* @covers ::checkCreateAccess
|
||||
*/
|
||||
public function testHandler() {
|
||||
$entity = ContentModerationState::create([]);
|
||||
$this->assertFalse($this->accessControlHandler->access($entity, 'view'));
|
||||
$this->assertFalse($this->accessControlHandler->access($entity, 'update'));
|
||||
$this->assertFalse($this->accessControlHandler->access($entity, 'delete'));
|
||||
$this->assertFalse($this->accessControlHandler->createAccess());
|
||||
}
|
||||
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Test the ContentModerationState storage schema.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\content_moderation\ContentModerationStateStorageSchema
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationStateStorageSchemaTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'text',
|
||||
'workflows',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('entity_test');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
NodeType::create([
|
||||
'type' => 'example',
|
||||
])->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the ContentModerationState unique keys.
|
||||
*
|
||||
* @covers ::getEntitySchema
|
||||
*/
|
||||
public function testUniqueKeys() {
|
||||
// Create a node which will create a new ContentModerationState entity.
|
||||
$node = Node::create([
|
||||
'title' => 'Test title',
|
||||
'type' => 'example',
|
||||
'moderation_state' => 'draft',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
// Ensure an exception when all values match.
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => $node->getEntityTypeId(),
|
||||
'content_entity_id' => $node->id(),
|
||||
'content_entity_revision_id' => $node->getRevisionId(),
|
||||
], TRUE);
|
||||
|
||||
// No exception for the same values, with a different langcode.
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => $node->getEntityTypeId(),
|
||||
'content_entity_id' => $node->id(),
|
||||
'content_entity_revision_id' => $node->getRevisionId(),
|
||||
'langcode' => 'de',
|
||||
], FALSE);
|
||||
|
||||
// A different workflow should not trigger an exception.
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => $node->getEntityTypeId(),
|
||||
'content_entity_id' => $node->id(),
|
||||
'content_entity_revision_id' => $node->getRevisionId(),
|
||||
'workflow' => 'foo',
|
||||
], FALSE);
|
||||
|
||||
// Different entity types should not trigger an exception.
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => 'entity_test',
|
||||
'content_entity_id' => $node->id(),
|
||||
'content_entity_revision_id' => $node->getRevisionId(),
|
||||
], FALSE);
|
||||
|
||||
// Different entity and revision IDs should not trigger an exception.
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => $node->getEntityTypeId(),
|
||||
'content_entity_id' => 9999,
|
||||
'content_entity_revision_id' => 9999,
|
||||
], FALSE);
|
||||
|
||||
// Creating a version of the entity with a previously used, but not current
|
||||
// revision ID should trigger an exception.
|
||||
$old_revision_id = $node->getRevisionId();
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->title = 'Updated title';
|
||||
$node->moderation_state = 'published';
|
||||
$node->save();
|
||||
$this->assertStorageException([
|
||||
'content_entity_type_id' => $node->getEntityTypeId(),
|
||||
'content_entity_id' => $node->id(),
|
||||
'content_entity_revision_id' => $old_revision_id,
|
||||
], TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert if a storage exception is triggered when saving a given entity.
|
||||
*
|
||||
* @param array $values
|
||||
* An array of entity values.
|
||||
* @param bool $has_exception
|
||||
* If an exception should be triggered when saving the entity.
|
||||
*/
|
||||
protected function assertStorageException(array $values, $has_exception) {
|
||||
$defaults = [
|
||||
'moderation_state' => 'draft',
|
||||
'workflow' => 'editorial',
|
||||
];
|
||||
$entity = ContentModerationState::create($values + $defaults);
|
||||
$exception_triggered = FALSE;
|
||||
try {
|
||||
ContentModerationState::updateOrCreateFromEntity($entity);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$exception_triggered = TRUE;
|
||||
}
|
||||
$this->assertEquals($has_exception, $exception_triggered);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\content_moderation\Entity\ContentModerationState;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\entity_test\Entity\EntityTestBundle;
|
||||
use Drupal\entity_test\Entity\EntityTestWithBundle;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'block_content',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
'content_translation',
|
||||
'text',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeManager
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@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('entity_test_rev');
|
||||
$this->installEntitySchema('entity_test_no_bundle');
|
||||
$this->installEntitySchema('entity_test_mulrevpub');
|
||||
$this->installEntitySchema('block_content');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$this->entityTypeManager = $this->container->get('entity_type.manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests basic monolingual content moderation through the API.
|
||||
*
|
||||
* @dataProvider basicModerationTestCases
|
||||
*/
|
||||
public function testBasicModeration($entity_type_id) {
|
||||
// Make the 'entity_test_with_bundle' entity type revisionable.
|
||||
if ($entity_type_id == 'entity_test_with_bundle') {
|
||||
$this->setEntityTestWithBundleKeys(['revision' => 'revision_id']);
|
||||
}
|
||||
|
||||
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$bundle_id = $entity_type_id;
|
||||
$bundle_entity_type_id = $this->entityTypeManager->getDefinition($entity_type_id)->getBundleEntityType();
|
||||
if ($bundle_entity_type_id) {
|
||||
$bundle_entity_type_definition = $this->entityTypeManager->getDefinition($bundle_entity_type_id);
|
||||
$entity_type_storage = $this->entityTypeManager->getStorage($bundle_entity_type_id);
|
||||
|
||||
$entity_type = $entity_type_storage->create([
|
||||
$bundle_entity_type_definition->getKey('id') => 'example',
|
||||
]);
|
||||
$entity_type->save();
|
||||
$bundle_id = $entity_type->id();
|
||||
}
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$workflow->save();
|
||||
|
||||
$entity = $entity_storage->create([
|
||||
'title' => 'Test title',
|
||||
$this->entityTypeManager->getDefinition($entity_type_id)->getKey('bundle') => $bundle_id,
|
||||
]);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$entity->setUnpublished();
|
||||
}
|
||||
$entity->save();
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->save();
|
||||
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals('published', $entity->moderation_state->value);
|
||||
|
||||
// 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();
|
||||
|
||||
$entity = $this->reloadEntity($entity, 3);
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertFalse($entity->isPublished());
|
||||
}
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue((bool) $entity->isPublished());
|
||||
}
|
||||
$this->assertEquals(2, $entity->getRevisionId());
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->save();
|
||||
|
||||
$entity = $this->reloadEntity($entity, 4);
|
||||
$this->assertEquals('published', $entity->moderation_state->value);
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue((bool) $entity->isPublished());
|
||||
}
|
||||
$this->assertEquals(4, $entity->getRevisionId());
|
||||
|
||||
// Update the node to archived which will then be the default revision.
|
||||
$entity->moderation_state->value = 'archived';
|
||||
$entity->save();
|
||||
|
||||
// Revert to the previous (published) revision.
|
||||
$previous_revision = $entity_storage->loadRevision(4);
|
||||
$previous_revision->isDefaultRevision(TRUE);
|
||||
$previous_revision->setNewRevision(TRUE);
|
||||
$previous_revision->save();
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals('published', $entity->moderation_state->value);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue($entity->isPublished());
|
||||
}
|
||||
|
||||
// Set an invalid moderation state.
|
||||
$this->setExpectedException(EntityStorageException::class);
|
||||
$entity->moderation_state->value = 'foobar';
|
||||
$entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for basic moderation test.
|
||||
*/
|
||||
public function basicModerationTestCases() {
|
||||
return [
|
||||
'Nodes' => [
|
||||
'node',
|
||||
],
|
||||
'Block content' => [
|
||||
'block_content',
|
||||
],
|
||||
'Test Entity with Bundle' => [
|
||||
'entity_test_with_bundle',
|
||||
],
|
||||
'Test entity - revisions, data table, and published interface' => [
|
||||
'entity_test_mulrevpub',
|
||||
],
|
||||
'Entity Test with revisions' => [
|
||||
'entity_test_rev',
|
||||
],
|
||||
'Entity without bundle' => [
|
||||
'entity_test_no_bundle',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$english_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
// Revision 1 (en).
|
||||
$english_node
|
||||
->setUnpublished()
|
||||
->save();
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
|
||||
// Create a French translation.
|
||||
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
|
||||
$french_node->setUnpublished();
|
||||
// Revision 1 (fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->value);
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Move English node to create another draft.
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$english_node->moderation_state->value = 'draft';
|
||||
// Revision 2 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
|
||||
// French node should still be in draft.
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->value);
|
||||
|
||||
// Publish the French node.
|
||||
$french_node->moderation_state->value = 'published';
|
||||
// Revision 3 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($french_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertEquals('published', $french_node->moderation_state->value);
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$english_node = $french_node->getTranslation('en');
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
|
||||
// Publish the English node.
|
||||
$english_node->moderation_state->value = 'published';
|
||||
// Revision 4 (en, fr).
|
||||
$english_node->save();
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
|
||||
// Move the French node back to draft.
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$french_node->moderation_state->value = 'draft';
|
||||
// Revision 5 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($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->value = 'published';
|
||||
// Revision 6 (en, fr).
|
||||
$french_node->save();
|
||||
$french_node = $this->reloadEntity($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->reloadEntity($french_node, $french_node->getRevisionId() + 1);
|
||||
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
$french_node = $this->reloadEntity($english_node)->getTranslation('fr');
|
||||
$this->assertEquals('published', $french_node->moderation_state->value);
|
||||
|
||||
// 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->reloadEntity($english_node, $english_node->getRevisionId());
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
$french_node = $this->reloadEntity($english_node, '8')->getTranslation('fr');
|
||||
$this->assertEquals('draft', $french_node->moderation_state->value);
|
||||
// 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->reloadEntity($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.
|
||||
$this->setEntityTestWithBundleKeys(['revision' => 'revision_id']);
|
||||
|
||||
// Create a test bundle.
|
||||
$entity_test_bundle = EntityTestBundle::create([
|
||||
'id' => 'example',
|
||||
]);
|
||||
$entity_test_bundle->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
|
||||
$workflow->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->value);
|
||||
|
||||
$entity_test_with_bundle->moderation_state->value = 'published';
|
||||
$entity_test_with_bundle->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$this->setEntityTestWithBundleKeys(['revision' => 'revision_id'], ['langcode']);
|
||||
|
||||
// Create a test bundle.
|
||||
$entity_test_bundle = EntityTestBundle::create([
|
||||
'id' => 'example',
|
||||
]);
|
||||
$entity_test_bundle->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
|
||||
$workflow->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->value);
|
||||
|
||||
$entity_test_with_bundle->moderation_state->value = 'published';
|
||||
$entity_test_with_bundle->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the keys on the test entity type.
|
||||
*
|
||||
* @param array $keys
|
||||
* The entity keys to override
|
||||
* @param array $remove_keys
|
||||
* Keys to remove.
|
||||
*/
|
||||
protected function setEntityTestWithBundleKeys($keys, $remove_keys = []) {
|
||||
$entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
|
||||
$original_keys = $entity_type->getKeys();
|
||||
foreach ($remove_keys as $remove_key) {
|
||||
unset($original_keys[$remove_key]);
|
||||
}
|
||||
$entity_type->set('entity_keys', $keys + $original_keys);
|
||||
\Drupal::state()->set('entity_test_with_bundle.entity_type', $entity_type);
|
||||
\Drupal::entityDefinitionUpdateManager()->applyUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the dependencies of the workflow when using content moderation.
|
||||
*/
|
||||
public function testWorkflowDependencies() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
// Test both a config and non-config based bundle and entity type.
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
|
||||
$workflow->save();
|
||||
|
||||
$this->assertEquals([
|
||||
'module' => [
|
||||
'content_moderation',
|
||||
'entity_test',
|
||||
],
|
||||
'config' => [
|
||||
'node.type.example',
|
||||
],
|
||||
], $workflow->getDependencies());
|
||||
|
||||
$this->assertEquals([
|
||||
'entity_test_no_bundle',
|
||||
'entity_test_rev',
|
||||
'node'
|
||||
], $workflow->getTypePlugin()->getEntityTypes());
|
||||
|
||||
// Delete the node type and ensure it is removed from the workflow.
|
||||
$node_type->delete();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$entity_types = $workflow->getTypePlugin()->getEntityTypes();
|
||||
$this->assertFalse(in_array('node', $entity_types));
|
||||
|
||||
// Uninstall entity test and ensure it's removed from the workflow.
|
||||
$this->container->get('config.manager')->uninstall('module', 'entity_test');
|
||||
$workflow = Workflow::load('editorial');
|
||||
$entity_types = $workflow->getTypePlugin()->getEntityTypes();
|
||||
$this->assertEquals([], $entity_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the entity after clearing the static cache.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to reload.
|
||||
* @param int|bool $revision_id
|
||||
* The specific revision ID to load. Defaults FALSE and just loads the
|
||||
* default revision.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The reloaded entity.
|
||||
*/
|
||||
protected function reloadEntity(EntityInterface $entity, $revision_id = FALSE) {
|
||||
$storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId());
|
||||
$storage->resetCache([$entity->id()]);
|
||||
if ($revision_id) {
|
||||
return $storage->loadRevision($revision_id);
|
||||
}
|
||||
return $storage->load($entity->id());
|
||||
}
|
||||
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Tests the API of the ContentModeration workflow type plugin.
|
||||
*
|
||||
* @group content_moderation
|
||||
*
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration
|
||||
*/
|
||||
class ContentModerationWorkflowTypeApiTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* A workflow for testing.
|
||||
*
|
||||
* @var \Drupal\workflows\Entity\Workflow;
|
||||
*/
|
||||
protected $workflow;
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'workflows',
|
||||
'content_moderation',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->workflow = Workflow::create(['id' => 'test', 'type' => 'content_moderation']);
|
||||
$this->workflow
|
||||
->addState('draft', 'Draft')
|
||||
->addState('published', 'Published');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getBundlesForEntityType
|
||||
* @covers ::addEntityTypeAndBundle
|
||||
* @covers ::removeEntityTypeAndBundle
|
||||
*/
|
||||
public function testGetBundlesForEntityType() {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
|
||||
$workflow_plugin = $this->workflow->getTypePlugin();
|
||||
// The content moderation plugin does not validate the existence of the
|
||||
// entity type or bundle.
|
||||
$this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
|
||||
$workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
|
||||
$this->assertEquals(['fake_page'], $workflow_plugin->getBundlesForEntityType('fake_node'));
|
||||
$this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_block'));
|
||||
$workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
|
||||
$this->assertEquals([], $workflow_plugin->getBundlesForEntityType('fake_node'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::appliesToEntityTypeAndBundle
|
||||
* @covers ::addEntityTypeAndBundle
|
||||
* @covers ::removeEntityTypeAndBundle
|
||||
*/
|
||||
public function testAppliesToEntityTypeAndBundle() {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
|
||||
$workflow_plugin = $this->workflow->getTypePlugin();
|
||||
// The content moderation plugin does not validate the existence of the
|
||||
// entity type or bundle.
|
||||
$this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
|
||||
$workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
|
||||
$this->assertTrue($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
|
||||
$this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_block', 'fake_custom'));
|
||||
$workflow_plugin->removeEntityTypeAndBundle('fake_node', 'fake_page');
|
||||
$this->assertFalse($workflow_plugin->appliesToEntityTypeAndBundle('fake_node', 'fake_page'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::addEntityTypeAndBundle
|
||||
*/
|
||||
public function testAddEntityTypeAndBundle() {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $workflow_plugin */
|
||||
$workflow_plugin = $this->workflow->getTypePlugin();
|
||||
|
||||
// The bundles are intentionally added in reverse alphabetical order.
|
||||
$workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_page');
|
||||
$workflow_plugin->addEntityTypeAndBundle('fake_node', 'fake_article');
|
||||
|
||||
// Add another entity type that comes alphabetically before 'fake_node'.
|
||||
$workflow_plugin->addEntityTypeAndBundle('fake_block', 'fake_custom');
|
||||
|
||||
// The entity type keys and bundle values should be sorted alphabetically.
|
||||
// The bundle array index should not reflect the order in which they are
|
||||
// added.
|
||||
$this->assertSame(
|
||||
['fake_block' => ['fake_custom'], 'fake_node' => ['fake_article', 'fake_page']],
|
||||
$workflow_plugin->getConfiguration()['entity_types']
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Tests the correct default revision is set.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class DefaultRevisionStateTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'entity_test',
|
||||
'node',
|
||||
'block_content',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
'content_translation',
|
||||
'text',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeManager
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@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('entity_test_rev');
|
||||
$this->installEntitySchema('entity_test_mulrevpub');
|
||||
$this->installEntitySchema('block_content');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$this->entityTypeManager = $this->container->get('entity_type.manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a translatable Node.
|
||||
*/
|
||||
public function testMultilingual() {
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
$this->container->get('content_translation.manager')->setEnabled('node', 'example', TRUE);
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$english_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
// Revision 1 (en).
|
||||
$english_node
|
||||
->setUnpublished()
|
||||
->save();
|
||||
$this->assertEquals('draft', $english_node->moderation_state->value);
|
||||
$this->assertFalse($english_node->isPublished());
|
||||
$this->assertTrue($english_node->isDefaultRevision());
|
||||
|
||||
// Revision 2 (fr)
|
||||
$french_node = $english_node->addTranslation('fr', ['title' => 'French title']);
|
||||
$french_node->moderation_state->value = 'published';
|
||||
$french_node->save();
|
||||
$this->assertTrue($french_node->isPublished());
|
||||
$this->assertTrue($french_node->isDefaultRevision());
|
||||
|
||||
// Revision 3 (fr)
|
||||
$node = Node::load($english_node->id())->getTranslation('fr');
|
||||
$node->moderation_state->value = 'draft';
|
||||
$node->save();
|
||||
$this->assertFalse($node->isPublished());
|
||||
$this->assertFalse($node->isDefaultRevision());
|
||||
|
||||
// Revision 4 (en)
|
||||
$latest_revision = $this->entityTypeManager->getStorage('node')->loadRevision(3);
|
||||
$latest_revision->moderation_state->value = 'draft';
|
||||
$latest_revision->save();
|
||||
$this->assertFalse($latest_revision->isPublished());
|
||||
$this->assertFalse($latest_revision->isDefaultRevision());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\EntityOperations
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityOperationsTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation',
|
||||
'node',
|
||||
'user',
|
||||
'system',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@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->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
|
||||
$workflow->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->value = '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->value = '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->value = '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->value = '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->value = '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->value = '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() {
|
||||
$page = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => $this->randomString(),
|
||||
]);
|
||||
|
||||
$page->moderation_state->value = 'published';
|
||||
$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->value = 'archived';
|
||||
$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,101 @@
|
||||
<?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;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter
|
||||
* @group content_moderation
|
||||
*/
|
||||
class EntityRevisionConverterTest extends KernelTestBase {
|
||||
|
||||
public static $modules = [
|
||||
'user',
|
||||
'entity_test',
|
||||
'system',
|
||||
'content_moderation',
|
||||
'node',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@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() {
|
||||
$this->installConfig(['content_moderation']);
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'article',
|
||||
]);
|
||||
$node_type->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
|
||||
$workflow->save();
|
||||
|
||||
$revision_ids = [];
|
||||
$node = Node::create([
|
||||
'title' => 'test',
|
||||
'type' => 'article',
|
||||
]);
|
||||
$node->moderation_state->value = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision_ids[] = $node->getRevisionId();
|
||||
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
$revision_ids[] = $node->getRevisionId();
|
||||
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->moderation_state->value = 'draft';
|
||||
$node->save();
|
||||
$revision_ids[] = $node->getRevisionId();
|
||||
|
||||
/** @var \Symfony\Component\Routing\RouterInterface $router */
|
||||
$router = \Drupal::service('router.no_access_checks');
|
||||
$result = $router->match('/node/' . $node->id() . '/edit');
|
||||
|
||||
$this->assertInstanceOf(Node::class, $result['node']);
|
||||
$this->assertEquals($revision_ids[2], $result['node']->getRevisionId());
|
||||
$this->assertFalse($result['node']->isDefaultRevision());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?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;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* @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',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@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->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->moderation_state->value = 'draft';
|
||||
$node->save();
|
||||
|
||||
$node->moderation_state->value = 'published';
|
||||
$this->assertCount(0, $node->validate());
|
||||
$node->save();
|
||||
|
||||
$this->assertEquals('published', $node->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test invalid transitions.
|
||||
*
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testInvalidTransition() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
]);
|
||||
$node->moderation_state->value = 'draft';
|
||||
$node->save();
|
||||
|
||||
$node->moderation_state->value = '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.
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->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.
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->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,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\entity_test\Entity\EntityTestRev;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Tests the correct initial states are set on install.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class InitialStateTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'entity_test',
|
||||
'node',
|
||||
'user',
|
||||
'system',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('entity_test_rev');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the correct initial state.
|
||||
*/
|
||||
public function testInitialState() {
|
||||
$node_type = NodeType::create([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
// Test with an entity type that implements EntityPublishedInterface.
|
||||
$unpublished_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Unpublished node',
|
||||
'status' => 0,
|
||||
]);
|
||||
$unpublished_node->save();
|
||||
|
||||
$published_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Published node',
|
||||
'status' => 1,
|
||||
]);
|
||||
$published_node->save();
|
||||
|
||||
// Test with an entity type that doesn't implement EntityPublishedInterface.
|
||||
$entity_test = EntityTestRev::create();
|
||||
$entity_test->save();
|
||||
|
||||
\Drupal::service('module_installer')->install(['content_moderation'], TRUE);
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
|
||||
$loaded_unpublished_node = Node::load($unpublished_node->id());
|
||||
$loaded_published_node = Node::load($published_node->id());
|
||||
$loaded_entity_test = EntityTestRev::load($entity_test->id());
|
||||
$this->assertEquals('draft', $loaded_unpublished_node->moderation_state->value);
|
||||
$this->assertEquals('published', $loaded_published_node->moderation_state->value);
|
||||
$this->assertEquals('draft', $loaded_entity_test->moderation_state->value);
|
||||
|
||||
$presave_node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Presave node',
|
||||
]);
|
||||
$this->assertEquals('draft', $presave_node->moderation_state->value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ModerationStateFieldItemListTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'language',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* @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->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->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->assertFalse($this->testNode->isPublished());
|
||||
$this->assertEquals('draft', $this->testNode->moderation_state[0]->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the field item list when iterating.
|
||||
*/
|
||||
public function testArrayIteration() {
|
||||
$states = [];
|
||||
foreach ($this->testNode->moderation_state as $item) {
|
||||
$states[] = $item->value;
|
||||
}
|
||||
$this->assertEquals(['draft'], $states);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\entity_test\Entity\EntityTestRev;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Test the state field formatter.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class StateFormatterTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
'workflows',
|
||||
'content_moderation',
|
||||
'entity_test',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('entity_test_rev');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the embed field.
|
||||
*
|
||||
* @dataProvider formatterTestCases
|
||||
*/
|
||||
public function testStateFieldFormatter($field_value, $formatter_settings, $expected_output) {
|
||||
$entity = EntityTestRev::create([
|
||||
'moderation_state' => $field_value,
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
$field_output = $this->container->get('renderer')->executeInRenderContext(new RenderContext(), function() use ($entity, $formatter_settings) {
|
||||
return $entity->moderation_state->view($formatter_settings);
|
||||
});
|
||||
|
||||
$this->assertEquals($expected_output, $field_output[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for ::
|
||||
*/
|
||||
public function formatterTestCases() {
|
||||
return [
|
||||
'Draft State' => [
|
||||
'draft',
|
||||
[
|
||||
'type' => 'content_moderation_state',
|
||||
'settings' => [],
|
||||
],
|
||||
[
|
||||
'#markup' => 'Draft',
|
||||
],
|
||||
],
|
||||
'Published State' => [
|
||||
'published',
|
||||
[
|
||||
'type' => 'content_moderation_state',
|
||||
'settings' => [],
|
||||
],
|
||||
[
|
||||
'#markup' => 'Published',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
|
||||
use Drupal\views\Views;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Tests the views integration of content_moderation.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ViewsDataIntegrationTest extends ViewsKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'content_moderation_test_views',
|
||||
'node',
|
||||
'content_moderation',
|
||||
'workflows',
|
||||
'entity_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp($import_test_views = TRUE) {
|
||||
parent::setUp($import_test_views);
|
||||
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('entity_test_mulrevpub');
|
||||
$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->save();
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
|
||||
$workflow->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->value = 'published';
|
||||
$node->save();
|
||||
|
||||
// Create a totally unrelated entity to ensure the extra join information
|
||||
// joins by the correct entity type.
|
||||
$unrelated_entity = EntityTestMulRevPub::create([
|
||||
'id' => $node->id(),
|
||||
]);
|
||||
$unrelated_entity->save();
|
||||
|
||||
$this->assertEquals($unrelated_entity->id(), $node->id());
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->value = '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->value = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->value = 'draft';
|
||||
$revision->save();
|
||||
|
||||
$view = Views::getView('test_content_moderation_revision_test');
|
||||
$view->execute();
|
||||
|
||||
$expected_result = [
|
||||
[
|
||||
'vid' => $node->getRevisionId(),
|
||||
'moderation_state' => 'published',
|
||||
],
|
||||
[
|
||||
'vid' => $revision->getRevisionId(),
|
||||
'moderation_state' => 'draft',
|
||||
],
|
||||
];
|
||||
$this->assertIdenticalResultset($view, $expected_result, ['vid' => 'vid', '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->value = 'published';
|
||||
$node->save();
|
||||
|
||||
$revision = clone $node;
|
||||
$revision->setNewRevision(TRUE);
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$revision->title->value = 'Test title second revision';
|
||||
$revision->moderation_state->value = '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,132 @@
|
||||
<?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\Access\AccessResultNeutral;
|
||||
use Drupal\Core\Cache\Context\CacheContextsManager;
|
||||
use Drupal\Core\Routing\RouteMatch;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\content_moderation\Access\LatestRevisionCheck;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\Access\LatestRevisionCheck
|
||||
* @group content_moderation
|
||||
*/
|
||||
class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Initialize Drupal container since the cache context manager is needed.
|
||||
$contexts_manager = $this->prophesize(CacheContextsManager::class);
|
||||
$contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE);
|
||||
$builder = new ContainerBuilder();
|
||||
$builder->set('cache_contexts_manager', $contexts_manager->reveal());
|
||||
\Drupal::setContainer($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 array $account_permissions
|
||||
* An array of permissions the account has.
|
||||
* @param bool $is_owner
|
||||
* Indicates if the user should be the owner of the entity.
|
||||
* @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, array $account_permissions, $is_owner, $result_class) {
|
||||
|
||||
/** @var \Drupal\Core\Session\AccountInterface $account */
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$possible_permissions = [
|
||||
'view latest version',
|
||||
'view any unpublished content',
|
||||
'view own unpublished content',
|
||||
];
|
||||
foreach ($possible_permissions as $permission) {
|
||||
$account->hasPermission($permission)->willReturn(in_array($permission, $account_permissions));
|
||||
}
|
||||
$account->id()->willReturn(42);
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
$entity = $this->prophesize($entity_class);
|
||||
$entity->getCacheContexts()->willReturn([]);
|
||||
$entity->getCacheTags()->willReturn([]);
|
||||
$entity->getCacheMaxAge()->willReturn(0);
|
||||
if (is_subclass_of($entity_class, EntityOwnerInterface::class)) {
|
||||
$entity->getOwnerId()->willReturn($is_owner ? 42 : 3);
|
||||
}
|
||||
|
||||
/** @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(), $account->reveal());
|
||||
|
||||
$this->assertInstanceOf($result_class, $result);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testLastAccessPermissions().
|
||||
*/
|
||||
public function accessSituationProvider() {
|
||||
return [
|
||||
// Node with global permissions and latest version.
|
||||
[Node::class, 'node', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class],
|
||||
// Node with global permissions and no latest version.
|
||||
[Node::class, 'node', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class],
|
||||
// Node with own content permissions and latest version.
|
||||
[Node::class, 'node', TRUE, ['view latest version', 'view own unpublished content'], TRUE, AccessResultAllowed::class],
|
||||
// Node with own content permissions and no latest version.
|
||||
[Node::class, 'node', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class],
|
||||
// Node with own content permissions and latest version, but no perms to
|
||||
// view latest version.
|
||||
[Node::class, 'node', TRUE, ['view own unpublished content'], TRUE, AccessResultNeutral::class],
|
||||
// Node with own content permissions and no latest version, but no perms
|
||||
// to view latest version.
|
||||
[Node::class, 'node', TRUE, ['view own unpublished content'], FALSE, AccessResultNeutral::class],
|
||||
// Block with forward revision, and permissions to view any.
|
||||
[BlockContent::class, 'block_content', TRUE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultAllowed::class],
|
||||
// Block with no forward revision.
|
||||
[BlockContent::class, 'block_content', FALSE, ['view latest version', 'view any unpublished content'], FALSE, AccessResultForbidden::class],
|
||||
// Block with forward revision, but no permission to view any.
|
||||
[BlockContent::class, 'block_content', TRUE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultNeutral::class],
|
||||
// Block with no forward revision.
|
||||
[BlockContent::class, 'block_content', FALSE, ['view latest version', 'view own unpublished content'], FALSE, AccessResultForbidden::class],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\ContentEntityType;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\ModerationInformation;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* @return EntityTypeManagerInterface
|
||||
* The mocked entity type manager.
|
||||
*/
|
||||
protected function getEntityTypeManager() {
|
||||
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
return $entity_type_manager->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up content moderation and entity manager mocking.
|
||||
*
|
||||
* @param string $bundle
|
||||
* The bundle ID.
|
||||
* @param string|null $workflow
|
||||
* The workflow ID. If nul no workflow information is added to the bundle.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
* The mocked entity type manager.
|
||||
*/
|
||||
public function setupModerationBundleInfo($bundle, $workflow = NULL) {
|
||||
$bundle_info_array = [];
|
||||
if ($workflow) {
|
||||
$bundle_info_array['workflow'] = $workflow;
|
||||
}
|
||||
$bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
|
||||
$bundle_info->getBundleInfo("test_entity_type")->willReturn([$bundle => $bundle_info_array]);
|
||||
|
||||
return $bundle_info->reveal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerWorkflow
|
||||
* @covers ::isModeratedEntity
|
||||
*/
|
||||
public function testIsModeratedEntity($workflow, $expected) {
|
||||
$moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
|
||||
|
||||
$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($expected, $moderation_information->isModeratedEntity($entity->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerWorkflow
|
||||
* @covers ::getWorkflowForEntity
|
||||
*/
|
||||
public function testGetWorkflowForEntity($workflow) {
|
||||
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
if ($workflow) {
|
||||
$workflow_entity = $this->prophesize(WorkflowInterface::class)->reveal();
|
||||
$workflow_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$workflow_storage->load('workflow')->willReturn($workflow_entity)->shouldBeCalled();
|
||||
$entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal());
|
||||
}
|
||||
else {
|
||||
$workflow_entity = NULL;
|
||||
}
|
||||
$moderation_information = new ModerationInformation($entity_type_manager->reveal(), $this->setupModerationBundleInfo('test_bundle', $workflow));
|
||||
$entity = $this->prophesize(ContentEntityInterface::class);
|
||||
$entity->getEntityTypeId()->willReturn('test_entity_type');
|
||||
$entity->bundle()->willReturn('test_bundle');
|
||||
|
||||
$this->assertEquals($workflow_entity, $moderation_information->getWorkflowForEntity($entity->reveal()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providerWorkflow
|
||||
* @covers ::shouldModerateEntitiesOfBundle
|
||||
*/
|
||||
public function testShouldModerateEntities($workflow, $expected) {
|
||||
$entity_type = new ContentEntityType([
|
||||
'id' => 'test_entity_type',
|
||||
'bundle_entity_type' => 'entity_test_bundle',
|
||||
'handlers' => ['moderation' => ModerationHandler::class],
|
||||
]);
|
||||
|
||||
$moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
|
||||
|
||||
$this->assertEquals($expected, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for several tests.
|
||||
*/
|
||||
public function providerWorkflow() {
|
||||
return [
|
||||
[NULL, FALSE],
|
||||
['workflow', TRUE],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Unit;
|
||||
|
||||
use Drupal\content_moderation\ModerationInformationInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\content_moderation\StateTransitionValidation;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
use Drupal\workflows\WorkflowTypeInterface;
|
||||
use Drupal\workflows\WorkflowTypeManager;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\content_moderation\StateTransitionValidation
|
||||
* @group content_moderation
|
||||
*/
|
||||
class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* 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 getValidTransitions() is expected to have the.
|
||||
*
|
||||
* @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);
|
||||
|
||||
$entity = $this->prophesize(ContentEntityInterface::class);
|
||||
$entity = $entity->reveal();
|
||||
$entity->moderation_state = new \stdClass();
|
||||
$entity->moderation_state->value = $from_id;
|
||||
|
||||
$validator = new StateTransitionValidation($this->setUpModerationInformation($entity));
|
||||
$has_transition = FALSE;
|
||||
foreach ($validator->getValidTransitions($entity, $user->reveal()) as $transition) {
|
||||
if ($transition->to()->id() === $to_id) {
|
||||
$has_transition = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->assertSame($result, $has_transition);
|
||||
}
|
||||
|
||||
protected function setUpModerationInformation(ContentEntityInterface $entity) {
|
||||
// Create a container so that the plugin manager and workflow type can be
|
||||
// mocked.
|
||||
$container = new ContainerBuilder();
|
||||
$workflow_type = $this->prophesize(WorkflowTypeInterface::class);
|
||||
$workflow_type->decorateState(Argument::any())->willReturnArgument(0);
|
||||
$workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
|
||||
$workflow_manager = $this->prophesize(WorkflowTypeManager::class);
|
||||
$workflow_manager->createInstance('content_moderation', Argument::any())->willReturn($workflow_type->reveal());
|
||||
$container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
|
||||
$workflow = new Workflow(['id' => 'process', 'type' => 'content_moderation'], 'workflow');
|
||||
$workflow
|
||||
->addState('draft', 'draft')
|
||||
->addState('needs_review', 'needs_review')
|
||||
->addState('published', 'published')
|
||||
->addTransition('draft', 'draft', ['draft'], 'draft')
|
||||
->addTransition('review', 'review', ['draft'], 'needs_review')
|
||||
->addTransition('publish', 'publish', ['needs_review', 'published'], 'published');
|
||||
$moderation_info = $this->prophesize(ModerationInformationInterface::class);
|
||||
$moderation_info->getWorkflowForEntity($entity)->willReturn($workflow);
|
||||
return $moderation_info->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 process transition draft', TRUE, TRUE];
|
||||
|
||||
// The user doesn't have the right permission, block it.
|
||||
$ret[] = ['draft', 'draft', 'use process transition draft', FALSE, FALSE];
|
||||
|
||||
// The user has some other permission that doesn't matter.
|
||||
$ret[] = ['draft', 'draft', 'use process transition review', TRUE, FALSE];
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user