diff --git a/core/modules/content_moderation/config/schema/content_moderation.schema.yml b/core/modules/content_moderation/config/schema/content_moderation.schema.yml index 5a10d85e9e056aaaee0b9f30ecb9ffba662e787a..d48ffe439f8d9e6ff871ef98efc77dcfed0096ba 100644 --- a/core/modules/content_moderation/config/schema/content_moderation.schema.yml +++ b/core/modules/content_moderation/config/schema/content_moderation.schema.yml @@ -1,11 +1,3 @@ -views.filter.latest_revision: - type: views_filter - label: 'Latest revision' - mapping: - value: - type: string - label: 'Value' - content_moderation.state: type: workflows.state mapping: diff --git a/core/modules/content_moderation/content_moderation.install b/core/modules/content_moderation/content_moderation.install new file mode 100644 index 0000000000000000000000000000000000000000..e33a35973c6c94a66b5120a412c40894f9cf9c09 --- /dev/null +++ b/core/modules/content_moderation/content_moderation.install @@ -0,0 +1,16 @@ +schema(); + if ($database_schema->tableExists('content_revision_tracker')) { + $database_schema->dropTable('content_revision_tracker'); + } +} diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml index 256095a03af0878b7a4b3792941b9128da2eb056..5035b673c5720fa63cf023a92c22dd303cc02042 100644 --- a/core/modules/content_moderation/content_moderation.services.yml +++ b/core/modules/content_moderation/content_moderation.services.yml @@ -15,11 +15,6 @@ services: arguments: ['@content_moderation.moderation_information'] tags: - { name: access_check, applies_to: _content_moderation_latest_version } - content_moderation.revision_tracker: - class: Drupal\content_moderation\RevisionTracker - arguments: ['@database'] - tags: - - { name: backend_overridable } content_moderation.config_import_subscriber: class: Drupal\content_moderation\EventSubscriber\ConfigImportSubscriber arguments: ['@config.manager', '@entity_type.manager'] diff --git a/core/modules/content_moderation/content_moderation.views.inc b/core/modules/content_moderation/content_moderation.views.inc index faabc6aaeca9f0a7bbb5c77bb1f20cc1ee434129..799af942410d1965c4f77a6a5a2c938878036daa 100644 --- a/core/modules/content_moderation/content_moderation.views.inc +++ b/core/modules/content_moderation/content_moderation.views.inc @@ -16,13 +16,6 @@ function content_moderation_views_data() { return _content_moderation_views_data_object()->getViewsData(); } -/** - * Implements hook_views_data_alter(). - */ -function content_moderation_views_data_alter(array &$data) { - _content_moderation_views_data_object()->alterViewsData($data); -} - /** * Creates a ViewsData object to respond to views hooks. * diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index 580c21e81a74cd5a337ce1138dafd1c6ba00bf39..a7fcf7e8d925564688513fc409c42631850f2330 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -41,13 +41,6 @@ class EntityOperations implements ContainerInjectionInterface { */ protected $formBuilder; - /** - * The Revision Tracker service. - * - * @var \Drupal\content_moderation\RevisionTrackerInterface - */ - protected $tracker; - /** * The entity bundle information service. * @@ -64,16 +57,13 @@ class EntityOperations implements ContainerInjectionInterface { * Entity type manager service. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. - * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker - * The revision tracker. * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info * The entity bundle information service. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityTypeBundleInfoInterface $bundle_info) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; - $this->tracker = $tracker; $this->bundleInfo = $bundle_info; } @@ -85,7 +75,6 @@ public static function create(ContainerInterface $container) { $container->get('content_moderation.moderation_information'), $container->get('entity_type.manager'), $container->get('form_builder'), - $container->get('content_moderation.revision_tracker'), $container->get('entity_type.bundle.info') ); } @@ -132,7 +121,6 @@ public function entityPresave(EntityInterface $entity) { public function entityInsert(EntityInterface $entity) { if ($this->moderationInfo->isModeratedEntity($entity)) { $this->updateOrCreateFromEntity($entity); - $this->setLatestRevision($entity); } } @@ -145,7 +133,6 @@ public function entityInsert(EntityInterface $entity) { public function entityUpdate(EntityInterface $entity) { if ($this->moderationInfo->isModeratedEntity($entity)) { $this->updateOrCreateFromEntity($entity); - $this->setLatestRevision($entity); } } @@ -202,22 +189,6 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state); } - /** - * Set the latest revision. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The content entity to create content_moderation_state entity for. - */ - protected function setLatestRevision(EntityInterface $entity) { - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ - $this->tracker->setLatestRevision( - $entity->getEntityTypeId(), - $entity->id(), - $entity->language()->getId(), - $entity->getRevisionId() - ); - } - /** * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being deleted. diff --git a/core/modules/content_moderation/src/RevisionTracker.php b/core/modules/content_moderation/src/RevisionTracker.php deleted file mode 100644 index 3f82fdd362611a5cf27629cba146392998d39505..0000000000000000000000000000000000000000 --- a/core/modules/content_moderation/src/RevisionTracker.php +++ /dev/null @@ -1,154 +0,0 @@ -connection = $connection; - $this->tableName = $table; - } - - /** - * {@inheritdoc} - */ - public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) { - try { - $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); - } - catch (DatabaseExceptionWrapper $e) { - $this->ensureTableExists(); - $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); - } - - return $this; - } - - /** - * Records the latest revision of a given entity. - * - * @param string $entity_type_id - * The machine name of the type of entity. - * @param string $entity_id - * The Entity ID in question. - * @param string $langcode - * The langcode of the revision we're saving. Each language has its own - * effective tree of entity revisions, so in different languages - * different revisions will be "latest". - * @param int $revision_id - * The revision ID that is now the latest revision. - * - * @return int - * One of the valid returns from a merge query's execute method. - */ - protected function recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) { - return $this->connection->merge($this->tableName) - ->keys([ - 'entity_type' => $entity_type_id, - 'entity_id' => $entity_id, - 'langcode' => $langcode, - ]) - ->fields([ - 'revision_id' => $revision_id, - ]) - ->execute(); - } - - /** - * Checks if the table exists and create it if not. - * - * @return bool - * TRUE if the table was created, FALSE otherwise. - */ - protected function ensureTableExists() { - try { - if (!$this->connection->schema()->tableExists($this->tableName)) { - $this->connection->schema()->createTable($this->tableName, $this->schemaDefinition()); - return TRUE; - } - } - catch (SchemaObjectExistsException $e) { - // If another process has already created the table, attempting to - // recreate it will throw an exception. In this case just catch the - // exception and do nothing. - return TRUE; - } - return FALSE; - } - - /** - * Defines the schema for the tracker table. - * - * @return array - * The schema API definition for the SQL storage table. - */ - protected function schemaDefinition() { - $schema = [ - 'description' => 'Tracks the latest revision for any entity', - 'fields' => [ - 'entity_type' => [ - 'description' => 'The entity type', - 'type' => 'varchar_ascii', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'entity_id' => [ - 'description' => 'The entity ID', - 'type' => 'int', - 'length' => 255, - 'not null' => TRUE, - 'default' => 0, - ], - 'langcode' => [ - 'description' => 'The language of the entity revision', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ], - 'revision_id' => [ - 'description' => 'The latest revision ID for this entity', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ], - ], - 'primary key' => ['entity_type', 'entity_id', 'langcode'], - ]; - - return $schema; - } - -} diff --git a/core/modules/content_moderation/src/RevisionTrackerInterface.php b/core/modules/content_moderation/src/RevisionTrackerInterface.php deleted file mode 100644 index 5079151ae8bee1ebb9e9e6c606c1342b86fb693c..0000000000000000000000000000000000000000 --- a/core/modules/content_moderation/src/RevisionTrackerInterface.php +++ /dev/null @@ -1,30 +0,0 @@ -t('Content moderation (tracker)'); - - $data['content_revision_tracker']['entity_type'] = [ - 'title' => $this->t('Entity type'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'string', - ], - 'argument' => [ - 'id' => 'string', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['entity_id'] = [ - 'title' => $this->t('Entity ID'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'numeric', - ], - 'argument' => [ - 'id' => 'numeric', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['langcode'] = [ - 'title' => $this->t('Entity language'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'language', - ], - 'argument' => [ - 'id' => 'language', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - - $data['content_revision_tracker']['revision_id'] = [ - 'title' => $this->t('Latest revision ID'), - 'field' => [ - 'id' => 'standard', - ], - 'filter' => [ - 'id' => 'numeric', - ], - 'argument' => [ - 'id' => 'numeric', - ], - 'sort' => [ - 'id' => 'standard', - ], - ]; - $entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) { return $this->moderationInformation->canModerateEntitiesOfEntityType($type); }); - // Add a join for each entity type to the content_revision_tracker table. - foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) { - /** @var \Drupal\views\EntityViewsDataInterface $views_data */ - // We need the views_data handler in order to get the table name later. - if ($this->entityTypeManager->hasHandler($entity_type_id, 'views_data') && $views_data = $this->entityTypeManager->getHandler($entity_type_id, 'views_data')) { - // Add a join from the entity base table to the revision tracker table. - $base_table = $views_data->getViewsTableForEntityType($entity_type); - $data['content_revision_tracker']['table']['join'][$base_table] = [ - 'left_field' => $entity_type->getKey('id'), - 'field' => 'entity_id', - 'extra' => [ - [ - 'field' => 'entity_type', - 'value' => $entity_type_id, - ], - ], - ]; - - // Some entity types might not be translatable. - if ($entity_type->hasKey('langcode')) { - $data['content_revision_tracker']['table']['join'][$base_table]['extra'][] = [ - 'field' => 'langcode', - 'left_field' => $entity_type->getKey('langcode'), - 'operation' => '=', - ]; - } - - // Add a relationship between the revision tracker table to the latest - // revision on the entity revision table. - $data['content_revision_tracker']['latest_revision__' . $entity_type_id] = [ - 'title' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]), - 'group' => $this->t('@label revision', ['@label' => $entity_type->getLabel()]), - 'relationship' => [ - 'id' => 'standard', - 'label' => $this->t('@label latest revision', ['@label' => $entity_type->getLabel()]), - 'base' => $this->getRevisionViewsTableForEntityType($entity_type), - 'base field' => $entity_type->getKey('revision'), - 'relationship field' => 'revision_id', - 'extra' => [ - [ - 'left_field' => 'entity_type', - 'value' => $entity_type_id, - ], - ], - ], - ]; - - // Some entity types might not be translatable. - if ($entity_type->hasKey('langcode')) { - $data['content_revision_tracker']['latest_revision__' . $entity_type_id]['relationship']['extra'][] = [ - 'left_field' => 'langcode', - 'field' => $entity_type->getKey('langcode'), - 'operation' => '=', - ]; - } - } - } - // Provides a relationship from moderated entity to its moderation state // entity. - $content_moderation_state_entity_type = \Drupal::entityTypeManager()->getDefinition('content_moderation_state'); + $content_moderation_state_entity_type = $this->entityTypeManager->getDefinition('content_moderation_state'); $content_moderation_state_entity_base_table = $content_moderation_state_entity_type->getDataTable() ?: $content_moderation_state_entity_type->getBaseTable(); $content_moderation_state_entity_revision_base_table = $content_moderation_state_entity_type->getRevisionDataTable() ?: $content_moderation_state_entity_type->getRevisionTable(); foreach ($entity_types_with_moderation as $entity_type_id => $entity_type) { @@ -228,39 +104,4 @@ public function getViewsData() { return $data; } - /** - * Alters the table and field information from hook_views_data(). - * - * @param array $data - * An array of all information about Views tables and fields, collected from - * hook_views_data(), passed by reference. - * - * @see hook_views_data() - */ - public function alterViewsData(array &$data) { - $entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) { - return $this->moderationInformation->canModerateEntitiesOfEntityType($type); - }); - foreach ($entity_types_with_moderation as $type) { - $data[$type->getRevisionTable()]['latest_revision'] = [ - 'title' => t('Is Latest Revision'), - 'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'), - 'filter' => ['id' => 'latest_revision'], - ]; - } - } - - /** - * Gets the table of an entity type to be used as revision table in views. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * The entity type. - * - * @return string - * The revision base table. - */ - protected function getRevisionViewsTableForEntityType(EntityTypeInterface $entity_type) { - return $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); - } - } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml deleted file mode 100644 index 4727efa28d87e7dfd546cfe82060109682462eed..0000000000000000000000000000000000000000 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml +++ /dev/null @@ -1,447 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - - user -id: test_content_moderation_latest_revision -label: test_content_moderation_latest_revision -module: views -description: '' -tag: '' -base_table: node_field_data -base_field: nid -core: 8.x -display: - default: - display_plugin: default - id: default - display_title: Master - position: 0 - display_options: - access: - type: perm - options: - perm: 'access content' - cache: - type: tag - options: { } - query: - type: views_query - options: - disable_sql_rewrite: false - distinct: false - replica: false - query_comment: '' - query_tags: { } - exposed_form: - type: basic - options: - submit_button: Apply - reset_button: false - reset_button_label: Reset - exposed_sorts_label: 'Sort by' - expose_sort_order: true - sort_asc_label: Asc - sort_desc_label: Desc - pager: - type: mini - options: - items_per_page: 10 - offset: 0 - id: 0 - total_pages: null - expose: - items_per_page: false - items_per_page_label: 'Items per page' - items_per_page_options: '5, 10, 25, 50' - items_per_page_options_all: false - items_per_page_options_all_label: '- All -' - offset: false - offset_label: Offset - tags: - previous: ‹‹ - next: ›› - style: - type: default - options: - grouping: { } - row_class: '' - default_row_class: true - uses_fields: false - row: - type: fields - options: - inline: { } - separator: '' - hide_empty: false - default_field_elements: true - fields: - nid: - id: nid - table: node_field_data - field: nid - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: value - type: number_integer - settings: - thousand_separator: '' - prefix_suffix: true - group_column: value - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: node - entity_field: nid - plugin_id: field - revision_id: - id: revision_id - table: content_revision_tracker - field: revision_id - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - plugin_id: standard - title: - id: title - table: node_field_revision - field: title - relationship: latest_revision__node - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: value - type: string - settings: - link_to_entity: false - group_column: value - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: node - entity_field: title - plugin_id: field - moderation_state: - id: moderation_state - table: content_moderation_state_field_revision - field: moderation_state - relationship: moderation_state - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: target_id - type: string - settings: { } - group_column: target_id - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: content_moderation_state - entity_field: moderation_state - plugin_id: field - moderation_state_1: - id: moderation_state_1 - table: content_moderation_state_field_revision - field: moderation_state - relationship: moderation_state_1 - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: false - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - click_sort_column: target_id - type: string - settings: { } - group_column: target_id - group_columns: { } - group_rows: true - delta_limit: 0 - delta_offset: 0 - delta_reversed: false - delta_first_last: false - multi_type: separator - separator: ', ' - field_api_classes: false - entity_type: content_moderation_state - entity_field: moderation_state - plugin_id: field - filters: { } - sorts: - nid: - id: nid - table: node_field_data - field: nid - relationship: none - group_type: group - admin_label: '' - order: ASC - exposed: false - expose: - label: '' - entity_type: node - entity_field: nid - plugin_id: standard - header: { } - footer: { } - empty: { } - relationships: - latest_revision__node: - id: latest_revision__node - table: content_revision_tracker - field: latest_revision__node - relationship: none - group_type: group - admin_label: 'Content latest revision' - required: false - plugin_id: standard - moderation_state_1: - id: moderation_state_1 - table: node_field_revision - field: moderation_state - relationship: latest_revision__node - group_type: group - admin_label: 'Content moderation state (latest revision)' - required: false - entity_type: node - plugin_id: standard - moderation_state: - id: moderation_state - table: node_field_revision - field: moderation_state - relationship: none - group_type: group - admin_label: 'Content moderation state' - required: false - entity_type: node - plugin_id: standard - arguments: { } - display_extenders: { } - rendering_language: '***LANGUAGE_entity_default***' - cache_metadata: - max-age: -1 - contexts: - - 'languages:language_interface' - - url.query_args - - 'user.node_grants:view' - - user.permissions - tags: { } diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php deleted file mode 100644 index cf14b23297efd36795892bbb20ccf507aa0b099f..0000000000000000000000000000000000000000 --- a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php +++ /dev/null @@ -1,130 +0,0 @@ -createNodeType('Test', 'test'); - - $permissions = [ - 'access content', - 'view all revisions', - ]; - $editor1 = $this->drupalCreateUser($permissions); - - $this->drupalLogin($editor1); - - // Make a pre-moderation node. - /** @var Node $node_0 */ - $node_0 = Node::create([ - 'type' => 'test', - 'title' => 'Node 0 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_0->save(); - - // Now enable moderation for subsequent nodes. - $workflow = Workflow::load('editorial'); - $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test'); - $workflow->save(); - - // Make a node that is only ever in Draft. - /** @var Node $node_1 */ - $node_1 = Node::create([ - 'type' => 'test', - 'title' => 'Node 1 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_1->moderation_state->value = 'draft'; - $node_1->save(); - - // Make a node that is in Draft, then Published. - /** @var Node $node_2 */ - $node_2 = Node::create([ - 'type' => 'test', - 'title' => 'Node 2 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_2->moderation_state->value = 'draft'; - $node_2->save(); - - $node_2->setTitle('Node 2 - Rev 2'); - $node_2->moderation_state->value = 'published'; - $node_2->save(); - - // Make a node that is in Draft, then Published, then Draft. - /** @var Node $node_3 */ - $node_3 = Node::create([ - 'type' => 'test', - 'title' => 'Node 3 - Rev 1', - 'uid' => $editor1->id(), - ]); - $node_3->moderation_state->value = 'draft'; - $node_3->save(); - - $node_3->setTitle('Node 3 - Rev 2'); - $node_3->moderation_state->value = 'published'; - $node_3->save(); - - $node_3->setTitle('Node 3 - Rev 3'); - $node_3->moderation_state->value = 'draft'; - $node_3->save(); - - // Now show the View, and confirm that only the correct titles are showing. - $this->drupalGet('/latest'); - $page = $this->getSession()->getPage(); - $this->assertEquals(200, $this->getSession()->getStatusCode()); - $this->assertTrue($page->hasContent('Node 1 - Rev 1')); - $this->assertTrue($page->hasContent('Node 2 - Rev 2')); - $this->assertTrue($page->hasContent('Node 3 - Rev 3')); - $this->assertFalse($page->hasContent('Node 2 - Rev 1')); - $this->assertFalse($page->hasContent('Node 3 - Rev 1')); - $this->assertFalse($page->hasContent('Node 3 - Rev 2')); - $this->assertFalse($page->hasContent('Node 0 - Rev 1')); - } - - /** - * Creates a new node type. - * - * @param string $label - * The human-readable label of the type to create. - * @param string $machine_name - * The machine name of the type to create. - * - * @return NodeType - * The node type just created. - */ - protected function createNodeType($label, $machine_name) { - /** @var NodeType $node_type */ - $node_type = NodeType::create([ - 'type' => $machine_name, - 'label' => $label, - ]); - $node_type->save(); - - return $node_type; - } - -} diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index d4c4aa37cdecd401841203a4aea1b376688d178b..125d68fbd2b0cfc6da877815137cc0773d181ae0 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\content_moderation\Kernel; -use Drupal\entity_test\Entity\EntityTestMulRevPub; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; @@ -51,54 +50,6 @@ protected function setUp($import_test_views = TRUE) { $workflow->save(); } - /** - * Tests content_moderation_views_data(). - * - * @see content_moderation_views_data() - */ - public function testViewsData() { - $node = Node::create([ - 'type' => 'page', - 'title' => 'Test title first revision', - ]); - $node->moderation_state->value = 'published'; - $node->save(); - - // Create a totally unrelated entity to ensure the extra join information - // joins by the correct entity type. - $unrelated_entity = EntityTestMulRevPub::create([ - 'id' => $node->id(), - ]); - $unrelated_entity->save(); - - $this->assertEquals($unrelated_entity->id(), $node->id()); - - $revision = clone $node; - $revision->setNewRevision(TRUE); - $revision->isDefaultRevision(FALSE); - $revision->title->value = 'Test title second revision'; - $revision->moderation_state->value = 'draft'; - $revision->save(); - - $view = Views::getView('test_content_moderation_latest_revision'); - $view->execute(); - - // Ensure that the content_revision_tracker contains the right latest - // revision ID. - // Also ensure that the relationship back to the revision table contains the - // right latest revision. - $expected_result = [ - [ - 'nid' => $node->id(), - 'revision_id' => $revision->getRevisionId(), - 'title' => $revision->label(), - 'moderation_state_1' => 'draft', - 'moderation_state' => 'published', - ], - ]; - $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'content_revision_tracker_revision_id' => 'revision_id', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1']); - } - /** * Tests the join from the revision data table to the moderation state table. */ diff --git a/core/modules/views/config/schema/views.filter.schema.yml b/core/modules/views/config/schema/views.filter.schema.yml index 00eb11a97632bff9794dc04b85789a0561d53ba0..18c13b687ded2584e887085a8de515825a376145 100644 --- a/core/modules/views/config/schema/views.filter.schema.yml +++ b/core/modules/views/config/schema/views.filter.schema.yml @@ -142,6 +142,10 @@ views.filter.language: type: views.filter.in_operator label: 'Language' +views.filter.latest_revision: + type: views_filter + label: 'Latest revision' + views.filter_value.date: type: views.filter_value.numeric label: 'Date' diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 2ba2086ac10397f529244f4a0afdd50683f71725..49b0af80f88c2fe6356c5b1b4327a9a8b2f240d9 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -236,6 +236,13 @@ public function getViewsData() { 'type' => 'INNER', ]; } + + // Add a filter for showing only the latest revisions of an entity. + $data[$revision_table]['latest_revision'] = [ + 'title' => $this->t('Is Latest Revision'), + 'help' => $this->t('Restrict the view to only revisions that are the latest revision of their entity.'), + 'filter' => ['id' => 'latest_revision'], + ]; } $this->addEntityLinks($data[$base_table]); diff --git a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php b/core/modules/views/src/Plugin/views/filter/LatestRevision.php similarity index 61% rename from core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php rename to core/modules/views/src/Plugin/views/filter/LatestRevision.php index 64400197ecb1ae1239768e11ecb3f5ecbf5e6416..7930a7fb1cb753b5bae8dc753518a433bb20837a 100644 --- a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php +++ b/core/modules/views/src/Plugin/views/filter/LatestRevision.php @@ -1,12 +1,10 @@ entityTypeManager = $entity_type_manager; $this->joinHandler = $join_handler; - $this->connection = $connection; } /** @@ -70,8 +59,7 @@ public static function create(ContainerInterface $container, array $configuratio return new static( $configuration, $plugin_id, $plugin_definition, $container->get('entity_type.manager'), - $container->get('plugin.manager.views.join'), - $container->get('database') + $container->get('plugin.manager.views.join') ); } @@ -98,39 +86,28 @@ public function canExpose() { * {@inheritdoc} */ public function query() { - // The table doesn't exist until a moderated node has been saved at least - // once. Just in case, disable this filter until then. Note that this means - // the view will still show all revisions, not just latest, but this is - // sufficiently edge-case-y that it's probably not worth the time to - // handle more robustly. - if (!$this->connection->schema()->tableExists('content_revision_tracker')) { - return; - } - - $table = $this->ensureMyTable(); - /** @var \Drupal\views\Plugin\views\query\Sql $query */ $query = $this->query; + $query_base_table = $this->relationship ?: $this->view->storage->get('base_table'); - $definition = $this->entityTypeManager->getDefinition($this->getEntityType()); - $keys = $definition->getKeys(); + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType()); + $keys = $entity_type->getKeys(); $definition = [ - 'table' => 'content_revision_tracker', - 'type' => 'INNER', - 'field' => 'entity_id', - 'left_table' => $table, + 'table' => $query_base_table, + 'type' => 'LEFT', + 'field' => $keys['id'], + 'left_table' => $query_base_table, 'left_field' => $keys['id'], 'extra' => [ - ['left_field' => $keys['langcode'], 'field' => 'langcode'], - ['left_field' => $keys['revision'], 'field' => 'revision_id'], - ['field' => 'entity_type', 'value' => $this->getEntityType()], + ['left_field' => $keys['revision'], 'field' => $keys['revision'], 'operator' => '>'], ], ]; $join = $this->joinHandler->createInstance('standard', $definition); - $query->ensureTable('content_revision_tracker', $this->relationship, $join); + $join_table_alias = $query->addTable($query_base_table, $this->relationship, $join); + $query->addWhere($this->options['group'], "$join_table_alias.{$keys['id']}", NULL, 'IS NULL'); } } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml new file mode 100644 index 0000000000000000000000000000000000000000..53f0f72a664132d308d34b60571783f17f33118b --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_latest_revision_filter.yml @@ -0,0 +1,163 @@ +langcode: en +status: true +dependencies: + module: + - node +id: test_latest_revision_filter +label: '' +module: views +description: '' +tag: '' +base_table: node_field_revision +base_field: vid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + offset: 0 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + title: + id: title + table: node_field_revision + field: title + entity_type: node + entity_field: title + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: false + plugin_id: field + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + filters: + latest_revision: + id: latest_revision + table: node_revision + field: latest_revision + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + 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: node + plugin_id: latest_revision + sorts: { } + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + show_admin_links: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + tags: { } diff --git a/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php b/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d73a11793bc23913aa973e912968bcc4633f7742 --- /dev/null +++ b/core/modules/views/tests/src/Functional/Entity/LatestRevisionFilterTest.php @@ -0,0 +1,160 @@ +drupalCreateContentType(['type' => 'article']); + + // Create a node that goes through various default/pending revision stages. + $node = Node::create([ + 'title' => 'First node - v1 - default', + 'type' => 'article', + ]); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + + $node->setTitle('First node - v2 - pending'); + $node->setNewRevision(TRUE); + $node->isDefaultRevision(FALSE); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + + $node->setTitle('First node - v3 - default'); + $node->setNewRevision(TRUE); + $node->isDefaultRevision(TRUE); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + + $node->setTitle('First node - v4 - pending'); + $node->setNewRevision(TRUE); + $node->isDefaultRevision(TRUE); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + $this->latestRevisions[$node->getRevisionId()] = $node; + + // Create a node that has a default and a pending revision. + $node = Node::create([ + 'title' => 'Second node - v1 - default', + 'type' => 'article', + ]); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + + $node->setTitle('Second node - v2 - pending'); + $node->setNewRevision(TRUE); + $node->isDefaultRevision(FALSE); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + $this->latestRevisions[$node->getRevisionId()] = $node; + + // Create a node that only has a default revision. + $node = Node::create([ + 'title' => 'Third node - v1 - default', + 'type' => 'article', + ]); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + $this->latestRevisions[$node->getRevisionId()] = $node; + + // Create a node that only has a pending revision. + $node = Node::create([ + 'title' => 'Fourth node - v1 - pending', + 'type' => 'article', + ]); + $node->isDefaultRevision(FALSE); + $node->save(); + $this->allRevisions[$node->getRevisionId()] = $node; + $this->latestRevisions[$node->getRevisionId()] = $node; + } + + /** + * Tests the 'Latest revision' filter. + */ + public function testLatestRevisionFilter() { + $view = Views::getView('test_latest_revision_filter'); + + $this->executeView($view); + + // Check that we have all the results. + $this->assertCount(count($this->latestRevisions), $view->result); + + $expected = $not_expected = []; + foreach ($this->allRevisions as $revision_id => $revision) { + if (isset($this->latestRevisions[$revision_id])) { + $expected[] = [ + 'vid' => $revision_id, + 'title' => $revision->label(), + ]; + } + else { + $not_expected[] = $revision_id; + } + } + $this->assertIdenticalResultset($view, $expected, ['vid' => 'vid', 'title' => 'title'], 'The test view only shows the latest revisions.'); + $this->assertNotInResultSet($view, $not_expected, 'Non-latest revisions are not shown by the view.'); + $view->destroy(); + } + + /** + * Verifies that a list of revision IDs are not in the result. + * + * @param \Drupal\views\ViewExecutable $view + * An executed View. + * @param array $not_expected_revision_ids + * An array of revision IDs which should not be part of the result set. + * @param string $message + * (optional) A custom message to display with the assertion. + */ + protected function assertNotInResultSet(ViewExecutable $view, array $not_expected_revision_ids, $message = '') { + $found_revision_ids = array_filter($view->result, function ($row) use ($not_expected_revision_ids) { + return in_array($row->vid, $not_expected_revision_ids); + }); + $this->assertFalse($found_revision_ids, $message); + } + +}