diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php index 279d4b4e6f..22f98a79d3 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -33,6 +33,7 @@ class ContentModerationStateTest extends KernelTestBase { 'media_test_source', 'image', 'file', + 'taxonomy', 'field', 'content_moderation', 'user', @@ -63,6 +64,7 @@ protected function setUp() { $this->installEntitySchema('block_content'); $this->installEntitySchema('media'); $this->installEntitySchema('file'); + $this->installEntitySchema('taxonomy_term'); $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); $this->installSchema('file', 'file_usage'); @@ -161,6 +163,9 @@ public function basicModerationTestCases() { 'Media' => [ 'media', ], + 'Taxonomy term' => [ + 'taxonomy_term', + ], 'Test entity - revisions, data table, and published interface' => [ 'entity_test_mulrevpub', ], @@ -249,7 +254,7 @@ public function testContentModerationStateTranslationDataRemoval($entity_type_id ConfigurableLanguage::createFromLangcode($langcode) ->save(); $entity->save(); - $translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']); + $translation = $entity->addTranslation($langcode, [$entity->getEntityType()->getKey('label') => 'Titolo test']); // Make sure we add values for all of the required fields. if ($entity_type_id == 'block_content') { $translation->info = $this->randomString(); diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 51e295a517..10d729f2ff 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -2,8 +2,7 @@ namespace Drupal\taxonomy\Entity; -use Drupal\Core\Entity\ContentEntityBase; -use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -38,14 +37,23 @@ * }, * base_table = "taxonomy_term_data", * data_table = "taxonomy_term_field_data", + * revision_table = "taxonomy_term_revision", + * revision_data_table = "taxonomy_term_field_revision", * uri_callback = "taxonomy_term_uri", * translatable = TRUE, * entity_keys = { * "id" = "tid", + * "revision" = "revision_id", * "bundle" = "vid", * "label" = "name", * "langcode" = "langcode", - * "uuid" = "uuid" + * "uuid" = "uuid", + * "published" = "status", + * }, + * revision_metadata_keys = { + * "revision_user" = "revision_user", + * "revision_created" = "revision_created", + * "revision_log_message" = "revision_log_message", * }, * bundle_entity_type = "taxonomy_vocabulary", * field_ui_base_route = "entity.taxonomy_vocabulary.overview_form", @@ -56,12 +64,13 @@ * "edit-form" = "/taxonomy/term/{taxonomy_term}/edit", * "create" = "/taxonomy/term", * }, - * permission_granularity = "bundle" + * permission_granularity = "bundle", + * constraints = { + * "TaxonomyHierarchy" = {} + * } * ) */ -class Term extends ContentEntityBase implements TermInterface { - - use EntityChangedTrait; +class Term extends EditorialContentEntityBase implements TermInterface { /** * {@inheritdoc} @@ -129,6 +138,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['name'] = BaseFieldDefinition::create('string') ->setLabel(t('Name')) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setRequired(TRUE) ->setSetting('max_length', 255) ->setDisplayOptions('view', [ @@ -145,6 +155,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['description'] = BaseFieldDefinition::create('text_long') ->setLabel(t('Description')) ->setTranslatable(TRUE) + ->setRevisionable(TRUE) ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'text_default', @@ -171,7 +182,14 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) ->setDescription(t('The time that the term was last edited.')) - ->setTranslatable(TRUE); + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + // @todo Keep this field hidden until we have a revision UI for terms. + // @see https://www.drupal.org/project/drupal/issues/2936995 + $fields['revision_log_message']->setDisplayOptions('form', [ + 'region' => 'hidden', + ]); return $fields; } diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php index f9e0d59598..0cbb5ce830 100644 --- a/core/modules/taxonomy/src/Form/OverviewTerms.php +++ b/core/modules/taxonomy/src/Form/OverviewTerms.php @@ -2,6 +2,7 @@ namespace Drupal\taxonomy\Form; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormBase; @@ -227,6 +228,56 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular } } + $args = [ + '%capital_name' => Unicode::ucfirst($taxonomy_vocabulary->label()), + '%name' => $taxonomy_vocabulary->label(), + ]; + if ($this->currentUser()->hasPermission('administer taxonomy') || $this->currentUser()->hasPermission('edit terms in ' . $taxonomy_vocabulary->id())) { + switch ($taxonomy_vocabulary->getHierarchy()) { + case VocabularyInterface::HIERARCHY_DISABLED: + $help_message = $this->t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', $args); + break; + case VocabularyInterface::HIERARCHY_SINGLE: + $help_message = $this->t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', $args); + break; + case VocabularyInterface::HIERARCHY_MULTIPLE: + $help_message = $this->t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', $args); + break; + } + } + else { + switch ($taxonomy_vocabulary->getHierarchy()) { + case VocabularyInterface::HIERARCHY_DISABLED: + $help_message = $this->t('%capital_name contains the following terms.', $args); + break; + case VocabularyInterface::HIERARCHY_SINGLE: + $help_message = $this->t('%capital_name contains terms grouped under parent terms', $args); + break; + case VocabularyInterface::HIERARCHY_MULTIPLE: + $help_message = $this->t('%capital_name contains terms with multiple parents.', $args); + break; + } + } + + // Get IDs of terms which have pending revisions. + $pending_term_ids = $this->storageController->getTermIDsWithPendingRevisions(); + if ($pending_term_ids) { + $help_message = $this->formatPlural( + count($pending_term_ids), + '%capital_name contains 1 term with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.', + '%capital_name contains @count terms with pending revisions. Drag and drop of terms with pending revisions is not supported, but you can re-enable drag-and-drop support by getting each term to a published state.', + ['%capital_name' => Unicode::ucfirst($taxonomy_vocabulary->label())] + ); + } + + $form['help'] = [ + '#type' => 'container', + 'message' => ['#markup' => $help_message], + ]; + if ($pending_term_ids || $taxonomy_vocabulary->getHierarchy() === VocabularyInterface::HIERARCHY_MULTIPLE) { + $form['help']['#attributes']['class'] = ['messages', 'messages--warning']; + } + $errors = $form_state->getErrors(); $row_position = 0; // Build the actual form. @@ -277,6 +328,15 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular '#title' => $term->getName(), '#url' => $term->urlInfo(), ]; + + // Add a special class for terms with pending revision so we can highlight + // them in the form. + $form['terms'][$key]['#attributes']['class'] = []; + if (in_array($term->id(), $pending_term_ids)) { + $form['terms'][$key]['#attributes']['class'][] = 'color-warning'; + $form['terms'][$key]['#attributes']['class'][] = 'taxonomy-term-pending-revision'; + } + if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) { $parent_fields = TRUE; $form['terms'][$key]['term']['tid'] = [ @@ -326,7 +386,6 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular ]; } - $form['terms'][$key]['#attributes']['class'] = []; if ($parent_fields) { $form['terms'][$key]['#attributes']['class'][] = 'draggable'; } @@ -355,7 +414,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular } $this->renderer->addCacheableDependency($form['terms'], $change_weight_access); - if ($change_weight_access->isAllowed()) { + if (!$pending_term_ids && $change_weight_access->isAllowed()) { if ($parent_fields) { $form['terms']['#tabledrag'][] = [ 'action' => 'match', @@ -384,7 +443,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular ]; } - if (($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) { + if (!$pending_term_ids && ($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) { $form['actions'] = ['#type' => 'actions', '#tree' => FALSE]; $form['actions']['submit'] = [ '#type' => 'submit', diff --git a/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraint.php b/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraint.php new file mode 100644 index 0000000000..93abb07ac8 --- /dev/null +++ b/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraint.php @@ -0,0 +1,31 @@ +published version of this term.'; + + /** + * {@inheritdoc} + */ + public function coversFields() { + return ['parent', 'weight']; + } + +} diff --git a/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraintValidator.php b/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraintValidator.php new file mode 100644 index 0000000000..804ddb832b --- /dev/null +++ b/core/modules/taxonomy/src/Plugin/Validation/Constraint/TaxonomyTermHierarchyConstraintValidator.php @@ -0,0 +1,68 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function validate($entity, Constraint $constraint) { + if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) { + /** @var \Drupal\taxonomy\TermStorageInterface $term_storage */ + $term_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + + $new_parents = array_column($entity->parent->getValue(), 'target_id'); + $original_parents = array_keys($term_storage->loadParents($entity->id())) ?: [0]; + if ($new_parents != $original_parents) { + $this->context->buildViolation($constraint->message) + ->atPath('parent') + ->addViolation(); + } + + /** @var \Drupal\taxonomy\TermInterface $original */ + $original = $term_storage->loadUnchanged($entity->id()); + if (!$entity->weight->equals($original->weight)) { + $this->context->buildViolation($constraint->message) + ->atPath('weight') + ->addViolation(); + } + } + } + +} diff --git a/core/modules/taxonomy/src/TermForm.php b/core/modules/taxonomy/src/TermForm.php index 95909bd3b1..d00d197cb7 100644 --- a/core/modules/taxonomy/src/TermForm.php +++ b/core/modules/taxonomy/src/TermForm.php @@ -3,6 +3,7 @@ namespace Drupal\taxonomy; use Drupal\Core\Entity\ContentEntityForm; +use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Form\FormStateInterface; /** @@ -120,6 +121,29 @@ public function buildEntity(array $form, FormStateInterface $form_state) { return $term; } + /** + * {@inheritdoc} + */ + protected function getEditedFieldNames(FormStateInterface $form_state) { + return array_merge(['parent', 'weight'], parent::getEditedFieldNames($form_state)); + } + + /** + * {@inheritdoc} + */ + protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { + // Manually flag violations of fields not handled by the form display. + // @see ::form() + foreach ($violations->getByField('parent') as $violation) { + $form_state->setErrorByName('parent', $violation->getMessage()); + } + foreach ($violations->getByField('weight') as $violation) { + $form_state->setErrorByName('weight', $violation->getMessage()); + } + + parent::flagViolations($violations, $form, $form_state); + } + /** * {@inheritdoc} */ diff --git a/core/modules/taxonomy/src/TermInterface.php b/core/modules/taxonomy/src/TermInterface.php index 4cde8f45f6..bbab26c5d6 100644 --- a/core/modules/taxonomy/src/TermInterface.php +++ b/core/modules/taxonomy/src/TermInterface.php @@ -4,11 +4,13 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityPublishedInterface; +use Drupal\Core\Entity\RevisionLogInterface; /** * Provides an interface defining a taxonomy term entity. */ -interface TermInterface extends ContentEntityInterface, EntityChangedInterface { +interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface { /** * Gets the term's description. diff --git a/core/modules/taxonomy/src/TermStorage.php b/core/modules/taxonomy/src/TermStorage.php index f3e8ecb341..6aced7b57e 100644 --- a/core/modules/taxonomy/src/TermStorage.php +++ b/core/modules/taxonomy/src/TermStorage.php @@ -357,6 +357,23 @@ public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL) return $terms; } + /** + * {@inheritdoc} + */ + public function getTermIDsWithPendingRevisions() { + $table_mapping = $this->getTableMapping(); + $id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value']; + $revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value']; + + $query = $this->database->select($this->getDataTable(), 'td'); + $query->innerJoin($this->getRevisionDataTable(), 'tr', "td.$id_field = tr.$id_field"); + $query + ->fields('tr', [$revision_field, $id_field]) + ->where("tr.$revision_field > td.$revision_field"); + + return $query->execute()->fetchAllKeyed(); + } + /** * {@inheritdoc} */ diff --git a/core/modules/taxonomy/src/TermStorageInterface.php b/core/modules/taxonomy/src/TermStorageInterface.php index 4d7b5cc1dd..47dac242f5 100644 --- a/core/modules/taxonomy/src/TermStorageInterface.php +++ b/core/modules/taxonomy/src/TermStorageInterface.php @@ -126,4 +126,15 @@ public function resetWeights($vid); */ public function getNodeTerms(array $nids, array $vocabs = [], $langcode = NULL); + /** + * Gets a list of terms with pending revisions. + * + * @return int[] + * An array of term IDs which have pending revisions, keyed by their + * revision IDs. + * + * @internal + */ + public function getTermIDsWithPendingRevisions(); + } diff --git a/core/modules/taxonomy/src/TermStorageSchema.php b/core/modules/taxonomy/src/TermStorageSchema.php index 2b49ee247b..aa5e1e6b66 100644 --- a/core/modules/taxonomy/src/TermStorageSchema.php +++ b/core/modules/taxonomy/src/TermStorageSchema.php @@ -17,7 +17,7 @@ class TermStorageSchema extends SqlContentEntityStorageSchema { protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { $schema = parent::getEntitySchema($entity_type, $reset = FALSE); - $schema['taxonomy_term_field_data']['indexes'] += [ + $schema[$entity_type->getDataTable()]['indexes'] += [ 'taxonomy_term__tree' => ['vid', 'weight', 'name'], 'taxonomy_term__vid_name' => ['vid', 'name'], ]; diff --git a/core/modules/taxonomy/taxonomy.install b/core/modules/taxonomy/taxonomy.install index c1a18bcb2c..d9307a674e 100644 --- a/core/modules/taxonomy/taxonomy.install +++ b/core/modules/taxonomy/taxonomy.install @@ -5,6 +5,25 @@ * Install, update and uninstall functions for the taxonomy module. */ +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Entity\Sql\SqlContentEntityStorage; + +/** + * Implements hook_update_dependencies(). + */ +function taxonomy_update_dependencies() { + // The update function that adds the status field must run after + // content_translation_update_8400() which fixes NULL values for the + // 'content_translation_status' field. + if (\Drupal::moduleHandler()->moduleExists('content_translation')) { + $dependencies['taxonomy'][8600] = [ + 'content_translation' => 8400, + ]; + + return $dependencies; + } +} + /** * Convert the custom taxonomy term hierarchy storage to a default storage. */ @@ -126,3 +145,95 @@ function taxonomy_update_8503() { } } } + +/** + * Add the 'published' and revisionable metadata fields to taxonomy terms. + */ +function taxonomy_update_8600() { + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + + // Add the published entity key and revisionable metadata fields to the + // taxonomy_term entity type. + $entity_type = $definition_update_manager->getEntityType('taxonomy_term'); + + $entity_keys = $entity_type->getKeys(); + $entity_keys['published'] = 'status'; + $entity_type->set('entity_keys', $entity_keys); + + $revision_metadata_keys = [ + 'revision_user' => 'revision_user', + 'revision_created' => 'revision_created', + 'revision_log_message' => 'revision_log_message', + ]; + $entity_type->set('revision_metadata_keys', $revision_metadata_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); + + // Add the revision metadata fields. + $revision_created = BaseFieldDefinition::create('created') + ->setLabel(t('Revision create time')) + ->setDescription(t('The time that the current revision was created.')) + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_created', 'taxonomy_term', 'taxonomy_term', $revision_created); + + $revision_user = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision user')) + ->setDescription(t('The user ID of the author of the current revision.')) + ->setSetting('target_type', 'user') + ->setRevisionable(TRUE); + $definition_update_manager->installFieldStorageDefinition('revision_user', 'taxonomy_term', 'taxonomy_term', $revision_user); + + $revision_log_message = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision log message')) + ->setDescription(t('Briefly describe the changes you have made.')) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textarea', + 'weight' => 25, + 'region' => 'hidden', + 'settings' => [ + 'rows' => 4, + ], + ]); + $definition_update_manager->installFieldStorageDefinition('revision_log_message', 'taxonomy_term', 'taxonomy_term', $revision_log_message); + + // Uninstall the 'content_translation_status' field if needed. + $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + if ($has_content_translation_status_field &&$storage instanceof SqlContentEntityStorage) { + $database = \Drupal::database(); + // First we have to remove the field data. + $database->update($entity_type->getDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + + // A site may have disabled revisionability for this entity type. + if ($entity_type->isRevisionable()) { + $database->update($entity_type->getRevisionDataTable()) + ->fields(['content_translation_status' => NULL]) + ->execute(); + } + + $content_translation_status = $definition_update_manager->getFieldStorageDefinition('content_translation_status', 'taxonomy_term'); + $definition_update_manager->uninstallFieldStorageDefinition($content_translation_status); + } + + return t('Taxonomy terms have been converted to revisionable and publishable.'); +} diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 179487d3f0..dbbdaba620 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -6,7 +6,6 @@ */ use Drupal\Component\Utility\Tags; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Sql\SqlContentEntityStorage; use Drupal\Core\Render\Element; @@ -78,29 +77,6 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) { case 'entity.taxonomy_vocabulary.collection': $output = '

' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '

'; return $output; - - case 'entity.taxonomy_vocabulary.overview_form': - $vocabulary = $route_match->getParameter('taxonomy_vocabulary'); - if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) { - switch ($vocabulary->getHierarchy()) { - case VocabularyInterface::HIERARCHY_DISABLED: - return '

' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '

'; - case VocabularyInterface::HIERARCHY_SINGLE: - return '

' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '

'; - case VocabularyInterface::HIERARCHY_MULTIPLE: - return '

' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; - } - } - else { - switch ($vocabulary->getHierarchy()) { - case VocabularyInterface::HIERARCHY_DISABLED: - return '

' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; - case VocabularyInterface::HIERARCHY_SINGLE: - return '

' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; - case VocabularyInterface::HIERARCHY_MULTIPLE: - return '

' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; - } - } } } diff --git a/core/modules/taxonomy/taxonomy.post_update.php b/core/modules/taxonomy/taxonomy.post_update.php new file mode 100644 index 0000000000..8ecda9adef --- /dev/null +++ b/core/modules/taxonomy/taxonomy.post_update.php @@ -0,0 +1,106 @@ +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) { + // Add a filter on the published field for the default and all the + // overridden displays. + $filters = isset($display['display_options']['filters']) ? $display['display_options']['filters'] : []; + $status_filter['id'] = ViewExecutable::generateHandlerId($published_key, $filters); + + $display['display_options']['filters'][$status_filter['id']] = $status_filter; + } + return TRUE; + }); +} + +/** + * Update taxonomy terms to be revisionable. + */ +function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) { + $schema_converter = new SqlContentEntityStorageSchemaConverter( + 'taxonomy_term', + \Drupal::entityTypeManager(), + \Drupal::entityDefinitionUpdateManager(), + \Drupal::service('entity.last_installed_schema.repository'), + \Drupal::keyValue('entity.storage_schema.sql'), + \Drupal::database() + ); + + $schema_converter->convertToRevisionable( + $sandbox, + [ + 'name', + 'description', + 'changed', + ] + ); +} diff --git a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php index 7e5144e513..6ff8304346 100644 --- a/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php +++ b/core/modules/taxonomy/tests/src/Functional/Rest/TermResourceTestBase.php @@ -156,6 +156,9 @@ protected function getExpectedNormalizedEntity() { 'tid' => [ ['value' => 1], ], + 'revision_id' => [ + ['value' => 1], + ], 'uuid' => [ ['value' => $this->entity->uuid()], ], @@ -200,6 +203,21 @@ protected function getExpectedNormalizedEntity() { 'langcode' => 'en', ], ], + 'status' => [ + [ + 'value' => TRUE, + ], + ], + 'revision_created' => [ + $this->formatExpectedTimestampItemValues((int) $this->entity->getRevisionCreationTime()), + ], + 'revision_user' => [], + 'revision_log_message' => [], + 'revision_translation_affected' => [ + [ + 'value' => TRUE, + ], + ], ]; } diff --git a/core/modules/taxonomy/tests/src/Functional/TaxonomyTermContentModerationTest.php b/core/modules/taxonomy/tests/src/Functional/TaxonomyTermContentModerationTest.php new file mode 100644 index 0000000000..ce5e885c83 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/TaxonomyTermContentModerationTest.php @@ -0,0 +1,152 @@ +drupalLogin($this->drupalCreateUser(['administer taxonomy', 'use editorial transition create_new_draft', 'use editorial transition publish'])); + + // Create a vocabulary. + $this->vocabulary = $this->createVocabulary(); + + // Set the vocabulary as moderated. + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('taxonomy_term', $this->vocabulary->id()); + $workflow->save(); + } + + /** + * Tests taxonomy term parents on a moderated vocabulary. + */ + public function testTaxonomyTermParents() { + // Create a simple hierarchy in the vocabulary, a root term and three parent + // terms. + $root = $this->createTerm($this->vocabulary, ['langcode' => 'en', 'moderation_state' => 'published']); + $parent_1 = $this->createTerm($this->vocabulary, ['langcode' => 'en', 'moderation_state' => 'published', 'parent' => $root->id()]); + $parent_2 = $this->createTerm($this->vocabulary, ['langcode' => 'en', 'moderation_state' => 'published', 'parent' => $root->id()]); + $parent_3 = $this->createTerm($this->vocabulary, ['langcode' => 'en', 'moderation_state' => 'published', 'parent' => $root->id()]); + + // Create a child term and assign one of the parents above. + $child = $this->createTerm($this->vocabulary, ['langcode' => 'en', 'moderation_state' => 'published', 'parent' => $parent_1->id()]); + + $taxonomy_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + $validation_message = 'You can only change the hierarchy for the published version of this term.'; + + // Add a pending revision without changing the term parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, ['moderation_state[0][state]' => 'draft'], 'Save'); + + $this->assertSession()->pageTextNotContains($validation_message); + + // Add a pending revision and change the parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'parent[]' => [$parent_2->id()], + 'moderation_state[0][state]' => 'draft', + ], 'Save'); + + // Check that parents were not changed. + $this->assertSession()->pageTextContains($validation_message); + $taxonomy_storage->resetCache(); + $this->assertEquals([$parent_1->id()], array_keys($taxonomy_storage->loadParents($child->id()))); + + // Add a pending revision and add a new parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'parent[]' => [$parent_1->id(), $parent_3->id()], + 'moderation_state[0][state]' => 'draft', + ], 'Save'); + + // Check that parents were not changed. + $this->assertSession()->pageTextContains($validation_message); + $taxonomy_storage->resetCache(); + $this->assertEquals([$parent_1->id()], array_keys($taxonomy_storage->loadParents($child->id()))); + + // Add a pending revision and use the root term as a parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'parent[]' => [$root->id()], + 'moderation_state[0][state]' => 'draft', + ], 'Save'); + + // Check that parents were not changed. + $this->assertSession()->pageTextContains($validation_message); + $taxonomy_storage->resetCache(); + $this->assertEquals([$parent_1->id()], array_keys($taxonomy_storage->loadParents($child->id()))); + + // Add a pending revision and remove the parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'parent[]' => [], + 'moderation_state[0][state]' => 'draft', + ], 'Save'); + + // Check that parents were not changed. + $this->assertSession()->pageTextContains($validation_message); + $taxonomy_storage->resetCache(); + $this->assertEquals([$parent_1->id()], array_keys($taxonomy_storage->loadParents($child->id()))); + + // Add a published revision and change the parent. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'parent[]' => [$parent_2->id()], + 'moderation_state[0][state]' => 'published', + ], 'Save'); + + // Check that parents were changed. + $this->assertSession()->pageTextNotContains($validation_message); + $taxonomy_storage->resetCache(); + $this->assertEquals([$parent_2->id()], array_keys($taxonomy_storage->loadParents($child->id()))); + + // Add a pending revision and change the weight. + $this->drupalGet('taxonomy/term/' . $child->id() . '/edit'); + $this->drupalPostForm(NULL, [ + 'weight' => 10, + 'moderation_state[0][state]' => 'draft', + ], 'Save'); + + // Check that weight was not changed. + $this->assertSession()->pageTextContains($validation_message); + + // Add a new term without any parent and publish it. + $edit = [ + 'name[0][value]' => $this->randomMachineName(), + 'moderation_state[0][state]' => 'published', + ]; + $this->drupalPostForm("admin/structure/taxonomy/manage/{$this->vocabulary->id()}/add", $edit, 'Save'); + // Add a pending revision without any changes. + $terms = taxonomy_term_load_multiple_by_name($edit['name[0][value]']); + $term = reset($terms); + $this->drupalPostForm('taxonomy/term/' . $term->id() . '/edit', ['moderation_state[0][state]' => 'draft'], 'Save'); + $this->assertSession()->pageTextNotContains($validation_message); + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableUpdateTest.php b/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableUpdateTest.php new file mode 100644 index 0000000000..1b1cbe5419 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableUpdateTest.php @@ -0,0 +1,83 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz', + ]; + } + + /** + * Tests the conversion of taxonomy terms to be revisionable and publishable. + * + * @see taxonomy_update_8600() + * @see taxonomy_post_update_make_taxonomy_term_revisionable() + */ + public function testConversionToRevisionableAndPublishable() { + $this->runUpdates(); + + // Log in as user 1. + $account = User::load(1); + $account->passRaw = 'drupal'; + $this->drupalLogin($account); + + // Make sure our vocabulary exists. + $this->drupalGet('admin/structure/taxonomy/manage/test_vocabulary/overview'); + + // Make sure our terms exist. + $assert_session = $this->assertSession(); + $assert_session->pageTextContains('Test root term'); + $assert_session->pageTextContains('Test child term'); + + $this->drupalGet('taxonomy/term/3'); + $assert_session->statusCodeEquals('200'); + + // Make sure the terms are still translated. + $this->drupalGet('taxonomy/term/2/translations'); + $assert_session->linkExists('Test root term - Spanish'); + + // Check that taxonomy terms can be created, saved and then loaded. + $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term'); + /** @var \Drupal\taxonomy\TermInterface $term */ + $term = $storage->create([ + 'name' => 'Test term', + 'vid' => 'article', + ]); + $term->save(); + + $storage->resetCache(); + $term = $storage->loadRevision($term->getRevisionId()); + + $this->assertEquals('Test term', $term->label()); + $this->assertEquals('article', $term->bundle()); + $this->assertTrue($term->isPublished()); + + // Tests the revision log message field has been set to hidden. + $definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('taxonomy_term'); + $form_display_options = $definitions['revision_log_message']->getDisplayOptions('form'); + $this->assertEquals('hidden', $form_display_options['region']); + } + + /** + * {@inheritdoc} + */ + protected function replaceUser1() { + // Do not replace the user from our dump. + } + +} diff --git a/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableViewsUpdateTest.php b/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableViewsUpdateTest.php new file mode 100644 index 0000000000..ef0acfa288 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/Update/TermRevisionablePublishableViewsUpdateTest.php @@ -0,0 +1,56 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + ]; + } + + /** + * Tests the conversion of taxonomy terms to be revisionable and publishable. + * + * @see taxonomy_post_update_add_status_filter_to_taxonomy_views() + */ + public function testAddPublishedFilterToTaxonomyTermFieldDataViews() { + $this->runUpdates(); + + $views = Views::getAllViews(); + foreach ($views as $view) { + // Only check taxonomy term views. + if ($view->get('base_table') !== 'taxonomy_term_field_data') { + continue; + } + + foreach ($view->display as $display) { + // Assert that a published filter was added. + if (!empty($display['display_options']['filters'])) { + $this->assertArrayHasKey('status', $display['display_options']['filters']); + } + } + } + } + +}