diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index c3690ed..a77e7c9 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -317,6 +317,27 @@ public function calculateDependencies() { public function preSave(EntityStorageInterface $storage) { parent::preSave($storage); + $display_ids = array_keys($this->get('display')); + $executable = $this->executable; + + // Check for missing relationships and remove dependent handlers if any. + foreach ($display_ids as $display_id) { + $needs_check = TRUE; + while ($needs_check) { + $needs_check = FALSE; + $relationships = array_keys($executable->getHandlers('relationship', $display_id)); + foreach (Views::getHandlerTypes() as $type => $info) { + foreach ($executable->getHandlers($type, $display_id) as $handler_id => $handler) { + if (!empty($handler['relationship']) && $handler['relationship'] != 'none' && !in_array($handler['relationship'], $relationships)) { + $executable->removeHandler($display_id, $type, $handler_id); + $needs_check = TRUE; + // @todo: write message into watchdog + } + } + } + } + } + // Sort the displays. $display = $this->get('display'); ksort($display); diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index a49740d..2dbd470 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -2463,6 +2463,26 @@ public function validate() { $errors = array_merge($errors, $result); } + // Check for missing relationships. + + $dependent_handlers = array(); + + $relationships = array_keys($this->getHandlers('relationship')); + foreach (ViewExecutable::getHandlerTypes() as $type => $info) { + foreach ($this->getHandlers($type) as $handler_id => $handler) { + if (!empty($handler->options['relationship']) && $handler->options['relationship'] != 'none' && !in_array($handler->options['relationship'], $relationships)) { + $dependent_handlers[$type][] = $handler_id; + } + } + } + if (!empty($dependent_handlers)) { + $handler_type_ids = array(); + foreach ($dependent_handlers as $type => $handler_ids) { + $handler_type_ids[] = $type . ': ' . implode(', ', $handler_ids); + } + $errors[] = $this->t('You can not remove this relationship because it is used by other handlers (!param).', array('!param' => implode('; ', $handler_type_ids))); + } + // Validate handlers foreach (ViewExecutable::getHandlerTypes() as $type => $info) { foreach ($this->getHandlers($type) as $handler) { diff --git a/core/modules/views/src/Tests/Handler/HandlerTest.php b/core/modules/views/src/Tests/Handler/HandlerTest.php index f5f81b5..75ef0d9 100644 --- a/core/modules/views/src/Tests/Handler/HandlerTest.php +++ b/core/modules/views/src/Tests/Handler/HandlerTest.php @@ -24,7 +24,7 @@ class HandlerTest extends ViewTestBase { * * @var array */ - public static $testViews = array('test_view', 'test_view_handler_weight', 'test_handler_relationships', 'test_handler_test_access', 'test_filter_in_operator_ui'); + public static $testViews = array('test_view', 'test_view_handler_weight', 'test_handler_relationships', 'test_handler_test_access', 'test_filter_in_operator_ui', 'test_exposed_relationship_admin_ui'); /** * Modules to enable. @@ -277,7 +277,7 @@ public function testSetRelationship() { // Setup a broken relationship. $view->addHandler('default', 'relationship', $this->randomMachineName(), $this->randomMachineName(), array(), 'broken_relationship'); // Setup a valid relationship. - $view->addHandler('default', 'relationship', 'comment_field_data', 'node', array('relationship' => 'cid'), 'valid_relationship'); + $view->addHandler('default', 'relationship', 'comment_field_data', 'node', array('relationship' => 'comment_cid'), 'valid_relationship'); $view->initHandlers(); $field = $view->field['title']; @@ -306,6 +306,23 @@ public function testSetRelationship() { } /** + * Tests missing relationship. + */ + public function testMissingRelationship() { + $view = Views::getView('test_exposed_relationship_admin_ui'); + $view->build(); + $fields = array_keys($view->field); + $this->assertTrue(in_array('created', $fields), 'Field without missing relationships found'); + + $view = Views::getView('test_exposed_relationship_admin_ui'); + $view->removeHandler('default', 'relationship', 'uid'); + $view->save(); + $view->build(); + $fields = array_keys($view->field); + $this->assertTrue(!in_array('created', $fields), 'Field with missing relationship not found'); + } + + /** * Tests the placeholder function. * * @see \Drupal\views\Plugin\views\HandlerBase::placeholder() diff --git a/core/modules/views/src/Tests/Plugin/DisplayTest.php b/core/modules/views/src/Tests/Plugin/DisplayTest.php index 4b5b638..db8e70a 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayTest.php @@ -22,7 +22,7 @@ class DisplayTest extends PluginTestBase { * * @var array */ - public static $testViews = array('test_filter_groups', 'test_get_attach_displays', 'test_view', 'test_display_more', 'test_display_invalid', 'test_display_empty'); + public static $testViews = array('test_filter_groups', 'test_get_attach_displays', 'test_view', 'test_display_more', 'test_display_invalid', 'test_display_empty', 'test_exposed_relationship_admin_ui'); /** * Modules to enable. @@ -278,6 +278,33 @@ public function testInvalidDisplayPlugins() { } /** + * Tests display validation when a required relationship is missing. + */ + public function testMissingRelationship() { + $errorMessage = 'You can not remove this relationship because it is used by other handlers (relationship: uid).'; + + $view = Views::getView('test_exposed_relationship_admin_ui'); + + // Remove relationship + $view->removeHandler('default', 'relationship', 'uid_1'); + $errors = $view->validate(); + // Check no error message + $errorFound = !empty($errors['default']) && in_array($errorMessage, $errors['default']); + $this->assertTrue(!$errorFound, 'Error message not found for removed relationship'); + + // Unset cached relationships (see DisplayPluginBase::getHandlers()) + unset($view->display_handler->handlers['relationship']); + + // Remove requried relationship + $view->removeHandler('default', 'relationship', 'uid'); + // Validate display + $errors = $view->validate(); + // Check error message + $errorFound = !empty($errors['default']) && in_array($errorMessage, $errors['default']); + $this->assertTrue($errorFound, 'Error message found for required relationship'); + } + + /** * Tests the outputIsEmpty method on the display. */ public function testOutputIsEmpty() { diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_relationship_admin_ui.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_relationship_admin_ui.yml new file mode 100644 index 0000000..0f1fcc5 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_relationship_admin_ui.yml @@ -0,0 +1,282 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: test_exposed_relationship_admin_ui +label: test_exposed_relationship_admin_ui +module: views +description: '' +tag: '' +base_table: node +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: none + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: false + 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: full + 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: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + quantity: 9 + 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_data + 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 + link_to_node: true + plugin_id: node + 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 + login: + id: login + table: users_field_data + field: login + relationship: uid + 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: '' + 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 + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: user + entity_field: login + plugin_id: date + created: + id: created + table: users_field_data + field: created + relationship: uid + 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: '' + 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 + date_format: fallback + custom_date_format: '' + timezone: '' + entity_type: user + entity_field: created + plugin_id: date + filters: + status: + value: true + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + header: { } + footer: { } + empty: { } + relationships: + uid: + id: uid + table: node_field_data + field: uid + relationship: none + group_type: group + admin_label: author + required: false + entity_type: node + entity_field: uid + plugin_id: standard + uid_1: + id: uid_1 + table: node_field_data + field: uid + relationship: none + group_type: group + admin_label: 'author 2' + required: false + entity_type: node + entity_field: uid + plugin_id: standard + arguments: { } + display_extenders: { } + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php index 3f0ee62..4f9ef0b 100644 --- a/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php +++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandler.php @@ -173,7 +173,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $ '#type' => 'submit', '#value' => $this->t('Remove'), '#submit' => array(array($this, 'remove')), - '#limit_validation_errors' => array(array('override')), + '#limit_validation_errors' => ($type == 'relationship') ? array(array('override'), array(NULL)) : array(array('override')), '#ajax' => array( 'url' => Url::fromRoute(''), ), @@ -193,6 +193,32 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $ public function validateForm(array &$form, FormStateInterface $form_state) { $form_state->get('handler')->validateOptionsForm($form['options'], $form_state); + // Removal of relationships should not be allowed if there are handlers that depend on it. + if ($form_state->get('type') == 'relationship' && $form_state->getValue('op') == t('Remove')) { + $id = $form_state->get('id'); + $executable = $form_state->get('view')->getExecutable(); + + $dependent_handlers = array(); + $override = $form_state->getValue('override'); + $display_id = $override['dropdown']; + + foreach (ViewExecutable::getHandlerTypes() as $type => $info) { + $handlers = $executable->getHandlers($type, $display_id); + foreach ($handlers as $handler) { + if (!empty($handler['relationship']) && $handler['relationship'] == $id) { + $dependent_handlers[$type][] = $handler['id']; + } + } + } + if (!empty($dependent_handlers)) { + $handler_type_ids = array(); + foreach ($dependent_handlers as $type => $handler_ids) { + $handler_type_ids[] = $type . ': ' . implode(', ', $handler_ids); + } + $form_state->setErrorByName(NULL, t('You can not remove this relationship because it is used by other handlers (!param).', array('!param' => implode('; ', $handler_type_ids)))); + } + } + if ($form_state->getErrors()) { $form_state->set('rerender', TRUE); } diff --git a/core/modules/views_ui/src/Tests/ExposedRelationshipFormUITest.php b/core/modules/views_ui/src/Tests/ExposedRelationshipFormUITest.php new file mode 100644 index 0000000..1765749 --- /dev/null +++ b/core/modules/views_ui/src/Tests/ExposedRelationshipFormUITest.php @@ -0,0 +1,53 @@ +install(array('views_test_config')); + + $edit = array(); + // Here we don't need the whole message. + $validationMessage = t('You can not remove this relationship because it is used by other handlers ('); + + $this->drupalGet('admin/structure/views/nojs/handler/test_exposed_relationship_admin_ui/default/relationship/uid'); + + // Click the Remove button. + $this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_relationship_admin_ui/default/relationship/uid', $edit, t('Remove')); + + // Validation error message found for requried relationship. + $this->assertText($validationMessage, 'Validation error found.'); + + + + $this->drupalGet('admin/structure/views/nojs/handler/test_exposed_relationship_admin_ui/default/relationship/uid_1'); + + // Click the Remove button. + $this->drupalPostForm('admin/structure/views/nojs/handler/test_exposed_relationship_admin_ui/default/relationship/uid_1', $edit, t('Remove')); + + // Validation error message not found for non-requried relationship. + $this->assertNoText($validationMessage, 'Validation error not found.'); + } +}