diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/Data.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/Data.php new file mode 100644 index 0000000..1e5433e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/Data.php @@ -0,0 +1,181 @@ +contentType = $content_type; + } + + /** + * Gets the request content type. + * + * This will return the overriden content type if set, otherwise returns the + * content type from the request. + */ + public function getContentType() { + if (!isset($this->contentType)) { + // Return the content type based on the request object. + $negotiation = drupal_container()->get('content_negotiation'); + $request = drupal_container()->get('request'); + $content_type = $negotiation->getContentType($request); + + $this->setContentType($request->getMimeType($content_type)); + } + + return $this->contentType; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::defineOptions(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + // Set the default style plugin to 'json'. + $options['style']['contains']['type']['default'] = 'serialize'; + $options['row']['contains']['type']['default'] = 'data_entity'; + $options['defaults']['default']['style'] = FALSE; + $options['defaults']['default']['row'] = FALSE; + + // Remove css/exposed form settings, as they are not used for the data display. + unset($options['exposed_form']); + unset($options['exposed_block']); + unset($options['css_class']); + + return $options; + } + + /** + * Overrides Drupal\views\Plugin\views\display\PathPluginBase::optionsSummary(). + */ + public function optionsSummary(&$categories, &$options) { + parent::optionsSummary($categories, $options); + + unset($categories['page'], $categories['exposed']); + // Hide some settings, as they aren't useful for pure data output. + unset($options['hide_admin_links'], $options['analyze-theme']); + + $categories['path'] = array( + 'title' => t('Path settings'), + 'column' => 'second', + 'build' => array( + '#weight' => -10, + ), + ); + + $options['path']['category'] = 'path'; + $options['path']['title'] = t('Path'); + + // Remove css/exposed form settings, as they are not used for the data display. + unset($options['exposed_form']); + unset($options['exposed_block']); + unset($options['css_class']); + } + + + + /** + * Overrides Drupal\views\Plugin\views\display\PathPluginBase::execute(). + */ + public function execute() { + parent::execute(); + + $output = $this->view->render(); + + $response = new Response($output, 200, array('Content-type' => $this->getContentType())); + + return $response; + } + + /** + * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::render(). + */ + public function render() { + $output = $this->view->style_plugin->render(); + + if (!empty($this->view->live_preview)) { + return '
' . $output . '
'; + } + + return $output; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/row/DataEntityRow.php b/core/modules/views/lib/Drupal/views/Plugin/views/row/DataEntityRow.php new file mode 100644 index 0000000..c635e5e --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/DataEntityRow.php @@ -0,0 +1,41 @@ +_entity; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/row/DataFieldRow.php b/core/modules/views/lib/Drupal/views/Plugin/views/row/DataFieldRow.php new file mode 100644 index 0000000..a7a2074 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/DataFieldRow.php @@ -0,0 +1,128 @@ +options['aliases'])) { + // Prepare a trimmed version of replacement aliases. + $this->replacementAliases = array_filter(array_map('trim', (array) $this->options['aliases'])); + } + } + + /** + * Overrides \Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm(). + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['aliases'] = array('default' => array()); + + return $options; + } + + + /** + * Overrides \Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm(). + */ + public function buildOptionsForm(&$form, &$form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['aliases'] = array( + '#type' => 'fieldset', + '#title' => t('Field ID aliases'), + '#description' => t('Rename views default field ID\'s in the output data.'), + '#tree' => TRUE, + ); + + if ($fields = $this->view->display_handler->getOption('fields')) { + foreach ($fields as $id => $field) { + $form['aliases'][$id] = array( + '#type' => 'textfield', + '#title' => $id, + '#default_value' => isset($this->options['aliases'][$id]) ? $this->options['aliases'][$id] : '', + ); + } + } + } + + /** + * Overrides \Drupal\views\Plugin\views\row\RowPluginBase::validateOptionsForm(). + */ + public function validateOptionsForm(&$form, &$form_state) { + $aliases = $form_state['values']['row_options']['aliases']; + if (array_unique($aliases) !== $aliases) { + form_set_error('aliases', t('All field aliases must be unique')); + } + + } + + /** + * Overrides \Drupal\views\Plugin\views\row\RowPluginBase::render(). + */ + public function render($row) { + $output = array(); + + foreach ($this->view->field as $id => $field) { + $output[$this->getFieldKeyAlias($id)] = $row->{$field->field_alias}; + } + + return $output; + } + + /** + * Return an alias for a field ID, as set in the options form. + * + * @param string $id + * The field id to lookup an alias for. + * + * @return string + * The matches user entered alias, or the original ID if nothing is found. + */ + public function getFieldKeyAlias($id) { + if (isset($this->replacementAliases[$id])) { + return $this->replacementAliases[$id]; + } + return $id; + } + +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Serialize.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Serialize.php new file mode 100644 index 0000000..c8ac3b8 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Serialize.php @@ -0,0 +1,92 @@ +serializer = $container->get('serializer'); + $this->request = $container->get('request'); + $this->negotiation = $container->get('content_negotiation'); + } + + /** + * Overrides Drupal\views\Plugin\views\style\StylePluginBase::render(). + */ + public function render() { + $rows = array(); + // If the Data Entity row plugin is used, this will be an array of entities + // which will pass through Serializer to one of the registered Normalizers, + // which will transform it to arrays/scalars. If the Data field row plugin + // is used, $rows will not contain objects and will pass directly to the + // Encoder. + foreach ($this->view->result as $row) { + $rows[] = $this->row_plugin->render($row); + } + + $content_type = $this->negotiation->getContentType($this->request); + $this->displayHandler->setContentType($this->request->getMimeType($content_type)); + + return $this->serializer->serialize($rows, $content_type); + } + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleSerializeTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleSerializeTest.php new file mode 100644 index 0000000..33d74c6 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleSerializeTest.php @@ -0,0 +1,173 @@ + 'Style: Serializer plugin', + 'description' => 'Tests the serializer style plugin.', + 'group' => 'Views Plugins', + ); + } + + protected function setUp() { + parent::setUp(); + + $this->adminUser = $this->drupalCreateUser(array('administer views', 'administer entity_test content', 'access user profiles', 'view test entity')); + + // Save some entity_test entities. + for ($i = 1; $i <= 10; $i++) { + entity_create('entity_test', array('name' => 'test_' . $i, 'user_id' => $this->adminUser->id()))->save(); + } + + $this->enableViewsTestModule(); + } + + /** + * Checks the behavior of serialize callback paths and row plugins. + */ + public function testSerializeResponses() { + // Test the serialize callback. + $view = views_get_view('test_serialize_display_field'); + $view->initDisplay(); + $this->executeView($view); + + $actual_json = $this->drupalGet('test/serialize/field', array(), array('Accept: application/json')); + $this->assertResponse(200); + + // Test the http Content-type. + $headers = $this->drupalGetHeaders(); + $this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.'); + + $expected = array(); + foreach ($view->result as $row) { + $expected_row = array(); + foreach ($view->field as $id => $field) { + $expected_row[$id] = $row->{$field->field_alias}; + } + $expected[] = $expected_row; + } + + $this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.'); + + // Test a 403 callback. + $this->drupalGet('test/serialize/denied'); + $this->assertResponse(403); + + // Test the entity rows. + + $view = views_get_view('test_serialize_display_entity'); + $view->initDisplay(); + $this->executeView($view); + + // Get the serializer service. + $serializer = drupal_container()->get('serializer'); + + $entities = array(); + foreach ($view->result as $row) { + $entities[] = $row->_entity; + } + + $expected = $serializer->serialize($entities, 'json'); + + $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/json')); + $this->assertResponse(200); + + $this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.'); + + $expected = $serializer->serialize($entities, 'jsonld'); + $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/ld+json')); + $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.'); + + $expected = $serializer->serialize($entities, 'drupal_jsonld'); + $actual_json = $this->drupalGet('test/serialize/entity', array(), array('Accept: application/vnd.drupal.ld+json')); + $this->assertIdentical($actual_json, $expected, 'The expected JSONLD output was found.'); + } + + /** + * Test the field ID alias functionality of the DataFieldRow plugin. + */ + public function testUIFieldAlias() { + $this->drupalLogin($this->adminUser); + + // Test the UI settings for adding field ID aliases. + $this->drupalGet('admin/structure/views/view/test_serialize_display_field/edit/data_1'); + $row_options = 'admin/structure/views/nojs/display/test_serialize_display_field/data_1/row_options'; + $this->assertLinkByHref($row_options); + + // Test an empty string for an alias, this should not be used. + $this->drupalPost($row_options, array('row_options[aliases][name]' => ''), t('Apply')); + $this->drupalPost(NULL, array(), t('Save')); + + $view = views_get_view('test_serialize_display_field'); + $view->setDisplay('data_1'); + $this->executeView($view); + + $expected = array(); + foreach ($view->result as $row) { + $expected_row = array(); + foreach ($view->field as $id => $field) { + // Original field key is expected. + $expected_row[$id] = $row->{$field->field_alias}; + } + $expected[] = $expected_row; + } + + // Use an AJAX call, as this will return decoded JSON data. + $this->assertIdentical($this->drupalGetAJAX('test/serialize/field'), $expected); + + // Test a random alias for the name field, this should be replaced. + $random_name = $this->randomName(); + $this->drupalPost($row_options, array('row_options[aliases][name]' => $random_name), t('Apply')); + $this->drupalPost(NULL, array(), t('Save')); + + $view = views_get_view('test_serialize_display_field'); + $view->setDisplay('data_1'); + $this->executeView($view); + + $expected = array(); + foreach ($view->result as $row) { + $expected_row = array(); + foreach ($view->field as $id => $field) { + // Replacement alias is expected. + $expected_row[$random_name] = $row->{$field->field_alias}; + } + $expected[] = $expected_row; + } + + $this->assertIdentical($this->drupalGetAJAX('test/serialize/field'), $expected); + } + +} diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php index fdc4ce2..ae41f24 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -1249,9 +1249,6 @@ public function render($display_id = NULL) { drupal_theme_initialize(); $config = config('views.settings'); - // Set the response so other parts can alter it. - $this->response = new Response('', 200); - $start = microtime(TRUE); if (!empty($this->live_preview) && $config->get('ui.show.additional_queries')) { $this->startQueryCapture(); @@ -1282,6 +1279,11 @@ public function render($display_id = NULL) { // Initialize the style plugin. $this->initStyle(); + if (!isset($this->response)) { + // Set the response so other parts can alter it. + $this->response = new Response('', 200); + } + // Give field handlers the opportunity to perform additional queries // using the entire resultset prior to rendering. if ($this->style_plugin->usesFields()) { diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_entity.yml b/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_entity.yml new file mode 100644 index 0000000..7f08074 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_entity.yml @@ -0,0 +1,49 @@ +base_table: entity_test +name: test_serialize_display_entity +description: '' +tag: '' +human_name: 'Test serialize display entity rows' +core: 8.x +api_version: '3.0' +display: + default: + display_plugin: default + id: default + display_title: Master + position: '' + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: none + query: + type: views_query + exposed_form: + type: basic + style: + type: serialize + row: + type: data_entity + sorts: + id: + id: standard + table: entity_test + field: id + order: DESC + title: 'Test serialize' + arguments: { } + data_1: + display_plugin: data + id: data_1 + display_title: serialize + position: '' + display_options: + defaults: + access: false + path: test/serialize/entity +base_field: nid +disabled: '0' +module: views +langcode: und diff --git a/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_field.yml b/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_field.yml new file mode 100644 index 0000000..a3214e5 --- /dev/null +++ b/core/modules/views/tests/views_test_config/config/views.view.test_serialize_display_field.yml @@ -0,0 +1,82 @@ +base_table: views_test_data +name: test_serialize_display_field +description: '' +tag: '' +human_name: 'Test serialize display field rows' +core: 8.x +api_version: '3.0' +display: + default: + display_plugin: default + id: default + display_title: Master + position: '' + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: none + query: + type: views_query + exposed_form: + type: basic + style: + type: serialize + row: + type: data_field + fields: + name: + id: name + table: views_test_data + field: name + label: '' + sorts: + created: + id: created + table: views_test_data + field: created + order: DESC + title: 'Test serialize' + arguments: { } + data_1: + display_plugin: data + id: data_1 + display_title: serialize + position: '' + display_options: + defaults: + access: false + style: false + row: false + path: test/serialize/field + access: + type: none + style: + type: serialize + row: + type: data_field + data_2: + display_plugin: data + id: data_2 + display_title: 'serialize - access denied' + position: '' + display_options: + defaults: + access: false + style: false + row: false + path: test/serialize/denied + access: + type: perm + options: + perm: 'administer views' + style: + type: serialize + row: + type: data_field +base_field: id +disabled: '0' +module: views +langcode: und diff --git a/core/modules/views/views_ui/lib/Drupal/views_ui/ViewEditFormController.php b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewEditFormController.php index c8a2c96..925202f 100644 --- a/core/modules/views/views_ui/lib/Drupal/views_ui/ViewEditFormController.php +++ b/core/modules/views/views_ui/lib/Drupal/views_ui/ViewEditFormController.php @@ -816,10 +816,16 @@ public function getFormBucket(ViewUI $view, $type, $display) { $build['#name'] = $build['#title'] = $types[$type]['title']; + $rearrange_url = "admin/structure/views/nojs/rearrange/{$view->get('name')}/{$display['id']}/$type"; + $rearrange_text = t('Rearrange'); + $class = 'icon compact rearrange'; + // Different types now have different rearrange forms, so we use this switch // to get the right one. switch ($type) { case 'filter': + // The rearrange form for filters contains the and/or UI, so override + // the used path. $rearrange_url = "admin/structure/views/nojs/rearrange-$type/{$view->get('name')}/{$display['id']}/$type"; $rearrange_text = t('And/Or, Rearrange'); // TODO: Add another class to have another symbol for filter rearrange. @@ -837,6 +843,7 @@ public function getFormBucket(ViewUI $view, $type, $display) { ); return $build; } + break; case 'header': case 'footer': case 'empty': @@ -848,10 +855,7 @@ public function getFormBucket(ViewUI $view, $type, $display) { ); return $build; } - default: - $rearrange_url = "admin/structure/views/nojs/rearrange/{$view->get('name')}/{$display['id']}/$type"; - $rearrange_text = t('Rearrange'); - $class = 'icon compact rearrange'; + break; } // Create an array of actions to pass to theme_links