From 3f29fd91435a2c1b6ca53e1f7fbfa02902d78e7e Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Tue, 3 Feb 2015 10:05:06 +0000 Subject: [PATCH] Issue #2165989 by Xano, pcambra, damiankloip, dawehner: Add a Views field handler for multiple entity operations --- .../ContentTranslationOperationsTest.php | 66 ++++++ .../config/install/views.view.content.yml | 72 ++++--- core/modules/node/src/Tests/NodeAdminTest.php | 1 + .../install/views.view.user_admin_people.yml | 70 +------ .../config/schema/views.data_types.schema.yml | 3 + core/modules/views/src/EntityViewsData.php | 8 + .../Plugin/views/field/EntityOperations.php | 124 +++++++++++ .../Handler/FieldEntityOperationsTest.php | 58 ++++++ .../views/field/EntityOperationsUnitTest.php | 170 +++++++++++++++ .../views.view.test_entity_operations.yml | 193 ++++++++++++++++++ 10 files changed, 677 insertions(+), 88 deletions(-) create mode 100644 core/modules/content_translation/src/Tests/ContentTranslationOperationsTest.php create mode 100644 core/modules/views/src/Plugin/views/field/EntityOperations.php create mode 100644 core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php create mode 100644 core/modules/views/src/Tests/Plugin/views/field/EntityOperationsUnitTest.php create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml diff --git a/core/modules/content_translation/src/Tests/ContentTranslationOperationsTest.php b/core/modules/content_translation/src/Tests/ContentTranslationOperationsTest.php new file mode 100644 index 000000000000..2fce045cfdc8 --- /dev/null +++ b/core/modules/content_translation/src/Tests/ContentTranslationOperationsTest.php @@ -0,0 +1,66 @@ +<?php + +/** + * @file + * Contains \Drupal\content_translation\Tests\ContentTranslationOperationsTest. + */ + +namespace Drupal\content_translation\Tests; + +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Tests\NodeTestBase; + +/** + * Tests the content translation operations available in the content listing. + * + * @group content_translation + */ +class ContentTranslationOperationsTest extends NodeTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['language', 'content_translation', 'node', 'views']; + + protected function setUp() { + parent::setUp(); + + // Enable additional languages. + $langcodes = ['es', 'ast']; + foreach ($langcodes as $langcode) { + ConfigurableLanguage::createFromLangcode($langcode)->save(); + } + + // Enable translation for the current entity type and ensure the change is + // picked up. + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + drupal_static_reset(); + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('router.builder')->rebuild(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + + $this->base_user_1 = $this->drupalCreateUser(['access content overview']); + $this->base_user_2 = $this->drupalCreateUser(['access content overview', 'create content translations', 'update content translations', 'delete content translations']); + } + + /** + * Test that the operation "Translate" is displayed in the content listing. + */ + function testOperationTranslateLink() { + $node = $this->drupalCreateNode(['type' => 'article', 'langcode' => 'es']); + // Verify no translation operation links are displayed for users without + // permission. + $this->drupalLogin($this->base_user_1); + $this->drupalGet('admin/content'); + $this->assertNoLinkByHref('node/' . $node->id() . '/translations'); + $this->drupalLogout(); + // Verify there's a translation operation link for users with enough + // permissions. + $this->drupalLogin($this->base_user_2); + $this->drupalGet('admin/content'); + $this->assertLinkByHref('node/' . $node->id() . '/translations'); + } + +} diff --git a/core/modules/node/config/install/views.view.content.yml b/core/modules/node/config/install/views.view.content.yml index 2a44101f8dc3..7fd418ac8883 100644 --- a/core/modules/node/config/install/views.view.content.yml +++ b/core/modules/node/config/install/views.view.content.yml @@ -249,34 +249,56 @@ display: plugin_id: date entity_type: node entity_field: changed - edit_node: - id: edit_node + operations: + id: operations table: node - field: edit_node - label: '' - exclude: true - text: Edit - plugin_id: node_link_edit - entity_type: node - delete_node: - id: delete_node - table: node - field: delete_node - label: '' - exclude: true - text: Delete - plugin_id: node_link_delete - entity_type: node - dropbutton: - id: dropbutton - table: views - field: dropbutton + field: operations + relationship: none + group_type: group + admin_label: '' label: Operations - fields: - edit_node: edit_node - delete_node: delete_node + 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: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true destination: true - plugin_id: dropbutton + plugin_id: entity_operations filters: status_extra: id: status_extra diff --git a/core/modules/node/src/Tests/NodeAdminTest.php b/core/modules/node/src/Tests/NodeAdminTest.php index 616538c774d0..44b77ff54754 100644 --- a/core/modules/node/src/Tests/NodeAdminTest.php +++ b/core/modules/node/src/Tests/NodeAdminTest.php @@ -171,4 +171,5 @@ function testContentAdminPages() { $this->assertLinkByHref('node/' . $node->id() . '/delete'); } } + } diff --git a/core/modules/user/config/install/views.view.user_admin_people.yml b/core/modules/user/config/install/views.view.user_admin_people.yml index ff62509461c6..a62860eea04f 100644 --- a/core/modules/user/config/install/views.view.user_admin_people.yml +++ b/core/modules/user/config/install/views.view.user_admin_people.yml @@ -458,61 +458,10 @@ display: plugin_id: date entity_type: user entity_field: access - edit_node: - id: edit_node + operations: + id: operations table: users - field: edit_node - relationship: none - group_type: group - admin_label: '' - label: 'Link to edit user' - exclude: true - 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: '' - 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: true - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - text: Edit - plugin_id: user_link_edit - entity_type: user - dropbutton: - id: dropbutton - table: views - field: dropbutton + field: operations relationship: none group_type: group admin_label: '' @@ -557,16 +506,9 @@ display: hide_empty: false empty_zero: false hide_alter_empty: true - fields: - edit_node: edit_node - user_bulk_form: '0' - name: '0' - status: '0' - roles_target_id: '0' - created: '0' - access: '0' destination: true - plugin_id: dropbutton + entity_type: user + plugin_id: entity_operations mail: id: mail table: users_field_data @@ -875,6 +817,7 @@ display: 1: AND field_langcode: '***LANGUAGE_language_content***' field_langcode_add_to_query: null + display_extenders: { } page_1: display_plugin: page id: page_1 @@ -900,3 +843,4 @@ display: show_admin_links: false field_langcode: '***LANGUAGE_language_content***' field_langcode_add_to_query: null + display_extenders: { } diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 7cf0cad1a992..76ffb14ca8bd 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -581,6 +581,9 @@ views_field: hide_alter_empty: type: boolean label: 'Hide rewriting if empty' + destination: + type: boolean + label: 'Append a destination query string to operation links.' plugin_id: type: string label: 'Plugin ID' diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index cb3042279719..0efd1640fcb5 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -148,6 +148,14 @@ public function getViewsData() { } } + $data[$base_table]['operations'] = array( + 'field' => array( + 'title' => $this->t('Operations links'), + 'help' => $this->t('Provides links to perform entity operations.'), + 'id' => 'entity_operations', + ), + ); + // Setup relations to the revisions/property data. if ($data_table) { $data[$data_table]['table']['join'][$base_table] = [ diff --git a/core/modules/views/src/Plugin/views/field/EntityOperations.php b/core/modules/views/src/Plugin/views/field/EntityOperations.php new file mode 100644 index 000000000000..a3b10483da08 --- /dev/null +++ b/core/modules/views/src/Plugin/views/field/EntityOperations.php @@ -0,0 +1,124 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Plugin\views\field\EntityOperations. + */ + +namespace Drupal\views\Plugin\views\field; + +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\views\ResultRow; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Renders all operations links for an entity. + * + * @ingroup views_field_handlers + * + * @ViewsField("entity_operations") + */ +class EntityOperations extends FieldPluginBase { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManagerInterface $entity_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function usesGroupBy() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function defineOptions() { + $options = parent::defineOptions(); + + $options['destination'] = array( + 'default' => TRUE, + ); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['destination'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Include destination'), + '#description' => $this->t('Include a <code>destination</code> parameter in the link to return the user to the original view upon completing the link action.'), + '#default_value' => $this->options['destination'], + ); + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values) { + $entity = $this->getEntity($values); + $operations = $this->entityManager->getListBuilder($entity->getEntityTypeId())->getOperations($entity); + if ($this->options['destination']) { + foreach ($operations as &$operation) { + if (!isset($operation['options']['query'])) { + $operation['options']['query'] = array(); + } + $operation['options']['query'] += drupal_get_destination(); + } + } + $build = array( + '#type' => 'operations', + '#links' => $operations, + ); + + return $build; + } + + /** + * {@inheritdoc} + */ + public function query() { + // There is nothing to ensure or add for this handler, so we purposefully do + // nothing here and do not call parent::query() either. + } + +} diff --git a/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php b/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php new file mode 100644 index 000000000000..38a54b54c894 --- /dev/null +++ b/core/modules/views/src/Tests/Handler/FieldEntityOperationsTest.php @@ -0,0 +1,58 @@ +<?php + +/** + * @file + * Definition of \Drupal\views\Tests\Handler\FieldEntityOperationsTest. + */ + +namespace Drupal\views\Tests\Handler; + +use Drupal\views\Tests\ViewUnitTestBase; +use Drupal\views\Views; + +/** + * Tests the core Drupal\views\Plugin\views\field\EntityOperations handler. + * + * @group views + */ +class FieldEntityOperationsTest extends HandlerTestBase { + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = array('test_entity_operations'); + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('node'); + + /** + * Tests entity operations field. + */ + public function testEntityOperations() { + // Create some test nodes. + $nodes = array(); + for ($i = 0; $i < 5; $i++) { + $nodes[] = $this->drupalCreateNode(); + } + + $admin_user = $this->drupalCreateUser(array('access administration pages', 'bypass node access')); + $this->drupalLogin($admin_user); + $this->drupalGet('test-entity-operations'); + + /* @var $node \Drupal\node\NodeInterface */ + foreach ($nodes as $node) { + $operations = \Drupal::entityManager()->getListBuilder('node')->getOperations($node); + foreach ($operations as $operation) { + $result = $this->xpath('//ul[contains(@class, dropbutton)]/li/a[contains(@href, :path) and text()=:title]', array(':path' => $operation['url']->getInternalPath(), ':title' => $operation['title'])); + $this->assertEqual(count($result), 1, t('Found node @operation link.', array('@operation' => $operation['title']))); + } + } + } + +} diff --git a/core/modules/views/src/Tests/Plugin/views/field/EntityOperationsUnitTest.php b/core/modules/views/src/Tests/Plugin/views/field/EntityOperationsUnitTest.php new file mode 100644 index 000000000000..751471040c04 --- /dev/null +++ b/core/modules/views/src/Tests/Plugin/views/field/EntityOperationsUnitTest.php @@ -0,0 +1,170 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Tests\Plugin\views\field\EntityOperationsUnitTest. + */ + +namespace Drupal\views\Tests\Plugin\views\field { + +use Drupal\Tests\UnitTestCase; +use Drupal\views\Plugin\views\field\EntityOperations; +use Drupal\views\ResultRow; + +/** + * @coversDefaultClass \Drupal\views\Plugin\views\field\EntityOperations + * @group Views + */ +class EntityOperationsUnitTest extends UnitTestCase { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $entityManager; + + /** + * The plugin under test. + * + * @var \Drupal\views\Plugin\views\field\EntityOperations + */ + protected $plugin; + + /** + * {@inheritdoc} + * + * @covers ::__construct + */ + public function setUp() { + $this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); + + $configuration = array(); + $plugin_id = $this->randomMachineName(); + $plugin_definition = array( + 'title' => $this->randomMachineName(), + ); + $this->plugin = new EntityOperations($configuration, $plugin_id, $plugin_definition, $this->entityManager); + $view = $this->getMockBuilder('\Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $display = $this->getMockBuilder('\Drupal\views\Plugin\views\display\DisplayPluginBase') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $view->display_handler = $display; + $this->plugin->init($view, $display); + } + + /** + * @covers ::usesGroupBy + */ + public function testUsesGroupBy() { + $this->assertFalse($this->plugin->usesGroupBy()); + } + + /** + * @covers ::defineOptions + */ + public function testDefineOptions() { + $options = $this->plugin->defineOptions(); + $this->assertInternalType('array', $options); + $this->assertArrayHasKey('destination', $options); + } + + /** + * @covers ::render + */ + public function testRenderWithDestination() { + $entity_type_id = $this->randomMachineName(); + $entity = $this->getMockBuilder('\Drupal\user\Entity\Role') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->any()) + ->method('getEntityTypeId') + ->will($this->returnValue($entity_type_id)); + + $operations = array( + 'foo' => array( + 'title' => $this->randomMachineName(), + ), + ); + $list_builder = $this->getMock('\Drupal\Core\Entity\EntityListBuilderInterface'); + $list_builder->expects($this->once()) + ->method('getOperations') + ->with($entity) + ->will($this->returnValue($operations)); + + $this->entityManager->expects($this->once()) + ->method('getListBuilder') + ->with($entity_type_id) + ->will($this->returnValue($list_builder)); + + $this->plugin->options['destination'] = TRUE; + + $result = new ResultRow(); + $result->_entity = $entity; + + $expected_build = array( + '#type' => 'operations', + '#links' => $operations + ); + $expected_build['#links']['foo']['options']['query'] = drupal_get_destination(); + $build = $this->plugin->render($result); + $this->assertSame($expected_build, $build); + } + + /** + * @covers ::render + */ + public function testRenderWithoutDestination() { + $entity_type_id = $this->randomMachineName(); + $entity = $this->getMockBuilder('\Drupal\user\Entity\Role') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->any()) + ->method('getEntityTypeId') + ->will($this->returnValue($entity_type_id)); + + $operations = array( + 'foo' => array( + 'title' => $this->randomMachineName(), + ), + ); + $list_builder = $this->getMock('\Drupal\Core\Entity\EntityListBuilderInterface'); + $list_builder->expects($this->once()) + ->method('getOperations') + ->with($entity) + ->will($this->returnValue($operations)); + + $this->entityManager->expects($this->once()) + ->method('getListBuilder') + ->with($entity_type_id) + ->will($this->returnValue($list_builder)); + + $this->plugin->options['destination'] = FALSE; + + $result = new ResultRow(); + $result->_entity = $entity; + + $expected_build = array( + '#type' => 'operations', + '#links' => $operations + ); + $build = $this->plugin->render($result); + $this->assertSame($expected_build, $build); + } +} + +} + +namespace { + +if (!function_exists('drupal_get_destination')) { + function drupal_get_destination() { + return array( + 'destination' => 'foobar', + ); + } +} + +} diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml new file mode 100644 index 000000000000..205e8ce64fbb --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_operations.yml @@ -0,0 +1,193 @@ +langcode: und +status: true +dependencies: + module: + - node + - user +id: test_entity_operations +label: test_entity_operations +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: null + display_options: + access: + type: perm + cache: + type: none + query: + type: views_query + exposed_form: + type: basic + pager: + type: full + options: + items_per_page: 10 + style: + type: default + row: + type: fields + fields: + nid: + id: nid + table: node + field: nid + plugin_id: node + relationship: none + group_type: group + admin_label: '' + label: Nid + exclude: true + 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: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + link_to_node: false + entity_type: node + entity_field: nid + title: + id: title + table: node_field_data + field: title + plugin_id: node + 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 + link_to_node: true + entity_type: node + entity_field: title + operations: + id: operations + table: node + field: operations + relationship: none + group_type: group + admin_label: '' + label: Operations + 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: '' + 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: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + plugin_id: entity_operations + filters: + status: + value: true + table: node_field_data + field: status + id: status + plugin_id: boolean + expose: + operator: '0' + group: 1 + entity_type: node + entity_field: status + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + plugin_id: date + entity_type: node + entity_field: created + title: test_entity_operations + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: null + display_options: + path: test-entity-operations + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null -- GitLab