Commit 80818954 authored by plach's avatar plach

Issue #2981887 by amateescu, joachim, jibran, chr.fritsch, Manuel Garcia,...

Issue #2981887 by amateescu, joachim, jibran, chr.fritsch, Manuel Garcia, timmillwood, plach, Wim Leers, Gábor Hojtsy, Berdir, jojototh, pameeela, dawehner, catch, Bojhan, Fabianx, Jo Fitzgerald: Add a publishing status to taxonomy terms

(cherry picked from commit 543b3f4e)
parent 80d7332e
......@@ -14,6 +14,7 @@
use Drupal\options\Plugin\Field\FieldType\ListIntegerItem;
use Drupal\path\Plugin\Field\FieldType\PathItem;
use Drupal\Tests\rest\Functional\XmlNormalizationQuirksTrait;
use Drupal\user\StatusItem;
/**
* Trait for EntityResourceTestBase subclasses testing $format='xml'.
......@@ -63,6 +64,9 @@ protected function applyXmlFieldDecodingQuirks(array $normalization) {
for ($i = 0; $i < count($normalization[$field_name]); $i++) {
switch ($field->getItemDefinition()->getClass()) {
case BooleanItem::class:
case StatusItem::class:
// @todo Remove the StatusItem case in
// https://www.drupal.org/project/drupal/issues/2936864.
$value = &$normalization[$field_name][$i]['value'];
$value = $value === TRUE ? '1' : '0';
break;
......
......@@ -8,6 +8,8 @@
use Drupal\comment\CommentInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\node\NodeInterface;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
......@@ -30,7 +32,7 @@ class EntityReferenceSelectionAccessTest extends KernelTestBase {
*
* @var array
*/
public static $modules = ['comment', 'field', 'node', 'system', 'text', 'user'];
public static $modules = ['comment', 'field', 'node', 'system', 'taxonomy', 'text', 'user'];
/**
* {@inheritdoc}
......@@ -43,9 +45,10 @@ protected function setUp() {
$this->installEntitySchema('comment');
$this->installEntitySchema('node');
$this->installEntitySchema('taxonomy_term');
$this->installEntitySchema('user');
$this->installConfig(['comment', 'field', 'node', 'user']);
$this->installConfig(['comment', 'field', 'node', 'taxonomy', 'user']);
// Create the anonymous and the admin users.
$anonymous_user = User::create([
......@@ -534,4 +537,148 @@ public function testCommentHandler() {
$this->assertReferenceable($selection_options, $referenceable_tests, 'Comment handler (comment + node admin)');
}
/**
* Test the term-specific overrides of the selection handler.
*/
public function testTermHandler() {
// Create a 'Tags' vocabulary.
Vocabulary::create([
'name' => 'Tags',
'description' => $this->randomMachineName(),
'vid' => 'tags',
])->save();
$selection_options = [
'target_type' => 'taxonomy_term',
'handler' => 'default',
'target_bundles' => NULL,
];
// Build a set of test data.
$term_values = [
'published1' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published1',
],
'published2' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published2',
],
'unpublished' => [
'vid' => 'tags',
'status' => 0,
'name' => 'Term unpublished',
],
'published3' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published3',
'parent' => 'unpublished',
],
'published4' => [
'vid' => 'tags',
'status' => 1,
'name' => 'Term published4',
'parent' => 'published3',
],
];
$terms = [];
$term_labels = [];
foreach ($term_values as $key => $values) {
$term = Term::create($values);
if (isset($values['parent'])) {
$term->parent->entity = $terms[$values['parent']];
}
$term->save();
$terms[$key] = $term;
$term_labels[$key] = Html::escape($term->label());
}
// Test as a non-admin.
$normal_user = $this->createUser(['access content']);
$this->setCurrentUser($normal_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
$terms['published2']->id() => $term_labels['published2'],
],
],
],
[
'arguments' => [
['published1', 'CONTAINS'],
['Published1', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
],
],
],
[
'arguments' => [
['published2', 'CONTAINS'],
['Published2', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published2']->id() => $term_labels['published2'],
],
],
],
[
'arguments' => [
['invalid term', 'CONTAINS'],
],
'result' => [],
],
[
'arguments' => [
['Term unpublished', 'CONTAINS'],
],
'result' => [],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Term handler');
// Test as an admin.
$admin_user = $this->createUser(['access content', 'administer taxonomy']);
$this->setCurrentUser($admin_user);
$referenceable_tests = [
[
'arguments' => [
[NULL, 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['published1']->id() => $term_labels['published1'],
$terms['published2']->id() => $term_labels['published2'],
$terms['unpublished']->id() => $term_labels['unpublished'],
$terms['published3']->id() => '-' . $term_labels['published3'],
$terms['published4']->id() => '--' . $term_labels['published4'],
],
],
],
[
'arguments' => [
['Term unpublished', 'CONTAINS'],
],
'result' => [
'tags' => [
$terms['unpublished']->id() => $term_labels['unpublished'],
],
],
],
];
$this->assertReferenceable($selection_options, $referenceable_tests, 'Term handler (admin)');
}
}
......@@ -4,10 +4,12 @@
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\taxonomy\TermInterface;
use Drupal\user\StatusItem;
/**
* Defines the taxonomy term entity.
......@@ -45,7 +47,8 @@
* "bundle" = "vid",
* "label" = "name",
* "langcode" = "langcode",
* "uuid" = "uuid"
* "uuid" = "uuid",
* "published" = "status",
* },
* bundle_entity_type = "taxonomy_vocabulary",
* field_ui_base_route = "entity.taxonomy_vocabulary.overview_form",
......@@ -62,6 +65,7 @@
class Term extends ContentEntityBase implements TermInterface {
use EntityChangedTrait;
use EntityPublishedTrait;
/**
* {@inheritdoc}
......@@ -116,6 +120,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
// Add the published field.
$fields += static::publishedBaseFieldDefinitions($entity_type);
// @todo Remove the usage of StatusItem in
// https://www.drupal.org/project/drupal/issues/2936864.
$fields['status']->getItemDefinition()->setClass(StatusItem::class);
$fields['tid']->setLabel(t('Term ID'))
->setDescription(t('The term ID.'));
......
......@@ -59,10 +59,17 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA
$bundles = $this->entityManager->getBundleInfo('taxonomy_term');
$bundle_names = $this->getConfiguration()['target_bundles'] ?: array_keys($bundles);
$has_admin_access = $this->currentUser->hasPermission('administer taxonomy');
$unpublished_terms = [];
foreach ($bundle_names as $bundle) {
if ($vocabulary = Vocabulary::load($bundle)) {
/** @var \Drupal\taxonomy\TermInterface[] $terms */
if ($terms = $this->entityManager->getStorage('taxonomy_term')->loadTree($vocabulary->id(), 0, NULL, TRUE)) {
foreach ($terms as $term) {
if (!$has_admin_access && (!$term->isPublished() || in_array($term->parent->target_id, $unpublished_terms))) {
$unpublished_terms[] = $term->id();
continue;
}
$options[$vocabulary->id()][$term->id()] = str_repeat('-', $term->depth) . Html::escape($this->entityManager->getTranslationFromContext($term)->label());
}
}
......@@ -72,4 +79,63 @@ public function getReferenceableEntities($match = NULL, $match_operator = 'CONTA
return $options;
}
/**
* {@inheritdoc}
*/
public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
if ($match) {
return parent::countReferenceableEntities($match, $match_operator);
}
$total = 0;
$referenceable_entities = $this->getReferenceableEntities($match, $match_operator, 0);
foreach ($referenceable_entities as $bundle => $entities) {
$total += count($entities);
}
return $total;
}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
// Adding the 'taxonomy_term_access' tag is sadly insufficient for terms:
// core requires us to also know about the concept of 'published' and
// 'unpublished'.
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$query->condition('status', 1);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
$term = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
// In order to create a referenceable term, it needs to published.
/** @var \Drupal\taxonomy\TermInterface $term */
$term->setPublished();
return $term;
}
/**
* {@inheritdoc}
*/
public function validateReferenceableNewEntities(array $entities) {
$entities = parent::validateReferenceableNewEntities($entities);
// Mirror the conditions checked in buildEntityQuery().
if (!$this->currentUser->hasPermission('administer taxonomy')) {
$entities = array_filter($entities, function ($term) {
/** @var \Drupal\taxonomy\TermInterface $term */
return $term->isPublished();
});
}
return $entities;
}
}
......@@ -18,19 +18,37 @@ class TermAccessControlHandler extends EntityAccessControlHandler {
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission('administer taxonomy')) {
return AccessResult::allowed()->cachePerPermissions();
}
switch ($operation) {
case 'view':
return AccessResult::allowedIfHasPermission($account, 'access content');
$access_result = AccessResult::allowedIf($account->hasPermission('access content') && $entity->isPublished())
->cachePerPermissions()
->addCacheableDependency($entity);
if (!$access_result->isAllowed()) {
$access_result->setReason("The 'access content' permission is required and the taxonomy term must be published.");
}
return $access_result;
case 'update':
return AccessResult::allowedIfHasPermissions($account, ["edit terms in {$entity->bundle()}", 'administer taxonomy'], 'OR');
if ($account->hasPermission("edit terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'edit terms in {$entity->bundle()}' OR 'administer taxonomy'.");
case 'delete':
return AccessResult::allowedIfHasPermissions($account, ["delete terms in {$entity->bundle()}", 'administer taxonomy'], 'OR');
if ($account->hasPermission("delete terms in {$entity->bundle()}")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms in {$entity->bundle()}' OR 'administer taxonomy'.");
default:
// No opinion.
return AccessResult::neutral();
return AccessResult::neutral()->cachePerPermissions();
}
}
......
......@@ -4,11 +4,12 @@
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
/**
* Provides an interface defining a taxonomy term entity.
*/
interface TermInterface extends ContentEntityInterface, EntityChangedInterface {
interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface {
/**
* Gets the term's description.
......
......@@ -5,6 +5,8 @@
* Install, update and uninstall functions for the taxonomy module.
*/
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Convert the custom taxonomy term hierarchy storage to a default storage.
*/
......@@ -126,3 +128,50 @@ function taxonomy_update_8503() {
}
}
}
/**
* Add the publishing status fields to taxonomy terms.
*/
function taxonomy_update_8601() {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $definition_update_manager->getEntityType('taxonomy_term');
// Bail out early if a field named 'status' is already installed.
if ($definition_update_manager->getFieldStorageDefinition('status', 'taxonomy_term')) {
return t('The publishing status field has <strong>not</strong> been added to taxonomy terms. See <a href=":link">this page</a> for more information on how to install it.', [
':link' => 'https://www.drupal.org/node/2985366',
]);
}
// Add the 'published' entity key to the taxonomy_term entity type.
$entity_keys = $entity_type->getKeys();
$entity_keys['published'] = 'status';
$entity_type->set('entity_keys', $entity_keys);
$definition_update_manager->updateEntityType($entity_type);
// Add the status field.
$status = BaseFieldDefinition::create('boolean')
->setLabel(t('Publishing status'))
->setDescription(t('A boolean indicating the published state.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setDefaultValue(TRUE);
$has_content_translation_status_field = \Drupal::moduleHandler()->moduleExists('content_translation') && $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'taxonomy_term');
if ($has_content_translation_status_field) {
$status->setInitialValueFromField('content_translation_status', TRUE);
}
else {
$status->setInitialValue(TRUE);
}
$definition_update_manager->installFieldStorageDefinition('status', 'taxonomy_term', 'taxonomy_term', $status);
// Uninstall the 'content_translation_status' field if needed.
if ($has_content_translation_status_field) {
$content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'taxonomy_term');
$definition_update_manager->uninstallFieldStorageDefinition($content_translation_status);
}
return t('The publishing status field has been added to taxonomy terms.');
}
......@@ -5,6 +5,9 @@
* Post update functions for Taxonomy.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\views\ViewExecutable;
/**
* Clear caches due to updated taxonomy entity views data.
*/
......@@ -18,3 +21,107 @@ function taxonomy_post_update_clear_views_data_cache() {
function taxonomy_post_update_clear_entity_bundle_field_definitions_cache() {
// An empty update will flush caches.
}
/**
* Add a 'published' = TRUE filter for all Taxonomy term views and converts
* existing ones that were using the 'content_translation_status' field.
*/
function taxonomy_post_update_handle_publishing_status_addition_in_views(&$sandbox = NULL) {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
$entity_type = $definition_update_manager->getEntityType('taxonomy_term');
$published_key = $entity_type->getKey('published');
$status_filter = [
'id' => 'status',
'table' => 'taxonomy_term_field_data',
'field' => $published_key,
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'operator' => '=',
'value' => '1',
'group' => 1,
'exposed' => FALSE,
'expose' => [
'operator_id' => '',
'label' => '',
'description' => '',
'use_operator' => FALSE,
'operator' => '',
'identifier' => '',
'required' => FALSE,
'remember' => FALSE,
'multiple' => FALSE,
'remember_roles' => [
'authenticated' => 'authenticated',
'anonymous' => '0',
'administrator' => '0',
],
],
'is_grouped' => FALSE,
'group_info' => [
'label' => '',
'description' => '',
'identifier' => '',
'optional' => TRUE,
'widget' => 'select',
'multiple' => FALSE,
'remember' => FALSE,
'default_group' => 'All',
'default_group_multiple' => [],
'group_items' => [],
],
'entity_type' => 'taxonomy_term',
'entity_field' => $published_key,
'plugin_id' => 'boolean',
];
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function ($view) use ($published_key, $status_filter) {
/** @var \Drupal\views\ViewEntityInterface $view */
// Only alter taxonomy term views.
if ($view->get('base_table') !== 'taxonomy_term_field_data') {
return FALSE;
}
$displays = $view->get('display');
foreach ($displays as $display_name => &$display) {
// Update any existing 'content_translation_status fields.
$fields = isset($display['display_options']['fields']) ? $display['display_options']['fields'] : [];
foreach ($fields as $id => $field) {
if (isset($field['field']) && $field['field'] == 'content_translation_status') {
$fields[$id]['field'] = $published_key;
}
}
$display['display_options']['fields'] = $fields;
// Update any existing 'content_translation_status sorts.
$sorts = isset($display['display_options']['sorts']) ? $display['display_options']['sorts'] : [];
foreach ($sorts as $id => $sort) {
if (isset($sort['field']) && $sort['field'] == 'content_translation_status') {
$sorts[$id]['field'] = $published_key;
}
}
$display['display_options']['sorts'] = $sorts;
// Update any existing 'content_translation_status' filters or add a new
// one if necessary.
$filters = isset($display['display_options']['filters']) ? $display['display_options']['filters'] : [];
$has_status_filter = FALSE;
foreach ($filters as $id => $filter) {
if (isset($filter['field']) && $filter['field'] == 'content_translation_status') {
$filters[$id]['field'] = $published_key;
$has_status_filter = TRUE;
}
}
if (!$has_status_filter) {
$status_filter['id'] = ViewExecutable::generateHandlerId($published_key, $filters);
$filters[$status_filter['id']] = $status_filter;
}
$display['display_options']['filters'] = $filters;
}
$view->set('display', $displays);
return TRUE;
});
}
<?php
/**
* @file
* Contains database additions to drupal-8.filled.standard.php.gz for testing
* the upgrade path of https://www.drupal.org/project/drupal/issues/2981887.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$view_file = __DIR__ . '/views.view.test_taxonomy_term_view_with_content_translation_status.yml';
$view_with_cts_config = Yaml::decode(file_get_contents($view_file));
$view_file = __DIR__ . '/views.view.test_taxonomy_term_view_without_content_translation_status.yml';
$view_without_cts_config = Yaml::decode(file_get_contents($view_file));
$connection->insert('config')
->fields(['collection', 'name', 'data'])
->values([
'collection' => '',
'name' => 'views.view.test_taxonomy_term_view_with_content_translation_status',
'data' => serialize($view_with_cts_config),
])
->values([
'collection' => '',
'name' => 'views.view.test_taxonomy_term_view_without_content_translation_status',
'data' => serialize($view_without_cts_config),
])
->execute();
langcode: en
status: true
dependencies:
module:
- taxonomy
- user
id: test_taxonomy_term_view_with_content_translation_status
label: 'Test taxonomy term view with content translation status'
module: views
description: ''
tag: ''
base_table: taxonomy_term_field_data
base_field: tid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: none
options:
offset: 0
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
id: name
table: taxonomy_term_field_data
field: name
entity_type: taxonomy_term
entity_field: name
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
type: string
settings:
link_to_entity: true
plugin_id: term_name
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''