diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc new file mode 100644 index 0000000..c3b7503 --- /dev/null +++ b/core/modules/datetime/datetime.views.inc @@ -0,0 +1,47 @@ + $table_data) { + // Set the 'datetime' filter type. + $data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime'; + + // Set the 'datetime' argument type. + $data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime'; + + // Create year, month, and day arguments. + $group = $data[$table_name][$field_storage->getName() . '_value']['group']; + $arguments = array( + // Argument type => help text. + 'year' => t('Date in the form of YYYY.'), + 'month' => t('Date in the form of MM.'), + 'day' => t('Date in the form of DD.'), + ); + foreach ($arguments as $argument_type => $help_text) { + $data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = array( + 'title' => $field_storage->getLabel() . ' (' . $argument_type . ')', + 'help' => $help_text, + 'argument' => array( + 'field' => $field_storage->getName() . '_value', + 'id' => 'datetime_' . $argument_type, + ), + 'group' => $group, + ); + } + + // Set the 'datetime' sort handler. + $data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime'; + } + + return $data; +} diff --git a/core/modules/datetime/src/Plugin/views/argument/Date.php b/core/modules/datetime/src/Plugin/views/argument/Date.php new file mode 100644 index 0000000..d6b933b --- /dev/null +++ b/core/modules/datetime/src/Plugin/views/argument/Date.php @@ -0,0 +1,46 @@ +tableAlias.$this->realField"; + } + + /** + * Override to account for dates stored as strings. + * + * {@inheritdoc} + */ + public function getDateFormat($format) { + // Pass in the string-field option. + return $this->query->getDateFormat($this->getDateField(), $format, TRUE); + } +} diff --git a/core/modules/datetime/src/Plugin/views/argument/DayDate.php b/core/modules/datetime/src/Plugin/views/argument/DayDate.php new file mode 100644 index 0000000..17d5cbd --- /dev/null +++ b/core/modules/datetime/src/Plugin/views/argument/DayDate.php @@ -0,0 +1,22 @@ +value['min'], 0)); + $b = intval(strtotime($this->value['max'], 0)); + + if ($this->value['type'] == 'offset') { + $a = REQUEST_TIME + $a; + $b = REQUEST_TIME + $b; + } + // Convert to ISO format and format for query. + $a = $this->query->getDateFormat("'" . format_date($a, 'custom', 'c') . "'", static::$dateFormat, TRUE); + $b = $this->query->getDateFormat("'" . format_date($b, 'custom', 'c') . "'", static::$dateFormat, TRUE); + + // This is safe because we are manually scrubbing the values. + $operator = strtoupper($this->operator); + $field = $this->query->getDateFormat($field, static::$dateFormat, TRUE); + $this->query->addWhereExpression($this->options['group'], "$field $operator $a AND $b"); + } + + /** + * Override parent method, which deals with dates as integers. + */ + protected function opSimple($field) { + $value = intval(strtotime($this->value['value'], 0)); + if (!empty($this->value['type']) && $this->value['type'] == 'offset') { + $value = REQUEST_TIME + $value; + } + // Convert to ISO. + $value = $this->query->getDateFormat("'" . format_date($value, 'custom', 'c') . "'", static::$dateFormat, TRUE); + + // This is safe because we are manually scrubbing the value. + $field = $this->query->getDateFormat($field, static::$dateFormat, TRUE); + $this->query->addWhereExpression($this->options['group'], "$field $this->operator $value"); + } + +} diff --git a/core/modules/datetime/src/Plugin/views/sort/Date.php b/core/modules/datetime/src/Plugin/views/sort/Date.php new file mode 100644 index 0000000..b86d424 --- /dev/null +++ b/core/modules/datetime/src/Plugin/views/sort/Date.php @@ -0,0 +1,46 @@ +tableAlias.$this->realField"; + } + + /** + * Override query to provide 'second' granularity. + */ + public function query() { + $this->ensureMyTable(); + switch ($this->options['granularity']) { + case 'second': + $formula = $this->getDateFormat('YmdHis'); + $this->query->addOrderBy(NULL, $formula, $this->options['order'], $this->tableAlias . '_' . $this->field . '_' . $this->options['granularity']); + return; + } + + // All other granularities are handled by the numeric sort handler. + parent::query(); + } + +} diff --git a/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php b/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php new file mode 100644 index 0000000..a7d9e92 --- /dev/null +++ b/core/modules/datetime/src/Tests/Views/ArgumentDateTimeTest.php @@ -0,0 +1,141 @@ +nodes[] = $this->drupalCreateNode(array( + 'field_date' => array( + 'value' => $date, + ) + )); + } + } + + /** + * Test year argument. + * + * @see \Drupal\datetime\Plugin\views\argument\YearDate + */ + public function testDatetimeArgumentYear() { + $view = Views::getView('test_argument_datetime'); + + // The 'default' display has the 'year' argument. + $view->setDisplay('default'); + $this->executeView($view, array('2000')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[0]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + + $view->setDisplay('default'); + $this->executeView($view, array('2002')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[2]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + } + + /** + * Test month argument. + * + * @see \Drupal\datetime\Plugin\views\argument\MonthDate + */ + public function testDatetimeArgumentMonth() { + $view = Views::getView('test_argument_datetime'); + // The 'embed_1' display has the 'month' argument. + $view->setDisplay('embed_1'); + + $this->executeView($view, array('10')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[0]->id()); + $expected[] = array('nid' => $this->nodes[1]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + + $view->setDisplay('embed_1'); + $this->executeView($view, array('01')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[2]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + } + + /** + * Test day argument. + * + * @see \Drupal\datetime\Plugin\views\argument\DayDate + */ + public function testDatetimeArgumentDay() { + $view = Views::getView('test_argument_datetime'); + + // The 'embed_2' display has the 'day' argument. + $view->setDisplay('embed_2'); + $this->executeView($view, array('10')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[0]->id()); + $expected[] = array('nid' => $this->nodes[1]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + + $view->setDisplay('embed_2'); + $this->executeView($view, array('01')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[2]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + } + + /** + * Test year, month, and day arguments combined. + */ + public function testDatetimeArgumentAll() { + $view = Views::getView('test_argument_datetime'); + // The 'embed_3' display has year, month, and day arguments. + $view->setDisplay('embed_3'); + + $this->executeView($view, array('2000', '10', '10')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[0]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + + $view->setDisplay('embed_3'); + $this->executeView($view, array('2002', '01', '01')); + $expected = array(); + $expected[] = array('nid' => $this->nodes[2]->id()); + $this->assertIdenticalResultset($view, $expected, $this->map); + $view->destroy(); + } + +} diff --git a/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php b/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php new file mode 100644 index 0000000..b68516b --- /dev/null +++ b/core/modules/datetime/src/Tests/Views/DateTimeHandlerTestBase.php @@ -0,0 +1,77 @@ + 'page', + 'name' => 'page' + )); + $node_type->save(); + $fieldStorage = entity_create('field_storage_config', array( + 'field_name' => static::$field_name, + 'entity_type' => 'node', + 'type' => 'datetime', + 'settings' => array('datetime_type' => 'datetime'), + )); + $fieldStorage->save(); + $field = entity_create('field_config', array( + 'field_storage' => $fieldStorage, + 'bundle' => 'page', + 'required' => TRUE, + )); + $field->save(); + + // Views needs to be aware of the new field. + $this->container->get('views.views_data')->clear(); + + // Set column map. + $this->map = array( + 'nid' => 'nid', + ); + + // Load test views. + ViewTestData::createTestViews(get_class($this), array('datetime_test')); + } + +} diff --git a/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php b/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php new file mode 100644 index 0000000..051e24d --- /dev/null +++ b/core/modules/datetime/src/Tests/Views/FilterDateTimeTest.php @@ -0,0 +1,151 @@ +nodes[] = $this->drupalCreateNode(array( + 'field_date' => array( + 'value' => $date, + ) + )); + } + } + + /** + * Test filter operations. + */ + public function testDatetimeFilter() { + $this->_testOffset(); + $this->_testBetween(); + } + + /** + * Test offset operations. + */ + protected function _testOffset() { + $view = Views::getView('test_filter_datetime'); + $field = static::$field_name . '_value'; + + // Test simple operations. + $view->initHandlers(); + + $view->filter[$field]->operator = '>'; + $view->filter[$field]->value['type'] = 'offset'; + $view->filter[$field]->value['value'] = '+1 hour'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[3]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test offset for between operator. + $view->initHandlers(); + $view->filter[$field]->operator = 'between'; + $view->filter[$field]->value['type'] = 'offset'; + $view->filter[$field]->value['max'] = '+2 days'; + $view->filter[$field]->value['min'] = '+1 hour'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[3]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + } + + /** + * Test between operations. + */ + protected function _testBetween() { + $view = Views::getView('test_filter_datetime'); + $field = static::$field_name . '_value'; + + // Test between with min and max. + $view->initHandlers(); + $view->filter[$field]->operator = 'between'; + $view->filter[$field]->value['min'] = '2001-01-01'; + $view->filter[$field]->value['max'] = '2002-01-01'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[1]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test between with just max. + $view->initHandlers(); + $view->filter[$field]->operator = 'between'; + $view->filter[$field]->value['max'] = '2002-01-01'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[0]->id()), + array('nid' => $this->nodes[1]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test not between with min and max. + $view->initHandlers(); + $view->filter[$field]->operator = 'not between'; + $view->filter[$field]->value['min'] = '2001-01-01'; + $view->filter[$field]->value['max'] = '2002-01-01'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[0]->id()), + array('nid' => $this->nodes[2]->id()), + array('nid' => $this->nodes[3]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Test not between with just max. + $view->initHandlers(); + $view->filter[$field]->operator = 'not between'; + $view->filter[$field]->value['max'] = '2001-01-01'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[1]->id()), + array('nid' => $this->nodes[2]->id()), + array('nid' => $this->nodes[3]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + } + +} diff --git a/core/modules/datetime/src/Tests/Views/SortDateTimeTest.php b/core/modules/datetime/src/Tests/Views/SortDateTimeTest.php new file mode 100644 index 0000000..9c9ddcb --- /dev/null +++ b/core/modules/datetime/src/Tests/Views/SortDateTimeTest.php @@ -0,0 +1,99 @@ +nodes[] = $this->drupalCreateNode(array( + 'field_date' => array( + 'value' => $date, + ) + )); + } + } + + /** + * Tests the datetime sort handler. + */ + public function testDateTimeSort() { + $field = static::$field_name . '_value'; + $view = Views::getView('test_sort_datetime'); + + // Sort order is DESC. + $view->initHandlers(); + $view->sort[$field]->options['granularity'] = 'minute'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[0]->id()), + array('nid' => $this->nodes[3]->id()), + array('nid' => $this->nodes[2]->id()), + array('nid' => $this->nodes[1]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Check ASC. + $view->initHandlers(); + $field = static::$field_name . '_value'; + $view->sort[$field]->options['order'] = 'ASC'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[1]->id()), + array('nid' => $this->nodes[2]->id()), + array('nid' => $this->nodes[3]->id()), + array('nid' => $this->nodes[0]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + + // Change granularity to 'year', and the secondary node ID order should + // define the order of nodes with the same year. + $view->initHandlers(); + $view->sort[$field]->options['granularity'] = 'year'; + $view->sort[$field]->options['order'] = 'DESC'; + $view->setDisplay('default'); + $this->executeView($view); + $expected_result = array( + array('nid' => $this->nodes[0]->id()), + array('nid' => $this->nodes[1]->id()), + array('nid' => $this->nodes[2]->id()), + array('nid' => $this->nodes[3]->id()), + ); + $this->assertIdenticalResultset($view, $expected_result, $this->map); + $view->destroy(); + } + +} diff --git a/core/modules/datetime/tests/modules/datetime_test/datetime_test.info.yml b/core/modules/datetime/tests/modules/datetime_test/datetime_test.info.yml new file mode 100644 index 0000000..84ba9e4 --- /dev/null +++ b/core/modules/datetime/tests/modules/datetime_test/datetime_test.info.yml @@ -0,0 +1,8 @@ +name: 'Datetime test' +type: module +description: 'Provides default views for tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - views diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml new file mode 100644 index 0000000..d2c4316 --- /dev/null +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_argument_datetime.yml @@ -0,0 +1,105 @@ +langcode: und +status: true +dependencies: { } +id: test_argument_datetime +label: '' +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: '8' +display: + default: + display_options: + defaults: + fields: false + pager: false + sorts: false + arguments: + field_date_value_year: + field: field_date_value_year + id: field_date_value + table: node__field_date + plugin_id: datetime_year + fields: + id: + field: nid + id: nid + relationship: none + table: node + plugin_id: numeric + pager: + options: + offset: 0 + type: none + sorts: + id: + field: nid + id: nid + order: ASC + relationship: none + table: node + plugin_id: numeric + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + display_plugin: default + display_title: Master + id: default + position: 0 + embed_1: + display_options: + defaults: + arguments: false + arguments: + field_date_value_month: + field: field_date_value_month + id: field_date_value + table: node__field_date + plugin_id: datetime_month + display_plugin: embed + id: embed_1 + display_title: '' + position: null + embed_2: + display_options: + defaults: + arguments: false + arguments: + field_date_value_day: + field: field_date_value_day + id: field_date_value + table: node__field_date + plugin_id: datetime_day + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + display_plugin: embed + id: embed_2 + display_title: '' + position: null + embed_3: + display_options: + defaults: + arguments: false + arguments: + field_date_value_year: + field: field_date_value_year + id: field_date_value + table: node__field_date + plugin_id: datetime_year + field_date_value_month: + field: field_date_value_month + id: field_date_value + table: node__field_date + plugin_id: datetime_month + field_date_value_day: + field: field_date_value_day + id: field_date_value + table: node__field_date + plugin_id: datetime_day + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + display_plugin: embed + id: embed_2 + display_title: '' + position: null diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml new file mode 100644 index 0000000..4b3fea8 --- /dev/null +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_filter_datetime.yml @@ -0,0 +1,58 @@ +langcode: und +status: true +dependencies: + module: + - node +id: test_filter_datetime +label: '' +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + nid: + field: nid + id: nid + table: node + plugin_id: node + filters: + field_date_value: + id: field_date_value + table: node__field_date + field: field_date_value + plugin_id: datetime + sorts: + id: + field: nid + id: nid + order: ASC + relationship: none + table: node + plugin_id: numeric + pager: + type: full + query: + options: + query_comment: false + type: views_query + style: + type: default + row: + type: fields + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + display_plugin: default + display_title: Master + id: default + position: 0 diff --git a/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml new file mode 100644 index 0000000..eca398a --- /dev/null +++ b/core/modules/datetime/tests/modules/datetime_test/test_views/views.view.test_sort_datetime.yml @@ -0,0 +1,59 @@ +langcode: und +status: true +dependencies: + module: + - node +id: test_sort_datetime +label: '' +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: '8' +display: + default: + display_options: + access: + type: none + cache: + type: none + exposed_form: + type: basic + fields: + nid: + field: nid + id: nid + table: node + plugin_id: node + sorts: + field_date_value: + field: field_date_value + id: field_date_value + relationship: none + table: node__field_date + order: DESC + plugin_id: datetime + id: + field: nid + id: nid + order: ASC + relationship: none + table: node + plugin_id: numeric + pager: + type: full + query: + options: + query_comment: false + type: views_query + style: + type: default + row: + type: fields + field_langcode: '***LANGUAGE_language_content***' + field_langcode_add_to_query: null + display_plugin: default + display_title: Master + id: default + position: 0 diff --git a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php index 1c254dc..80184be 100644 --- a/core/modules/views/src/Plugin/views/query/QueryPluginBase.php +++ b/core/modules/views/src/Plugin/views/query/QueryPluginBase.php @@ -235,12 +235,15 @@ public function setupTimezone() { * An appropriate query expression pointing to the date field. * @param string $format * A format string for the result, like 'Y-m-d H:i:s'. + * @param boolean $string_date + * For certain databases, date format functions vary depending on string or + * numeric storage. * * @return string * A string representing the field formatted as a date in the format * specified by $format. */ - public function getDateFormat($field, $format) { + public function getDateFormat($field, $format, $string_date = FALSE) { return $field; } diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 0bc5134..ebe0bc9 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -1693,9 +1693,9 @@ public function setupTimezone() { } /** - * Overrides \Drupal\views\Plugin\views\query\QueryPluginBase::getDateFormat(). + * {@inheritdoc} */ - public function getDateFormat($field, $format) { + public function getDateFormat($field, $format, $string_date = FALSE) { $db_type = Database::getConnection()->databaseType(); switch ($db_type) { case 'mysql': @@ -1742,7 +1742,13 @@ public function getDateFormat($field, $format) { 'A' => 'AM', ); $format = strtr($format, $replace); - return "TO_CHAR($field, '$format')"; + if (!$string_date) { + return "TO_CHAR($field, '$format')"; + } + // In order to allow for partials (eg, only the year), transform to a + // date, back to a string again. + // @todo this is very messy, and EXTRACT should probably be used. + return "TO_CHAR(TO_TIMESTAMP($field, 'YYYY-MM-DD HH24:MI:SS'), '$format')"; case 'sqlite': $replace = array( 'Y' => '%Y',