diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/display/RestExport.php b/core/modules/views/lib/Drupal/views/Plugin/views/display/RestExport.php new file mode 100644 index 0000000..7185acf --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/RestExport.php @@ -0,0 +1,230 @@ +get('content_negotiation'); + $request = $container->get('request'); + + $this->setContentType($negotiation->getContentType($request)); + $this->setMimeType($request->getMimeType($this->contentType)); + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::getStyleType(). + */ + protected function getStyleType() { + return 'data'; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::usesExposed(). + */ + public function usesExposed() { + return FALSE; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::displaysExposed(). + */ + public function displaysExposed() { + return FALSE; + } + + /** + * Sets the request content type. + * + * @var string $mime_type + * The response mime type. E.g. 'application/json'. + */ + public function setMimeType($mime_type) { + $this->mimeType = $mime_type; + } + + /** + * Gets the mime type. + * + * This will return any overriden mime type, otherwise returns the mime type + * from the request. + * + * @var string + * The response mime type. E.g. 'application/json'. + */ + public function getMimeType() { + return $this->mimeType; + } + + /** + * Sets the content type. + * + * @var string $content_type + * The content type machine name. E.g. 'json'. + */ + public function setContentType($content_type) { + $this->contentType = $content_type; + } + + /** + * Gets the content type. + * + * @return string + * The content type machine name. E.g. 'json'. + */ + public function getContentType() { + 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'] = 'serializer'; + $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(); + + return new Response($this->view->render(), 200, array('Content-type' => $this->getMimeType())); + } + + /** + * 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; + } + + /** + * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::preview(). + * + * The DisplayPluginBase preview method assumes we will be returning a render + * array. The data plugin will already return the serialized string. + */ + public function preview() { + return $this->view->render(); + } + +} 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..dd520d5 --- /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..fa04a17 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/row/DataFieldRow.php @@ -0,0 +1,141 @@ +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 IDs 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 filter returns empty, no values have been entered. Unique keys + // should only be validated if we have some. + if (array_filter($aliases) && (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) { + // If we don't have a field alias, Just try to get the rendered output + // from the field. + if ($field->field_alias == 'unknown') { + $value = $field->render($row); + } + // Get the value directly from the result row. + else { + $value = $row->{$field->field_alias}; + } + + $output[$this->getFieldKeyAlias($id)] = $value; + } + + 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/Serializer.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Serializer.php new file mode 100644 index 0000000..01dd596 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Serializer.php @@ -0,0 +1,74 @@ +serializer = drupal_container()->get('serializer'); + } + + /** + * 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); + } + + return $this->serializer->serialize($rows, $this->displayHandler->getContentType()); + } + +} 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..9a987d2 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleSerializeTest.php @@ -0,0 +1,200 @@ + '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 the Serializer callback paths and row plugins. + */ + public function testSerializerResponses() { + // 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) { + if ($field->field_alias == 'unknown') { + $expected_row[$id] = $field->render($row); + } + else { + $expected_row[$id] = $row->{$field->field_alias}; + } + } + $expected[] = $expected_row; + } + + $this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.'); + + + // Test that the rendered output and the preview output are the same. + $view->destroy(); + $view->setDisplay('rest_export_1'); + // Mock the request content type by setting it on the display handler. + $view->display_handler->setContentType('json'); + $this->assertIdentical($actual_json, $view->preview(), 'The expected JSON preview 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/rest_export_1'); + $row_options = 'admin/structure/views/nojs/display/test_serialize_display_field/rest_export_1/row_options'; + $this->assertLinkByHref($row_options); + + // Test an empty string for an alias, this should not be used. This also + // tests that the form can be submitted with no aliases. + $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('rest_export_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. + if ($field->field_alias == 'unknown') { + $expected_row[$id] = $field->render($row); + } + else { + $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 aliases for fields, they should be replaced. + $random_name = $this->randomName(); + $random_string = $this->randomString(); + $edit = array('row_options[aliases][name]' => $random_name, 'row_options[aliases][nothing]' => $random_string); + $this->drupalPost($row_options, $edit, t('Apply')); + $this->drupalPost(NULL, array(), t('Save')); + + $view = views_get_view('test_serialize_display_field'); + $view->setDisplay('ws_endpoint_1'); + $this->executeView($view); + + $expected = array(); + foreach ($view->result as $row) { + $expected_row = array(); + foreach ($view->field as $id => $field) { + // This will be the custom field. + if ($field->field_alias == 'unknown') { + $expected_row[$random_string] = $field->render($row); + } + // This will be the name field. + else { + $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 1b6e4fc..a67af9f 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -1227,9 +1227,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(); @@ -1260,6 +1257,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/test_views/views.view.test_serialize_display_entity.yml b/core/modules/views/tests/views_test_config/test_views/views.view.test_serialize_display_entity.yml new file mode 100644 index 0000000..f19dc79 --- /dev/null +++ b/core/modules/views/tests/views_test_config/test_views/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: serializer + row: + type: data_entity + sorts: + id: + id: standard + table: entity_test + field: id + order: DESC + title: 'Test serialize' + arguments: { } + rest_export_1: + display_plugin: rest_export + id: rest_export_1 + display_title: serializer + position: '' + display_options: + defaults: + access: false + path: test/serialize/entity +base_field: id +disabled: '0' +module: views +langcode: und diff --git a/core/modules/views/tests/views_test_config/test_views/views.view.test_serialize_display_field.yml b/core/modules/views/tests/views_test_config/test_views/views.view.test_serialize_display_field.yml new file mode 100644 index 0000000..3329f8e --- /dev/null +++ b/core/modules/views/tests/views_test_config/test_views/views.view.test_serialize_display_field.yml @@ -0,0 +1,94 @@ +base_table: views_test_data +name: test_serialize_display_field +description: '' +tag: '' +human_name: 'Test serializer 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: serializer + row: + type: data_field + fields: + name: + id: name + table: views_test_data + field: name + label: '' + nothing: + id: nothing + table: views + field: nothing + relationship: none + group_type: group + admin_label: '' + label: 'Custom text' + exclude: '0' + alter: + alter_text: '1' + text: TEST + sorts: + created: + id: created + table: views_test_data + field: created + order: DESC + title: 'Test serialize' + arguments: { } + rest_export_1: + display_plugin: rest_export + id: rest_export_1 + display_title: serializer + position: '' + display_options: + defaults: + access: false + style: false + row: false + path: test/serialize/field + access: + type: none + style: + type: serializer + row: + type: data_field + rest_export_2: + display_plugin: rest_export + id: rest_export_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: serializer + 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