Issue #2880149 by amateescu, timmillwood, Manuel Garcia, Jo Fitzgerald, fidodido06, plach, blazey, hchonov, benjifisher, lbodiguel, vpeltot, larowlan, dixon_, jojototh, Wim Leers, catch, jibran, Berdir, mikelutz, pameeela, webchick, Bojhan: Convert taxonomy terms to be revisionable

merge-requests/1119/head
Nathaniel Catchpole 2019-03-11 19:56:03 +00:00
parent 2af907ca36
commit 5ce9bcd9f6
13 changed files with 668 additions and 54 deletions

View File

@ -2,9 +2,7 @@
namespace Drupal\taxonomy\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@ -40,16 +38,24 @@ use Drupal\user\StatusItem;
* },
* 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",
* "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",
* common_reference_target = TRUE,
@ -59,13 +65,13 @@ use Drupal\user\StatusItem;
* "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;
use EntityPublishedTrait;
class Term extends EditorialContentEntityBase implements TermInterface {
/**
* {@inheritdoc}
@ -120,8 +126,6 @@ class Term extends ContentEntityBase implements TermInterface {
/** @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);
@ -139,6 +143,7 @@ class Term extends ContentEntityBase implements TermInterface {
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel(t('Name'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('view', [
@ -155,6 +160,7 @@ class Term extends ContentEntityBase implements TermInterface {
$fields['description'] = BaseFieldDefinition::create('text_long')
->setLabel(t('Description'))
->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'text_default',
@ -181,7 +187,14 @@ class Term extends ContentEntityBase implements TermInterface {
$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;
}

View File

@ -2,6 +2,7 @@
namespace Drupal\taxonomy\Form;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait;
use Drupal\Core\Entity\EntityRepositoryInterface;
@ -251,6 +252,64 @@ class OverviewTerms extends FormBase {
}
}
$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 ($vocabulary_hierarchy) {
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 ($vocabulary_hierarchy) {
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 the IDs of the terms edited on the current page which have pending
// revisions.
$edited_term_ids = array_map(function ($item) {
return $item->id();
}, $current_page);
$pending_term_ids = array_intersect($this->storageController->getTermIdsWithPendingRevisions(), $edited_term_ids);
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.',
$args
);
}
// Only allow access to change parents and reorder the tree if there are no
// pending revisions and there are no terms with multiple parents.
$update_tree_access = AccessResult::allowedIf(empty($pending_term_ids) && $vocabulary_hierarchy !== VocabularyInterface::HIERARCHY_MULTIPLE);
$form['help'] = [
'#type' => 'container',
'message' => ['#markup' => $help_message],
];
if (!$update_tree_access->isAllowed()) {
$form['help']['#attributes']['class'] = ['messages', 'messages--warning'];
}
$errors = $form_state->getErrors();
$row_position = 0;
// Build the actual form.
@ -268,7 +327,7 @@ class OverviewTerms extends FormBase {
'#header' => [
'term' => $this->t('Name'),
'operations' => $this->t('Operations'),
'weight' => $this->t('Weight'),
'weight' => $update_tree_access->isAllowed() ? $this->t('Weight') : NULL,
],
'#attributes' => [
'id' => 'taxonomy',
@ -276,14 +335,11 @@ class OverviewTerms extends FormBase {
];
$this->renderer->addCacheableDependency($form['terms'], $create_access);
// Only allow access to changing weights if the user has update access for
// all terms.
$change_weight_access = AccessResult::allowed();
foreach ($current_page as $key => $term) {
$form['terms'][$key] = [
'term' => [],
'operations' => [],
'weight' => [],
'weight' => $update_tree_access->isAllowed() ? [] : NULL,
];
/** @var $term \Drupal\Core\Entity\EntityInterface */
$term = $this->entityRepository->getTranslationFromContext($term);
@ -301,7 +357,16 @@ class OverviewTerms extends FormBase {
'#title' => $term->getName(),
'#url' => $term->toUrl(),
];
if ($vocabulary_hierarchy != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) {
// 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 ($update_tree_access->isAllowed() && count($tree) > 1) {
$parent_fields = TRUE;
$form['terms'][$key]['term']['tid'] = [
'#type' => 'hidden',
@ -330,9 +395,9 @@ class OverviewTerms extends FormBase {
];
}
$update_access = $term->access('update', NULL, TRUE);
$change_weight_access = $change_weight_access->andIf($update_access);
$update_tree_access = $update_tree_access->andIf($update_access);
if ($update_access->isAllowed()) {
if ($update_tree_access->isAllowed()) {
$form['terms'][$key]['weight'] = [
'#type' => 'weight',
'#delta' => $delta,
@ -350,7 +415,6 @@ class OverviewTerms extends FormBase {
];
}
$form['terms'][$key]['#attributes']['class'] = [];
if ($parent_fields) {
$form['terms'][$key]['#attributes']['class'][] = 'draggable';
}
@ -378,8 +442,8 @@ class OverviewTerms extends FormBase {
$row_position++;
}
$this->renderer->addCacheableDependency($form['terms'], $change_weight_access);
if ($change_weight_access->isAllowed()) {
$this->renderer->addCacheableDependency($form['terms'], $update_tree_access);
if ($update_tree_access->isAllowed()) {
if ($parent_fields) {
$form['terms']['#tabledrag'][] = [
'action' => 'match',
@ -408,7 +472,7 @@ class OverviewTerms extends FormBase {
];
}
if (($vocabulary_hierarchy !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) {
if ($update_tree_access->isAllowed() && count($tree) > 1) {
$form['actions'] = ['#type' => 'actions', '#tree' => FALSE];
$form['actions']['submit'] = [
'#type' => 'submit',
@ -505,12 +569,25 @@ class OverviewTerms extends FormBase {
}
}
// Save all updated terms.
foreach ($changed_terms as $term) {
$term->save();
}
if (!empty($changed_terms)) {
$pending_term_ids = $this->storageController->getTermIdsWithPendingRevisions();
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
// Force a form rebuild if any of the changed terms has a pending
// revision.
if (array_intersect_key(array_flip($pending_term_ids), $changed_terms)) {
$this->messenger()->addError($this->t('The terms with updated parents have been modified by another user, the changes could not be saved.'));
$form_state->setRebuild();
return;
}
// Save all updated terms.
foreach ($changed_terms as $term) {
$term->save();
}
$this->messenger()->addStatus($this->t('The configuration options have been saved.'));
}
}
/**

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
/**
* Validation constraint for changing the term hierarchy in pending revisions.
*
* @Constraint(
* id = "TaxonomyHierarchy",
* label = @Translation("Taxonomy term hierarchy.", context = "Validation"),
* )
*/
class TaxonomyTermHierarchyConstraint extends CompositeConstraintBase {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the hierarchy for the <em>published</em> version of this term.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['parent', 'weight'];
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Drupal\taxonomy\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing term parents in pending revisions.
*/
class TaxonomyTermHierarchyConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new TaxonomyTermHierarchyConstraintValidator instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
$term_storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
assert($term_storage instanceof TermStorageInterface);
// Newly created entities should be able to specify a parent.
if ($entity && $entity->isNew()) {
return;
}
$is_pending_revision = !$entity->isDefaultRevision();
$pending_term_ids = $term_storage->getTermIdsWithPendingRevisions();
$ancestors = $term_storage->loadAllParents($entity->id());
$ancestor_is_pending_revision = (bool) array_intersect_key($ancestors, array_flip($pending_term_ids));
$new_parents = array_column($entity->parent->getValue(), 'target_id');
$original_parents = array_keys($term_storage->loadParents($entity->id())) ?: [0];
if (($is_pending_revision || $ancestor_is_pending_revision) && $new_parents != $original_parents) {
$a = 1;
$this->context->buildViolation($constraint->message)
->atPath('parent')
->addViolation();
}
$original = $term_storage->loadUnchanged($entity->id());
if (($is_pending_revision || $ancestor_is_pending_revision) && !$entity->weight->equals($original->weight)) {
$this->context->buildViolation($constraint->message)
->atPath('weight')
->addViolation();
}
}
}

View File

@ -3,6 +3,7 @@
namespace Drupal\taxonomy;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
@ -121,6 +122,31 @@ class TermForm extends ContentEntityForm {
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. This
// is necessary as entity form displays only flag violations for fields
// contained in the 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}
*/

View File

@ -5,11 +5,12 @@ namespace Drupal\taxonomy;
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, EntityPublishedInterface {
interface TermInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
/**
* Gets the term description.

View File

@ -372,6 +372,38 @@ class TermStorage extends SqlContentEntityStorage implements TermStorageInterfac
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'];
$rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
$langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
$revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
$query = $this->database->select($this->getRevisionDataTable(), 'tfr');
$query->fields('tfr', [$id_field]);
$query->addExpression("MAX(tfr.$revision_field)", $revision_field);
$query->join($this->getRevisionTable(), 'tr', "tfr.$revision_field = tr.$revision_field AND tr.$revision_default_field = 0");
$inner_select = $this->database->select($this->getRevisionDataTable(), 't');
$inner_select->condition("t.$rta_field", '1');
$inner_select->fields('t', [$id_field, $langcode_field]);
$inner_select->addExpression("MAX(t.$revision_field)", $revision_field);
$inner_select
->groupBy("t.$id_field")
->groupBy("t.$langcode_field");
$query->join($inner_select, 'mr', "tfr.$revision_field = mr.$revision_field AND tfr.$langcode_field = mr.$langcode_field");
$query->groupBy("tfr.$id_field");
return $query->execute()->fetchAllKeyed(1, 0);
}
/**
* {@inheritdoc}
*/

View File

@ -141,4 +141,15 @@ interface TermStorageInterface extends ContentEntityStorageInterface {
*/
public function getVocabularyHierarchyType($vid);
/**
* Gets a list of term IDs with pending revisions.
*
* @return int[]
* An array of term IDs which have pending revisions, keyed by their
* revision IDs.
*
* @internal
*/
public function getTermIdsWithPendingRevisions();
}

View File

@ -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,33 +77,19 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) {
case 'entity.taxonomy_vocabulary.collection':
$output = '<p>' . t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
return $output;
case 'entity.taxonomy_vocabulary.overview_form':
$vocabulary = $route_match->getParameter('taxonomy_vocabulary');
$vocabulary_hierarchy = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getVocabularyHierarchyType($vocabulary->id());
if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . 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()]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
return '<p>' . 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()]) . '</p>';
case VocabularyInterface::HIERARCHY_MULTIPLE:
return '<p>' . 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())]) . '</p>';
}
}
else {
switch ($vocabulary_hierarchy) {
case VocabularyInterface::HIERARCHY_DISABLED:
return '<p>' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
case VocabularyInterface::HIERARCHY_SINGLE:
return '<p>' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
case VocabularyInterface::HIERARCHY_MULTIPLE:
return '<p>' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
}
}
}
}
/**
* Implements hook_entity_type_alter().
*/
function taxonomy_entity_type_alter(array &$entity_types) {
// @todo Moderation is disabled for taxonomy terms until when we have an UI
// for them.
// @see https://www.drupal.org/project/drupal/issues/2899923
$entity_types['taxonomy_term']->setHandlerClass('moderation', '');
}
/**
* Entity URI callback.
*/

View File

@ -6,6 +6,8 @@
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\ViewExecutable;
/**
@ -134,3 +136,94 @@ function taxonomy_post_update_remove_hierarchy_from_vocabularies(&$sandbox = NUL
return TRUE;
});
}
/**
* Update taxonomy terms to be revisionable.
*/
function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
/** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
$last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
$entity_type = $definition_update_manager->getEntityType('taxonomy_term');
$field_storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions('taxonomy_term');
// Update the entity type definition.
$entity_keys = $entity_type->getKeys();
$entity_keys['revision'] = 'revision_id';
$entity_keys['revision_translation_affected'] = 'revision_translation_affected';
$entity_type->set('entity_keys', $entity_keys);
$entity_type->set('revision_table', 'taxonomy_term_revision');
$entity_type->set('revision_data_table', 'taxonomy_term_field_revision');
$revision_metadata_keys = [
'revision_default' => 'revision_default',
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// Update the field storage definitions and add the new ones required by a
// revisionable entity type.
$field_storage_definitions['langcode']->setRevisionable(TRUE);
$field_storage_definitions['name']->setRevisionable(TRUE);
$field_storage_definitions['description']->setRevisionable(TRUE);
$field_storage_definitions['changed']->setRevisionable(TRUE);
$field_storage_definitions['revision_id'] = BaseFieldDefinition::create('integer')
->setName('revision_id')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision ID'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$field_storage_definitions['revision_default'] = BaseFieldDefinition::create('boolean')
->setName('revision_default')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Default revision'))
->setDescription(new TranslatableMarkup('A flag indicating whether this was a default revision when it was saved.'))
->setStorageRequired(TRUE)
->setInternal(TRUE)
->setTranslatable(FALSE)
->setRevisionable(TRUE);
$field_storage_definitions['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
->setName('revision_translation_affected')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision translation affected'))
->setDescription(new TranslatableMarkup('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
$field_storage_definitions['revision_created'] = BaseFieldDefinition::create('created')
->setName('revision_created')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision create time'))
->setDescription(new TranslatableMarkup('The time that the current revision was created.'))
->setRevisionable(TRUE);
$field_storage_definitions['revision_user'] = BaseFieldDefinition::create('entity_reference')
->setName('revision_user')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision user'))
->setDescription(new TranslatableMarkup('The user ID of the author of the current revision.'))
->setSetting('target_type', 'user')
->setRevisionable(TRUE);
$field_storage_definitions['revision_log_message'] = BaseFieldDefinition::create('string_long')
->setName('revision_log_message')
->setTargetEntityTypeId('taxonomy_term')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision log message'))
->setDescription(new TranslatableMarkup('Briefly describe the changes you have made.'))
->setRevisionable(TRUE)
->setDefaultValue('');
$definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);
return t('Taxonomy terms have been converted to be revisionable.');
}

View File

@ -156,6 +156,9 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
'tid' => [
['value' => 1],
],
'revision_id' => [
['value' => 1],
],
'uuid' => [
['value' => $this->entity->uuid()],
],
@ -205,6 +208,16 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
'value' => TRUE,
],
],
'revision_created' => [
$this->formatExpectedTimestampItemValues((int) $this->entity->getRevisionCreationTime()),
],
'revision_user' => [],
'revision_log_message' => [],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
];
}

View File

@ -129,6 +129,67 @@ class TaxonomyTermUpdatePathTest extends UpdatePathTestBase {
}
}
/**
* Tests the conversion of taxonomy terms to be revisionable.
*
* @see taxonomy_post_update_make_taxonomy_term_revisionable()
*/
public function testConversionToRevisionable() {
$this->runUpdates();
// Check the database tables and the field storage definitions.
$schema = \Drupal::database()->schema();
$this->assertTrue($schema->tableExists('taxonomy_term_data'));
$this->assertTrue($schema->tableExists('taxonomy_term_field_data'));
$this->assertTrue($schema->tableExists('taxonomy_term_revision'));
$this->assertTrue($schema->tableExists('taxonomy_term_field_revision'));
$field_storage_definitions = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('taxonomy_term');
$this->assertTrue($field_storage_definitions['langcode']->isRevisionable());
$this->assertTrue($field_storage_definitions['name']->isRevisionable());
$this->assertTrue($field_storage_definitions['description']->isRevisionable());
$this->assertTrue($field_storage_definitions['changed']->isRevisionable());
// 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');
$storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
// Check that taxonomy terms can be created, saved and then loaded.
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $storage->create([
'name' => 'Test term',
'vid' => 'article',
'revision_log_message' => 'Initial revision.',
]);
$term->save();
$storage->resetCache();
$term = $storage->loadRevision($term->getRevisionId());
$this->assertEquals('Test term', $term->label());
$this->assertEquals('article', $term->bundle());
$this->assertEquals('Initial revision.', $term->getRevisionLogMessage());
$this->assertTrue($term->isPublished());
}
/**
* {@inheritdoc}
*/

View File

@ -0,0 +1,194 @@
<?php
namespace Drupal\Tests\taxonomy\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests handling of pending revisions.
*
* @coversDefaultClass \Drupal\taxonomy\Plugin\Validation\Constraint\TaxonomyTermHierarchyConstraintValidator
*
* @group taxonomy
*/
class TermHierarchyValidationTest extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['taxonomy'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
}
/**
* Tests the term hierarchy validation with re-parenting in pending revisions.
*/
public function testTermHierarchyValidation() {
$vocabulary_id = mb_strtolower($this->randomMachineName());
$vocabulary = Vocabulary::create([
'name' => $vocabulary_id,
'vid' => $vocabulary_id,
]);
$vocabulary->save();
// Create a simple hierarchy in the vocabulary, a root term and three parent
// terms.
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $term_storage */
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$root = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
]);
$root->save();
$parent1 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $root->id(),
]);
$parent1->save();
$parent2 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $root->id(),
]);
$parent2->save();
$parent3 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $root->id(),
]);
$parent3->save();
// Create a child term and assign one of the parents above.
$child1 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $parent1->id(),
]);
$violations = $child1->validate();
$this->assertEmpty($violations);
$child1->save();
$validation_message = 'You can only change the hierarchy for the <em>published</em> version of this term.';
// Add a pending revision without changing the term parent.
$pending_name = $this->randomMachineName();
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->name = $pending_name;
$violations = $child_pending->validate();
$this->assertEmpty($violations);
// Add a pending revision and change the parent.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->parent = $parent2;
$violations = $child_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Add a pending revision and add a new parent.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->parent[0] = $parent1;
$child_pending->parent[1] = $parent3;
$violations = $child_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Add a pending revision and use the root term as a parent.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->parent[0] = $root;
$violations = $child_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Add a pending revision and remove the parent.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->parent[0] = NULL;
$violations = $child_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Add a pending revision and change the weight.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->weight = 10;
$violations = $child_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('weight', $violations[0]->getPropertyPath());
// Add a pending revision and change both the parent and the weight.
$child_pending = $term_storage->createRevision($child1, FALSE);
$child_pending->parent = $parent2;
$child_pending->weight = 10;
$violations = $child_pending->validate();
$this->assertCount(2, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals($validation_message, $violations[1]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
$this->assertEquals('weight', $violations[1]->getPropertyPath());
// Add a published revision and change the parent.
$child_pending = $term_storage->createRevision($child1, TRUE);
$child_pending->parent[0] = $parent2;
$violations = $child_pending->validate();
$this->assertEmpty($violations);
// Add a new term as a third-level child.
// The taxonomy tree structure ends up as follows:
// root
// - parent1
// - parent2
// -- child1 <- this will be a term with a pending revision
// --- child2
// - parent3
$child2 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $child1->id(),
]);
$child2->save();
// Change 'child1' to be a pending revision.
$child1 = $term_storage->createRevision($child1, FALSE);
$child1->save();
// Check that a child of a pending term can not be re-parented.
$child2_pending = $term_storage->createRevision($child2, FALSE);
$child2_pending->parent = $parent3;
$violations = $child2_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Check that another term which has a pending revision can not moved under
// another term which has pending revision.
$parent3_pending = $term_storage->createRevision($parent3, FALSE);
$parent3_pending->parent = $child1;
$violations = $parent3_pending->validate();
$this->assertCount(1, $violations);
$this->assertEquals($validation_message, $violations[0]->getMessage());
$this->assertEquals('parent', $violations[0]->getPropertyPath());
// Check that a new term can be created under a term that has a pending
// revision.
$child3 = $term_storage->create([
'name' => $this->randomMachineName(),
'vid' => $vocabulary_id,
'parent' => $child1->id(),
]);
$violations = $child3->validate();
$this->assertEmpty($violations);
}
}