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
parent
2af907ca36
commit
5ce9bcd9f6
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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'];
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue