diff --git a/core/modules/node/config/schema/node.views.schema.yml b/core/modules/node/config/schema/node.views.schema.yml index f9596119be..2d0d7c8e75 100644 --- a/core/modules/node/config/schema/node.views.schema.yml +++ b/core/modules/node/config/schema/node.views.schema.yml @@ -66,6 +66,14 @@ views.argument.node_vid: type: boolean label: 'Exclude' +views.argument_default.node_created: + type: sequence + label: 'Current node created time' + +views.argument_default.node_changed: + type: sequence + label: 'Current node changed time' + views.field.node: type: views_field label: 'Node' diff --git a/core/modules/node/src/Plugin/views/argument_default/NodeChanged.php b/core/modules/node/src/Plugin/views/argument_default/NodeChanged.php new file mode 100644 index 0000000000..ec89636fe7 --- /dev/null +++ b/core/modules/node/src/Plugin/views/argument_default/NodeChanged.php @@ -0,0 +1,110 @@ +routeMatch = $route_match; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match'), + $container->get('date.formatter') + ); + } + + /** + * Return the current node changed time if a current node can be found. + */ + public function getArgument() { + if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) { + $argument = $this->argument; + + // The Date argument handlers provide their own format strings, otherwise + // use a default. + if ($argument instanceof Date) { + /** @var \Drupal\views\Plugin\views\argument\Date $argument */ + $format = $argument->getArgFormat(); + } + else { + $format = 'Y-m-d'; + } + + return $this->dateFormatter->format($node->getChangedTime(), 'custom', $format); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['url']; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + +} diff --git a/core/modules/node/src/Plugin/views/argument_default/NodeCreated.php b/core/modules/node/src/Plugin/views/argument_default/NodeCreated.php new file mode 100644 index 0000000000..377b8b2293 --- /dev/null +++ b/core/modules/node/src/Plugin/views/argument_default/NodeCreated.php @@ -0,0 +1,110 @@ +routeMatch = $route_match; + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match'), + $container->get('date.formatter') + ); + } + + /** + * Return the current node creation time if a current node can be found. + */ + public function getArgument() { + if (($node = $this->routeMatch->getParameter('node')) && $node instanceof NodeInterface) { + $argument = $this->argument; + + // The Date argument handlers provide their own format strings, otherwise + // use a default. + if ($argument instanceof Date) { + /** @var \Drupal\views\Plugin\views\argument\Date $argument */ + $format = $argument->getArgFormat(); + } + else { + $format = 'Y-m-d'; + } + + return $this->dateFormatter->format($node->getCreatedTime(), 'custom', $format); + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['url']; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + +} diff --git a/core/modules/node/tests/src/Functional/Views/DateArgumentDefaultTest.php b/core/modules/node/tests/src/Functional/Views/DateArgumentDefaultTest.php new file mode 100644 index 0000000000..7a2bfb1052 --- /dev/null +++ b/core/modules/node/tests/src/Functional/Views/DateArgumentDefaultTest.php @@ -0,0 +1,168 @@ +drupalCreateContentType(['type' => 'page']); + $this->currentNode = $this->drupalCreateNode(['type' => 'page']); + $this->sameTimeNode = $this->drupalCreateNode(['type' => 'page']); + $this->otherTimeNode = $this->drupalCreateNode(['type' => 'page', 'created' => strtotime('-5 days'), 'changed' => strtotime('-5 days')]); + $this->fixedTimeNode = $this->drupalCreateNode(['type' => 'page', 'created' => strtotime('1975-05-18'), 'changed' => strtotime('1975-05-18')]); + $this->sameMonthNode = $this->drupalCreateNode(['type' => 'page', 'created' => strtotime('1975-05-13'), 'changed' => strtotime('1975-05-13')]); + } + + /** + * Test the 'Current node created time' default argument handler. + * + * @see \Drupal\node\Plugin\views\argument_default\NodeCreated + */ + public function testArgumentDefaultNodeCreated() { + $this->drupalPlaceBlock('views_block:test_argument_default_date-block_1', ['label' => 'test_argument_default_date-block_1:1']); + $assert = $this->assertSession(); + + // Assert that only nodes with the same creation time as the current node + // are shown in the block. + $this->drupalGet($this->currentNode->toUrl()); + $assert->pageTextContains($this->currentNode->getTitle()); + $assert->pageTextContains($this->sameTimeNode->getTitle()); + $assert->pageTextNotContains($this->otherTimeNode->getTitle()); + + // Update the View to use the Y-m format argument. + $view = View::load('test_argument_default_date'); + $display = &$view->getDisplay('block_1'); + $display['display_options']['arguments']['created']['plugin_id'] = 'date_year_month'; + $display['display_options']['arguments']['created']['field'] = 'created_year_month'; + $display['display_options']['arguments']['created']['id'] = 'created_year_month'; + $view->save(); + + // Test that the nodes with a create date in the same month are shown. + $this->drupalGet($this->fixedTimeNode->toUrl()); + $assert->pageTextContains($this->fixedTimeNode->getTitle()); + $assert->pageTextContains($this->sameMonthNode->getTitle()); + $assert->pageTextNotContains($this->currentNode->getTitle()); + + // Update the View to use the date format argument for non-date field. + $display['display_options']['arguments']['created']['field'] = 'title'; + $view->save(); + + // Test that the nodes with a title in the same create date are shown. + $nodeTitleFixed = $this->drupalCreateNode(['type' => 'page', 'title' => '1975-05-18']); + $this->drupalGet($this->fixedTimeNode->toUrl()); + $assert->pageTextContains($nodeTitleFixed->getTitle()); + $assert->pageTextNotContains($this->sameMonthNode->getTitle()); + + // Test the getDefaultArgument() outside of node page. + $view = Views::getView('test_argument_default_date'); + $view->setDisplay('block_1'); + $view->initHandlers(); + $this->assertFalse($view->argument['created']->getDefaultArgument()); + } + + /** + * Test the 'Current node changed time' default argument handler. + * + * @see \Drupal\node\Plugin\views\argument_default\NodeChanged + */ + public function testArgumentDefaultNodeChanged() { + $this->drupalPlaceBlock('views_block:test_argument_default_date-block_2', ['label' => 'test_argument_default_date-block_2:2']); + $assert = $this->assertSession(); + + // Assert that only nodes with the same changed time as the current node + // are shown in the block. + $this->drupalGet($this->currentNode->toUrl()); + $assert->pageTextContains($this->currentNode->getTitle()); + $assert->pageTextContains($this->sameTimeNode->getTitle()); + $assert->pageTextNotContains($this->otherTimeNode->getTitle()); + + // Update the View to use the Y-m format argument. + $view = View::load('test_argument_default_date'); + $display = &$view->getDisplay('block_2'); + $display['display_options']['arguments']['changed']['plugin_id'] = 'date_year_month'; + $display['display_options']['arguments']['changed']['field'] = 'changed_year_month'; + $display['display_options']['arguments']['changed']['id'] = 'changed_year_month'; + $view->save(); + + // Test that the nodes with a create date in the same month are shown. + $this->drupalGet($this->fixedTimeNode->toUrl()); + $assert->pageTextContains($this->fixedTimeNode->getTitle()); + $assert->pageTextContains($this->sameMonthNode->getTitle()); + $assert->pageTextNotContains($this->currentNode->getTitle()); + + // Update the View to use the date format argument for non-date field. + $display['display_options']['arguments']['changed']['field'] = 'title'; + $view->save(); + + // Test that the nodes with a title in the same create date are shown. + $nodeTitleFixed = $this->drupalCreateNode(['type' => 'page', 'title' => '1975-05-18']); + $this->drupalGet($this->fixedTimeNode->toUrl()); + $assert->pageTextContains($nodeTitleFixed->getTitle()); + $assert->pageTextNotContains($this->sameMonthNode->getTitle()); + + // Test the getDefaultArgument() outside of node page. + $view = Views::getView('test_argument_default_date'); + $view->setDisplay('block_2'); + $view->initHandlers(); + $this->assertFalse($view->argument['changed']->getDefaultArgument()); + } + +} diff --git a/core/modules/views/config/schema/views.argument_default.schema.yml b/core/modules/views/config/schema/views.argument_default.schema.yml index 63fc232551..223f3d22b2 100644 --- a/core/modules/views/config/schema/views.argument_default.schema.yml +++ b/core/modules/views/config/schema/views.argument_default.schema.yml @@ -4,6 +4,10 @@ views.argument_default.*: type: mapping label: 'Base default argument' +views.argument_default.date: + type: boolean + label: 'Current date' + views.argument_default.fixed: type: mapping label: 'Fixed' diff --git a/core/modules/views/src/Plugin/views/argument/Date.php b/core/modules/views/src/Plugin/views/argument/Date.php index a751d561b7..0dccda5947 100644 --- a/core/modules/views/src/Plugin/views/argument/Date.php +++ b/core/modules/views/src/Plugin/views/argument/Date.php @@ -2,10 +2,7 @@ namespace Drupal\views\Plugin\views\argument; -use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -34,40 +31,12 @@ class Date extends Formula implements ContainerFactoryPluginInterface { protected $format; /** - * The date format used in the query. + * The default date format used in the query. * * @var string */ protected $argFormat = 'Y-m-d'; - public $option_name = 'default_argument_date'; - - /** - * The route match. - * - * @var \Drupal\Core\Routing\RouteMatchInterface - */ - protected $routeMatch; - - /** - * Constructs a new Date instance. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * - * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The route match. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - - $this->routeMatch = $route_match; - } - /** * {@inheritdoc} */ @@ -80,41 +49,6 @@ public static function create(ContainerInterface $container, array $configuratio ); } - /** - * Add an option to set the default value to the current date. - */ - public function defaultArgumentForm(&$form, FormStateInterface $form_state) { - parent::defaultArgumentForm($form, $form_state); - $form['default_argument_type']['#options'] += ['date' => $this->t('Current date')]; - $form['default_argument_type']['#options'] += ['node_created' => $this->t("Current node's creation time")]; - $form['default_argument_type']['#options'] += ['node_changed' => $this->t("Current node's update time")]; - } - - /** - * Set the empty argument value to the current date, - * formatted appropriately for this argument. - */ - public function getDefaultArgument($raw = FALSE) { - if (!$raw && $this->options['default_argument_type'] == 'date') { - return date($this->argFormat, REQUEST_TIME); - } - elseif (!$raw && in_array($this->options['default_argument_type'], ['node_created', 'node_changed'])) { - $node = $this->routeMatch->getParameter('node'); - - if (!($node instanceof NodeInterface)) { - return parent::getDefaultArgument(); - } - elseif ($this->options['default_argument_type'] == 'node_created') { - return date($this->argFormat, $node->getCreatedTime()); - } - elseif ($this->options['default_argument_type'] == 'node_changed') { - return date($this->argFormat, $node->getChangedTime()); - } - } - - return parent::getDefaultArgument($raw); - } - /** * {@inheritdoc} */ @@ -130,4 +64,14 @@ public function getFormula() { return parent::getFormula(); } + /** + * Returns the date format used in the query in a form usable by PHP. + * + * @return string + * The date format used in the query. + */ + public function getArgFormat() { + return $this->argFormat; + } + } diff --git a/core/modules/views/src/Plugin/views/argument_default/Date.php b/core/modules/views/src/Plugin/views/argument_default/Date.php new file mode 100644 index 0000000000..00dac4003a --- /dev/null +++ b/core/modules/views/src/Plugin/views/argument_default/Date.php @@ -0,0 +1,105 @@ +dateFormatter = $date_formatter; + $this->request = $request; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('date.formatter'), + $container->get('request_stack')->getCurrentRequest() + ); + } + + /** + * Return the default argument. + */ + public function getArgument() { + $argument = $this->argument; + + // The Date argument handlers provide their own format strings, otherwise + // use a default. + if ($argument instanceof \Drupal\views\Plugin\views\argument\Date) { + /** @var \Drupal\views\Plugin\views\argument\Date $argument */ + $format = $argument->getArgFormat(); + } + else { + $format = 'Y-m-d'; + } + + $request_time = $this->request->server->get('REQUEST_TIME'); + + return $this->dateFormatter->format($request_time, 'custom', $format); + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return 0; + } + +} diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_date.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_date.yml new file mode 100644 index 0000000000..20cbe99111 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_date.yml @@ -0,0 +1,170 @@ +langcode: und +status: true +dependencies: + module: + - node +id: test_argument_default_date +label: '' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: '8' +display: + default: + display_options: + access: + type: none + arguments: + 'null': + default_action: default + default_argument_type: date + field: 'null' + id: 'null' + must_not_be: false + table: views + plugin_id: 'null' + cache: + type: none + exposed_form: + type: basic + fields: + title: + alter: + alter_text: false + ellipsis: true + html: false + make_link: false + strip_tags: false + trim: false + word_boundary: true + empty_zero: false + field: title + hide_empty: false + id: title + link_to_node: false + table: node_field_data + plugin_id: node + entity_type: node + entity_field: title + pager: + options: + id: 0 + items_per_page: 10 + offset: 0 + type: full + style: + type: default + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: 0 + block_1: + display_plugin: block + id: block_1 + display_title: Block + position: 1 + display_options: + display_extenders: { } + arguments: + created: + id: created + table: node_field_data + field: created + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: node_created + default_argument_options: { } + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + entity_type: node + entity_field: created + plugin_id: date + defaults: + arguments: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } + block_2: + display_plugin: block + id: block_2 + display_title: 'Block 2' + position: 2 + display_options: + display_extenders: { } + arguments: + changed: + id: changed + table: node_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: node_changed + default_argument_options: { } + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + entity_type: node + entity_field: changed + plugin_id: date + defaults: + arguments: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php b/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php index 57eb2320b8..61652ba0f6 100644 --- a/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/ArgumentDefaultTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\views\Functional\Plugin; use Drupal\Core\Url; +use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\views\Functional\ViewTestBase; @@ -28,6 +29,7 @@ class ArgumentDefaultTest extends ViewTestBase { 'test_argument_default_current_user', 'test_argument_default_node', 'test_argument_default_query_param', + 'test_argument_default_date', ]; /** @@ -127,6 +129,41 @@ public function testArgumentDefaultFixed() { $this->assertEqual($view->args[0], $random_string, 'Provided argument should be used.'); } + /** + * Tests current date default argument. + * + * @see \Drupal\views\Plugin\views\argument_default\Date + */ + public function testArgumentDefaultDate() { + /** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */ + $date_formatter = \Drupal::service('date.formatter'); + $request_time = \Drupal::requestStack()->getCurrentRequest()->server->get('REQUEST_TIME'); + + $view = Views::getView('test_argument_default_date'); + $view->setDisplay(); + $view->initHandlers(); + + $expected = $date_formatter->format($request_time, 'custom', 'Y-m-d'); + $this->assertEquals($expected, $view->argument['null']->getDefaultArgument(), 'Current date argument should be used by default.'); + + // Update the View to use the Ym format argument. + $view = Views::getView('test_argument_default_date'); + $view->setDisplay(); + $view->displayHandlers->get('default')->overrideOption('arguments', [ + 'null' => [ + 'id' => 'year_month', + 'table' => 'node_field_data', + 'field' => 'created_year_month', + 'plugin_id' => 'date_year_month', + 'default_argument_type' => 'date', + ], + ]); + $view->initHandlers(); + + $expected = $date_formatter->format($request_time, 'custom', 'Ym'); + $this->assertEquals($expected, $view->argument['null']->getDefaultArgument(), 'Current date argument should be used by default.'); + } + /** * @todo Test php default argument. */ @@ -186,4 +223,52 @@ public function testArgumentDefaultQueryParameter() { $this->assertEqual($view->argument['type']->getDefaultArgument(), 'page'); } + /** + * Tests the cacheability of the date argument default. + */ + public function testArgumentDefaultCacheability() { + // Create page for testing. + $view = Views::getView('test_argument_default_date'); + $view->setDisplay(); + $view->newDisplay('page', 'Page', 'page_1'); + $view->displayHandlers->get('page_1')->overrideOption('path', 'path-page-1'); + $view->displayHandlers->get('page_1')->overrideOption('cache', [ + 'type' => 'time', + 'options' => [ + // To eliminate UNCACHEABLE from the page as is. + 'results_lifespan' => '10000', + ], + ]); + $view->save(); + + $this->container->get('module_installer')->uninstall(['page_cache']); + + // Check that the page is not cached with date argument default. + $this->drupalGet('path-page-1'); + $this->assertSession()->statusCodeEquals(200); + $this->assertEquals('UNCACHEABLE', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + // Double check. + $this->drupalGet('path-page-1'); + $this->assertEquals('UNCACHEABLE', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + + // Change the argument to some cached option. + $view = Views::getView('test_argument_default_date'); + $view->setDisplay(); + $view->displayHandlers->get('page_1')->overrideOption('arguments', [ + 'null' => [ + 'id' => 'null', + 'table' => 'views', + 'field' => 'null', + ], + ]); + $view->save(); + + // Check that the page is cached without date argument default. + $this->drupalGet('path-page-1'); + $this->assertSession()->statusCodeEquals(200); + $this->assertEquals('MISS', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + $this->drupalGet('path-page-1'); + $this->assertEquals('HIT', $this->drupalGetHeader(DynamicPageCacheSubscriber::HEADER)); + } + } diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index f1030c71c6..d32d7da047 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -216,6 +216,13 @@ function views_post_update_revision_metadata_fields() { }); } +/** + * Clear cache to add new date default arguments. + */ +function views_post_update_add_date_default_arguments() { + // Empty update to cause a cache rebuild so that schema additions are read. +} + /** * Add additional settings to the entity link field and convert node_path usage * to entity_link.