Commit 4a4ee058 authored by alexpott's avatar alexpott

Issue #1838242 by jhedstrom, pivica, tim.plunkett, GaëlG, dawehner, olli,...

Issue #1838242 by jhedstrom, pivica, tim.plunkett, GaëlG, dawehner, olli, Lendude: Provide Views integration for datetime field
parent a108bf0f
<?php
/**
* @file
* Provides views data for the datetime module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_views_data().
*/
function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
// @todo This code only covers configurable fields, handle base table fields
// in https://www.drupal.org/node/2489476.
$data = views_field_default_views_data($field_storage);
foreach ($data as $table_name => $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;
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\Argument\Date.
*/
namespace Drupal\datetime\Plugin\views\Argument;
use Drupal\views\Plugin\views\argument\Date as NumericDate;
/**
* Abstract argument handler for dates.
*
* Adds an option to set a default argument based on the current date.
*
* Definitions terms:
* - many to one: If true, the "many to one" helper will be used.
* - invalid input: A string to give to the user for obviously invalid input.
* This is deprecated in favor of argument validators.
*
* @see \Drupal\views\ManyTonOneHelper
*
* @ingroup views_argument_handlers
*
* @ViewsArgument("datetime")
*/
class Date extends NumericDate {
/**
* {@inheritdoc}
*/
public function getDateField() {
// Return the real field, since it is already in string format.
return "$this->tableAlias.$this->realField";
}
/**
* {@inheritdoc}
*/
public function getDateFormat($format) {
// Pass in the string-field option.
return $this->query->getDateFormat($this->getDateField(), $format, TRUE);
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\argument\DayDate.
*/
namespace Drupal\datetime\Plugin\views\argument;
/**
* Argument handler for a day.
*
* @ViewsArgument("datetime_day")
*/
class DayDate extends Date {
/**
* {@inheritdoc}
*/
protected $argFormat = 'd';
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\argument\MonthDate.
*/
namespace Drupal\datetime\Plugin\views\argument;
/**
* Argument handler for a month.
*
* @ViewsArgument("datetime_month")
*/
class MonthDate extends Date {
/**
* {@inheritdoc}
*/
protected $argFormat = 'm';
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\argument\YearDate.
*/
namespace Drupal\datetime\Plugin\views\argument;
/**
* Argument handler for a year.
*
* @ViewsArgument("datetime_year")
*/
class YearDate extends Date {
/**
* {@inheritdoc}
*/
protected $argFormat = 'Y';
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\filter\String.
*/
namespace Drupal\datetime\Plugin\views\filter;
use Drupal\Core\Datetime\DateFormatter;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\Plugin\views\filter\Date as NumericDate;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Date/time views filter.
*
* Even thought dates are stored as strings, the numeric filter is extended
* because it provides more sensible operators.
*
* @ingroup views_filter_handlers
*
* @ViewsFilter("datetime")
*/
class Date extends NumericDate implements ContainerFactoryPluginInterface {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatter
*/
protected $dateFormatter;
/**
* Constructs a new Date handler.
*
* @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\Datetime\DateFormatter $date_formatter
* The date formatter service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DateFormatter $date_formatter) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$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('date.formatter')
);
}
/**
* Date format for SQL conversion.
*
* @var string
*
* @see \Drupal\views\Plugin\views\query\Sql::getDateFormat()
*/
protected static $dateFormat = 'Y-m-d H:i:s';
/**
* Override parent method, which deals with dates as integers.
*/
protected function opBetween($field) {
$origin = ($this->value['type'] == 'offset') ? REQUEST_TIME : 0;
$a = intval(strtotime($this->value['min'], $origin));
$b = intval(strtotime($this->value['max'], $origin));
// Convert to ISO format and format for query. UTC timezone is used since
// dates are stored in UTC.
$a = $this->query->getDateFormat("'" . $this->dateFormatter->format($a, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", static::$dateFormat, TRUE);
$b = $this->query->getDateFormat("'" . $this->dateFormatter->format($b, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", 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) {
$origin = (!empty($this->value['type']) && $this->value['type'] == 'offset') ? REQUEST_TIME : 0;
$value = intval(strtotime($this->value['value'], $origin));
// Convert to ISO. UTC is used since dates are stored in UTC.
$value = $this->query->getDateFormat("'" . $this->dateFormatter->format($value, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE) . "'", 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");
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Plugin\views\sort\Date.
*/
namespace Drupal\datetime\Plugin\views\sort;
use Drupal\views\Plugin\views\sort\Date as NumericDate;
/**
* Basic sort handler for datetime fields.
*
* This handler enables granularity, which is the ability to make dates
* equivalent based upon nearness.
*
* @ViewsSort("datetime")
*/
class Date extends NumericDate {
/**
* Override to account for dates stored as strings.
*/
public function getDateField() {
// Return the real field, since it is already in string format.
return "$this->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();
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Tests\Views\ArgumentDateTimeTest.
*/
namespace Drupal\datetime\Tests\Views;
use Drupal\views\Views;
/**
* Tests the Drupal\datetime\Plugin\views\filter\Date handler.
*
* @group datetime
*/
class ArgumentDateTimeTest extends DateTimeHandlerTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = array('test_argument_datetime');
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Add some basic test nodes.
$dates = array(
'2000-10-10',
'2001-10-10',
'2002-01-01',
);
foreach ($dates as $date) {
$this->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();
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Tests\Views\DateTimeHandlerTestBase.
*/
namespace Drupal\datetime\Tests\Views;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\views\Tests\Handler\HandlerTestBase;
use Drupal\views\Tests\ViewTestData;
/**
* Base class for testing datetime handlers.
*/
abstract class DateTimeHandlerTestBase extends HandlerTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('datetime_test', 'node', 'datetime');
/**
* Name of the field.
*
* Note, this is used in the default test view.
*
* @var string
*/
protected static $field_name = 'field_date';
/**
* Nodes to test.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $nodes = array();
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Add a date field to page nodes.
$node_type = entity_create('node_type', array(
'type' => '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' => DateTimeItem::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'));
}
}
<?php
/**
* @file
* Contains \Drupal\datetime\Tests\Views\FilterDateTimeTest.
*/
namespace Drupal\datetime\Tests\Views;
use Drupal\Core\Datetime\Element\Datetime;
use Drupal\views\Views;
/**
* Tests the Drupal\datetime\Plugin\views\filter\Date handler.
*
* @group datetime
*/
class FilterDateTimeTest extends DateTimeHandlerTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = array('test_filter_datetime');
/**
* For offset tests, set a date 1 day in the future.
*/
protected static $date;
/**
* Use a non-UTC timezone.
*/
protected static $timezone = 'America/Vancouver';
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
static::$date = REQUEST_TIME + 86400;
// Set the timezone.
date_default_timezone_set(static::$timezone);
// Add some basic test nodes.
$dates = array(
'2000-10-10',
'2001-10-10',
'2002-10-10',
// The date storage timezone is used (this mimics the steps taken in the
// widget: \Drupal\datetime\Plugin\Field\FieldWidget::messageFormValues().
\Drupal::service('date.formatter')->format(static::$date, 'custom', DATETIME_DATETIME_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
);
foreach ($dates as $date) {
$this->nodes[] = $this->drupalCreateNode(array(
'field_date' => array(
'value' => $date,
)
));
}
}
/**
* Test filter operations.
*/
public function testDatetimeFilter() {
$this->_testOffset();
$this->_testBetween();
$this->_testExact();
}
/**
* 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';