From 1be0f07ef5c238e2088258a3db402497fb3bed72 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Mon, 26 Feb 2024 17:43:51 +0000 Subject: [PATCH] Issue #2002174 by Wim Leers, xjm, fago, effulgentsia, catch, smustgrave: Allow vocabularies to be validated via the API, not just during form submissions --- core/modules/node/node.module | 15 +++++++ core/modules/node/node.post_update.php | 11 +---- .../config/schema/taxonomy.schema.yml | 11 ++++- .../taxonomy/src/Entity/Vocabulary.php | 6 +-- .../destination/EntityTaxonomyVocabulary.php | 31 +++++++++++++ core/modules/taxonomy/src/VocabularyForm.php | 15 +++++++ core/modules/taxonomy/taxonomy.module | 13 ++++++ .../modules/taxonomy/taxonomy.post_update.php | 12 +++++ ...emove-description-from-tags-vocabulary.php | 24 ++++++++++ .../Functional/Update/NullDescriptionTest.php | 45 +++++++++++++++++++ .../src/Kernel/VocabularyValidationTest.php | 5 +++ 11 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php create mode 100644 core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php create mode 100644 core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 7b795d72cfff..fe660625d190 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1323,3 +1323,18 @@ function node_comment_delete($comment) { function node_config_translation_info_alter(&$info) { $info['node_type']['class'] = 'Drupal\node\ConfigTranslation\NodeTypeMapper'; } + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function node_node_type_presave(NodeTypeInterface $node_type) { + // Content types' `help` and `description` fields must be stored as NULL + // at the config level if they are empty. + // @see node_post_update_set_node_type_description_and_help_to_null() + if (trim($node_type->getDescription()) === '') { + $node_type->set('description', NULL); + } + if (trim($node_type->getHelp()) === '') { + $node_type->set('help', NULL); + } +} diff --git a/core/modules/node/node.post_update.php b/core/modules/node/node.post_update.php index c02da56a16a0..47894ee83a4d 100644 --- a/core/modules/node/node.post_update.php +++ b/core/modules/node/node.post_update.php @@ -14,15 +14,8 @@ function node_post_update_set_node_type_description_and_help_to_null(array &$sandbox): void { \Drupal::classResolver(ConfigEntityUpdater::class) ->update($sandbox, 'node_type', function (NodeTypeInterface $node_type): bool { - // Content types' `help` and `description` fields must be stored as NULL - // at the config level if they are empty. - if (trim($node_type->getDescription()) === '') { - $node_type->set('description', NULL); - } - if (trim($node_type->getHelp()) === '') { - $node_type->set('help', NULL); - } - return TRUE; + // @see node_node_type_presave() + return trim($node_type->getDescription()) === '' || trim($node_type->getHelp()) === ''; }); } diff --git a/core/modules/taxonomy/config/schema/taxonomy.schema.yml b/core/modules/taxonomy/config/schema/taxonomy.schema.yml index 5fe96fce454d..4b4cbc0a22cd 100644 --- a/core/modules/taxonomy/config/schema/taxonomy.schema.yml +++ b/core/modules/taxonomy/config/schema/taxonomy.schema.yml @@ -22,6 +22,8 @@ taxonomy.settings: taxonomy.vocabulary.*: type: config_entity label: 'Vocabulary' + constraints: + FullyValidatable: ~ mapping: name: type: required_label @@ -35,11 +37,18 @@ taxonomy.vocabulary.*: Length: max: 32 description: - type: label + type: text label: 'Description' + nullable: true + constraints: + NotBlank: + allowNull: true weight: type: integer label: 'Weight' + # A weight can be any integer, positive or negative. + constraints: + NotNull: [] new_revision: type: boolean label: 'Whether a new revision should be created by default' diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index 606bdb011a17..790a3744c8e1 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -80,9 +80,9 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface { /** * Description of the vocabulary. * - * @var string + * @var string|null */ - protected $description; + protected $description = NULL; /** * The weight of this vocabulary in relation to other vocabularies. @@ -102,7 +102,7 @@ public function id() { * {@inheritdoc} */ public function getDescription() { - return $this->description; + return $this->description ?? ''; } /** diff --git a/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php b/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php new file mode 100644 index 000000000000..20990337acf4 --- /dev/null +++ b/core/modules/taxonomy/src/Plugin/migrate/destination/EntityTaxonomyVocabulary.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\taxonomy\Plugin\migrate\destination; + +use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase; +use Drupal\migrate\Row; + +/** + * @MigrateDestination( + * id = "entity:taxonomy_vocabulary" + * ) + */ +class EntityTaxonomyVocabulary extends EntityConfigBase { + + /** + * {@inheritdoc} + */ + public function getEntity(Row $row, array $old_destination_id_values) { + /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */ + $vocabulary = parent::getEntity($row, $old_destination_id_values); + + // Config schema does not allow description to be empty. + if (trim($vocabulary->getDescription()) === '') { + $vocabulary->set('description', NULL); + } + return $vocabulary; + } + +} diff --git a/core/modules/taxonomy/src/VocabularyForm.php b/core/modules/taxonomy/src/VocabularyForm.php index c91bf953c899..b822a34c0e75 100644 --- a/core/modules/taxonomy/src/VocabularyForm.php +++ b/core/modules/taxonomy/src/VocabularyForm.php @@ -42,6 +42,21 @@ public static function create(ContainerInterface $container) { ); } + /** + * {@inheritdoc} + */ + public function buildEntity(array $form, FormStateInterface $form_state) { + /** @var \Drupal\taxonomy\VocabularyInterface $entity */ + $entity = parent::buildEntity($form, $form_state); + + // The description cannot be an empty string. + if (trim($form_state->getValue('description')) === '') { + $entity->set('description', NULL); + } + + return $entity; + } + /** * {@inheritdoc} */ diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index d76ef3f10358..787263ca20a6 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -11,6 +11,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\taxonomy\Entity\Term; +use Drupal\taxonomy\VocabularyInterface; /** * Implements hook_help(). @@ -279,3 +280,15 @@ function taxonomy_taxonomy_term_delete(Term $term) { /** * @} End of "defgroup taxonomy_index". */ + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function taxonomy_taxonomy_vocabulary_presave(VocabularyInterface $vocabulary) { + // Vocabularies' `description` field must be stored as NULL at the config + // level if it is empty. + // @see taxonomy_post_update_set_vocabulary_description_to_null() + if (trim($vocabulary->getDescription()) === '') { + $vocabulary->set('description', NULL); + } +} diff --git a/core/modules/taxonomy/taxonomy.post_update.php b/core/modules/taxonomy/taxonomy.post_update.php index 0a8cc89090b9..f80db2876d96 100644 --- a/core/modules/taxonomy/taxonomy.post_update.php +++ b/core/modules/taxonomy/taxonomy.post_update.php @@ -6,6 +6,7 @@ */ use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\taxonomy\VocabularyInterface; /** * Implements hook_removed_post_updates(). @@ -31,3 +32,14 @@ function taxonomy_post_update_set_new_revision(&$sandbox = NULL) { return TRUE; }); } + +/** + * Converts empty `description` in vocabularies to NULL. + */ +function taxonomy_post_update_set_vocabulary_description_to_null(array &$sandbox): void { + \Drupal::classResolver(ConfigEntityUpdater::class) + ->update($sandbox, 'taxonomy_vocabulary', function (VocabularyInterface $vocabulary): bool { + // @see taxonomy_taxonomy_vocabulary_presave() + return trim($vocabulary->getDescription()) === ''; + }); +} diff --git a/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php b/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php new file mode 100644 index 000000000000..6cf86e877d7d --- /dev/null +++ b/core/modules/taxonomy/tests/fixtures/update/remove-description-from-tags-vocabulary.php @@ -0,0 +1,24 @@ +<?php + +/** + * @file + * Empties the description of the `tags` vocabulary. + */ + +use Drupal\Core\Database\Database; + +$connection = Database::getConnection(); + +$data = $connection->select('config') + ->condition('name', 'taxonomy.vocabulary.tags') + ->fields('config', ['data']) + ->execute() + ->fetchField(); +$data = unserialize($data); +$data['description'] = "\n"; +$connection->update('config') + ->condition('name', 'taxonomy.vocabulary.tags') + ->fields([ + 'data' => serialize($data), + ]) + ->execute(); diff --git a/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php b/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php new file mode 100644 index 000000000000..391ee7e7ae7b --- /dev/null +++ b/core/modules/taxonomy/tests/src/Functional/Update/NullDescriptionTest.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\taxonomy\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\taxonomy\Entity\Vocabulary; + +/** + * Tests the upgrade path for making vocabularies' description NULL. + * + * @group taxonomy + * @see taxonomy_post_update_set_vocabulary_description_to_null() + */ +class NullDescriptionTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz', + __DIR__ . '/../../../fixtures/update/remove-description-from-tags-vocabulary.php', + ]; + } + + /** + * Tests the upgrade path for updating empty description to NULL. + */ + public function testRunUpdates(): void { + $vocabulary = Vocabulary::load('tags'); + $this->assertInstanceOf(Vocabulary::class, $vocabulary); + + $this->assertSame("\n", $vocabulary->get('description')); + $this->runUpdates(); + + $vocabulary = Vocabulary::load('tags'); + $this->assertInstanceOf(Vocabulary::class, $vocabulary); + + $this->assertNull($vocabulary->get('description')); + $this->assertSame('', $vocabulary->getDescription()); + } + +} diff --git a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php index 7de121d2dffe..761802fe13f8 100644 --- a/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php +++ b/core/modules/taxonomy/tests/src/Kernel/VocabularyValidationTest.php @@ -12,6 +12,11 @@ */ class VocabularyValidationTest extends ConfigEntityValidationTestBase { + /** + * {@inheritdoc} + */ + protected static array $propertiesWithOptionalValues = ['description']; + /** * {@inheritdoc} */ -- GitLab