Commit f30938d3 authored by catch's avatar catch

Issue #2786577 by gambry, vaplas, j1n, adriancid, jibran, artis, diamondsea,...

Issue #2786577 by gambry, vaplas, j1n, adriancid, jibran, artis, diamondsea, mpdonadio, jhedstrom, Darvanen, bkosborne, xjm, larowlan, dkre, Lendude, effulgentsia, dawehner, alexpott: The Views integration Datetime Range fields should extend the views integration for regular Datetime fields
parent d2a14db5
......@@ -11,18 +11,42 @@
* Implements hook_field_views_data().
*/
function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
return datetime_type_field_views_data_helper($field_storage, [], $field_storage->getMainPropertyName());
}
/**
* Provides Views integration for any datetime-based fields.
*
* Overrides the default Views data for datetime-based fields, adding datetime
* views plugins. Modules defining new datetime-based fields may use this
* function to simplify Views integration.
*
* @param \Drupal\field\FieldStorageConfigInterface $field_storage
* The field storage config entity.
* @param array $data
* Field view data or views_field_default_views_data($field_storage) if empty.
* @param string $column_name
* The schema column name with the datetime value.
*
* @return array
* The array of field views data with the datetime plugin.
*
* @see datetime_field_views_data()
* @see datetime_range_field_views_data()
*/
function datetime_type_field_views_data_helper(FieldStorageConfigInterface $field_storage, array $data, $column_name) {
// @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);
$data = empty($data) ? views_field_default_views_data($field_storage) : $data;
foreach ($data as $table_name => $table_data) {
// Set the 'datetime' filter type.
$data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime';
$data[$table_name][$field_storage->getName() . '_' . $column_name]['filter']['id'] = 'datetime';
// Set the 'datetime' argument type.
$data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime';
$data[$table_name][$field_storage->getName() . '_' . $column_name]['argument']['id'] = 'datetime';
// Create year, month, and day arguments.
$group = $data[$table_name][$field_storage->getName() . '_value']['group'];
$group = $data[$table_name][$field_storage->getName() . '_' . $column_name]['group'];
$arguments = [
// Argument type => help text.
'year' => t('Date in the form of YYYY.'),
......@@ -33,11 +57,16 @@ function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
'full_date' => t('Date in the form of CCYYMMDD.'),
];
foreach ($arguments as $argument_type => $help_text) {
$data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = [
'title' => $field_storage->getLabel() . ' (' . $argument_type . ')',
$column_name_text = $column_name === $field_storage->getMainPropertyName() ? '' : ':' . $column_name;
$data[$table_name][$field_storage->getName() . '_' . $column_name . '_' . $argument_type] = [
'title' => t('@label@column (@argument)', [
'@label' => $field_storage->getLabel(),
'@column' => $column_name_text,
'@argument' => $argument_type,
]),
'help' => $help_text,
'argument' => [
'field' => $field_storage->getName() . '_value',
'field' => $field_storage->getName() . '_' . $column_name,
'id' => 'datetime_' . $argument_type,
'entity_type' => $field_storage->getTargetEntityTypeId(),
'field_name' => $field_storage->getName(),
......@@ -47,7 +76,7 @@ function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
}
// Set the 'datetime' sort handler.
$data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime';
$data[$table_name][$field_storage->getName() . '_' . $column_name]['sort']['id'] = 'datetime';
}
return $data;
......
......@@ -2,7 +2,9 @@
namespace Drupal\Tests\datetime\Kernel\Views;
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
......@@ -28,6 +30,13 @@ abstract class DateTimeHandlerTestBase extends ViewsKernelTestBase {
*/
protected static $field_name = 'field_date';
/**
* Type of the field.
*
* @var string
*/
protected static $field_type = 'datetime';
/**
* Nodes to test.
*
......@@ -54,7 +63,7 @@ protected function setUp($import_test_views = TRUE) {
$fieldStorage = FieldStorageConfig::create([
'field_name' => static::$field_name,
'entity_type' => 'node',
'type' => 'datetime',
'type' => static::$field_type,
'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
]);
$fieldStorage->save();
......@@ -91,4 +100,42 @@ protected function setSiteTimezone($timezone) {
->save();
}
/**
* Returns UTC timestamp of user's TZ 'now'.
*
* The date field stores date_only values without conversion, considering them
* already as UTC. This method returns the UTC equivalent of user's 'now' as a
* unix timestamp, so they match using Y-m-d format.
*
* @return int
* Unix timestamp.
*/
protected function getUTCEquivalentOfUserNowAsTimestamp() {
$user_now = new DateTimePlus('now', new \DateTimeZone(drupal_get_user_timezone()));
$utc_equivalent = new DateTimePlus($user_now->format('Y-m-d H:i:s'), new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
return $utc_equivalent->getTimestamp();
}
/**
* Returns an array formatted date_only values relative to timestamp.
*
* @param int $timestamp
* Unix Timestamp used as 'today'.
*
* @return array
* An array of DateTimeItemInterface::DATE_STORAGE_FORMAT date values. In
* order tomorrow, today and yesterday.
*/
protected function getRelativeDateValuesFromTimestamp($timestamp) {
return [
// Tomorrow.
\Drupal::service('date.formatter')->format($timestamp + 86400, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE),
// Today.
\Drupal::service('date.formatter')->format($timestamp, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE),
// Yesterday.
\Drupal::service('date.formatter')->format($timestamp - 86400, 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE),
];
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\Tests\datetime\Kernel\Views;
use Drupal\Component\Datetime\DateTimePlus;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\Node;
......@@ -182,44 +181,6 @@ public function testDateIs() {
}
}
/**
* Returns UTC timestamp of user's TZ 'now'.
*
* The date field stores date_only values without conversion, considering them
* already as UTC. This method returns the UTC equivalent of user's 'now' as a
* unix timestamp, so they match using Y-m-d format.
*
* @return int
* Unix timestamp.
*/
protected function getUTCEquivalentOfUserNowAsTimestamp() {
$user_now = new DateTimePlus('now', new \DateTimeZone(drupal_get_user_timezone()));
$utc_equivalent = new DateTimePlus($user_now->format('Y-m-d H:i:s'), new \DateTimeZone(DATETIME_STORAGE_TIMEZONE));
return $utc_equivalent->getTimestamp();
}
/**
* Returns an array formatted date_only values.
*
* @param int $timestamp
* Unix Timestamp equivalent to user's "now".
*
* @return array
* An array of DATETIME_DATE_STORAGE_FORMAT date values. In order tomorrow,
* today and yesterday.
*/
protected function getRelativeDateValuesFromTimestamp($timestamp) {
return [
// Tomorrow.
\Drupal::service('date.formatter')->format($timestamp + 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
// Today.
\Drupal::service('date.formatter')->format($timestamp, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
// Yesterday.
\Drupal::service('date.formatter')->format($timestamp - 86400, 'custom', DATETIME_DATE_STORAGE_FORMAT, DATETIME_STORAGE_TIMEZONE),
];
}
/**
* Updates tests nodes date fields values.
*
......
......@@ -6,6 +6,8 @@
*/
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\Views;
use Drupal\views\ViewEntityInterface;
/**
* Implements hook_help().
......@@ -26,3 +28,133 @@ function datetime_range_help($route_name, RouteMatchInterface $route_match) {
return $output;
}
}
/**
* Implements hook_view_presave().
*
* When a view is saved using the old string or standard plugin format for
* Datetime Range filters or sorts, they will automatically be updated to
* Datetime filters or sorts. Old plugins usage must to be considered
* deprecated and must be converted before 9.0.0, when this updating layer will
* be removed.
*
* @deprecated in Drupal 8.5.x and will be removed before 9.0.0.
*
* @see https://www.drupal.org/node/2857691
*/
function datetime_range_view_presave(ViewEntityInterface $view) {
$config_factory = \Drupal::configFactory();
$displays = $view->get('display');
$changed = FALSE;
foreach ($displays as $display_name => &$display) {
// Update datetime_range filters.
if (isset($display['display_options']['filters'])) {
foreach ($display['display_options']['filters'] as $field_name => &$filter) {
if ($filter['plugin_id'] === 'string') {
// Get field config.
$filter_views_data = Views::viewsData()->get($filter['table'])[$filter['field']]['filter'];
if (!isset($filter_views_data['entity_type']) || !isset($filter_views_data['field_name'])) {
continue;
}
$field_storage_name = 'field.storage.' . $filter_views_data['entity_type'] . '.' . $filter_views_data['field_name'];
$field_configuration = $config_factory->get($field_storage_name);
if ($field_configuration->get('type') === 'daterange') {
// Set entity_type if missing.
if (!isset($filter['entity_type'])) {
$filter['entity_type'] = $filter_views_data['entity_type'];
}
// Set datetime plugin_id.
$filter['plugin_id'] = 'datetime';
// Create datetime value array.
$datetime_value = [
'min' => '',
'max' => '',
'value' => $filter['value'],
'type' => 'date',
];
// Map string operator/value to numeric equivalent.
switch ($filter['operator']) {
case '=':
case 'empty':
case 'not empty':
$operator = $filter['operator'];
break;
case '!=':
case 'not':
$operator = '!=';
break;
case 'starts':
$operator = 'regular_expression';
$datetime_value['value'] = '^' . preg_quote($datetime_value['value']);
break;
case 'ends':
$operator = 'regular_expression';
$datetime_value['value'] = preg_quote($datetime_value['value']) . '$';
break;
default:
$operator = 'regular_expression';
// Add .* to prevent blank regexes.
if (empty($datetime_value['value'])) {
$datetime_value['value'] = '.*';
}
else {
$datetime_value['value'] = preg_quote($datetime_value['value']);
}
}
// Set value and operator.
$filter['value'] = $datetime_value;
$filter['operator'] = $operator;
$changed = TRUE;
@trigger_error('Use of string filters for datetime_range fields is deprecated. Use the datetime filters instead. See https://www.drupal.org/node/2857691', E_USER_DEPRECATED);
}
}
}
}
// Update datetime_range sort handlers.
if (isset($display['display_options']['sorts'])) {
foreach ($display['display_options']['sorts'] as $field_name => &$sort) {
if ($sort['plugin_id'] === 'standard') {
// Get field config.
$sort_views_data = Views::viewsData()->get($sort['table'])[$sort['field']]['sort'];
if (!isset($sort_views_data['entity_type']) || !isset($sort_views_data['field_name'])) {
continue;
}
$field_storage_name = 'field.storage.' . $sort_views_data['entity_type'] . '.' . $sort_views_data['field_name'];
$field_configuration = $config_factory->get($field_storage_name);
if ($field_configuration->get('type') === 'daterange') {
// Set entity_type if missing.
if (!isset($sort['entity_type'])) {
$sort['entity_type'] = $sort_views_data['entity_type'];
}
// Set datetime plugin_id.
$sort['plugin_id'] = 'datetime';
$changed = TRUE;
@trigger_error('Use of standard sort handlers for datetime_range fields is deprecated. Use the datetime sort handlers instead. See https://www.drupal.org/node/2857691', E_USER_DEPRECATED);
}
}
}
}
}
if ($changed) {
$view->set('display', $displays);
}
}
......@@ -5,9 +5,88 @@
* Post-update functions for Datetime Range module.
*/
use Drupal\views\Views;
/**
* Clear caches to ensure schema changes are read.
*/
function datetime_range_post_update_translatable_separator() {
// Empty post-update hook to cause a cache rebuild.
}
/**
* Update existing views using datetime_range fields.
*/
function datetime_range_post_update_views_string_plugin_id() {
/* @var \Drupal\views\Entity\View[] $views */
$views = \Drupal::entityTypeManager()->getStorage('view')->loadMultiple();
$config_factory = \Drupal::configFactory();
$message = NULL;
$ids = [];
foreach ($views as $view) {
$displays = $view->get('display');
$needs_bc_layer_update = FALSE;
foreach ($displays as $display_name => $display) {
// Check if datetime_range filters need updates.
if (!$needs_bc_layer_update && isset($display['display_options']['filters'])) {
foreach ($display['display_options']['filters'] as $field_name => $filter) {
if ($filter['plugin_id'] == 'string') {
// Get field config.
$filter_views_data = Views::viewsData()->get($filter['table'])[$filter['field']]['filter'];
if (!isset($filter_views_data['entity_type']) || !isset($filter_views_data['field_name'])) {
continue;
}
$field_storage_name = 'field.storage.' . $filter_views_data['entity_type'] . '.' . $filter_views_data['field_name'];
$field_configuration = $config_factory->get($field_storage_name);
if ($field_configuration->get('type') == 'daterange') {
// Trigger the BC layer control.
$needs_bc_layer_update = TRUE;
continue 2;
}
}
}
}
// Check if datetime_range sort handlers need updates.
if (!$needs_bc_layer_update && isset($display['display_options']['sorts'])) {
foreach ($display['display_options']['sorts'] as $field_name => $sort) {
if ($sort['plugin_id'] == 'standard') {
// Get field config.
$sort_views_data = Views::viewsData()->get($sort['table'])[$sort['field']]['sort'];
if (!isset($sort_views_data['entity_type']) || !isset($sort_views_data['field_name'])) {
continue;
}
$field_storage_name = 'field.storage.' . $sort_views_data['entity_type'] . '.' . $sort_views_data['field_name'];
$field_configuration = $config_factory->get($field_storage_name);
if ($field_configuration->get('type') == 'daterange') {
// Trigger the BC layer control.
$needs_bc_layer_update = TRUE;
continue 2;
}
}
}
}
}
// If current view needs BC layer updates save it and the hook view_presave
// will do the rest.
if ($needs_bc_layer_update) {
$view->save();
$ids[] = $view->id();
}
}
if (!empty($ids)) {
$message = \Drupal::translation()->translate('Updated datetime_range filter/sort plugins for views: @ids', ['@ids' => implode(', ', array_unique($ids))]);
}
return $message;
}
<?php
/**
* @file
* Provides views data for the datetime_range module.
*/
use Drupal\field\FieldStorageConfigInterface;
/**
* Implements hook_field_views_data().
*/
function datetime_range_field_views_data(FieldStorageConfigInterface $field_storage) {
// Include datetime.views.inc file in order for helper function
// datetime_type_field_views_data_helper() to be available.
\Drupal::moduleHandler()->loadInclude('datetime', 'inc', 'datetime.views');
// Get datetime field data for value and end_value.
$data = datetime_type_field_views_data_helper($field_storage, [], 'value');
$data = datetime_type_field_views_data_helper($field_storage, $data, 'end_value');
return $data;
}
<?php
// @codingStandardsIgnoreFile
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of https://www.drupal.org/node/2786577.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
// Configuration for an datetime_range field storage.
$field_storage_datetime_range = Yaml::decode(file_get_contents(__DIR__ . '/field.storage.node.field_range.yml'));
// Configuration for a datetime_range field on 'page' node bundle.
$field_datetime_range = Yaml::decode(file_get_contents(__DIR__ . '/field.field.node.page.field_range.yml'));
// Configuration for a View using datetime_range plugins.
$views_datetime_range = Yaml::decode(file_get_contents(__DIR__ . '/views.view.test_datetime_range_filter_values.yml'));
// Update core.entity_form_display.node.page.default
$data = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_form_display.node.page.default')
->execute()
->fetchField();
$data = unserialize($data);
$data['dependencies']['config'][] = 'field.field.' . $field_datetime_range['id'];
$data['dependencies']['module'][] = 'datetime_range';
$data['content'][$field_datetime_range['field_name']] = array(
"weight"=> 27,
"settings" => array(),
"third_party_settings" => array(),
"type" => "daterange_default",
"region" => "content"
);
$connection->update('config')
->fields([
'data' => serialize($data),
])
->condition('collection', '')
->condition('name', 'core.entity_form_display.node.page.default')
->execute();
// Update core.entity_view_display.node.page.default
$data = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.page.default')
->execute()
->fetchField();
$data = unserialize($data);
$data['dependencies']['config'][] = 'field.field.' . $field_datetime_range['id'];
$data['dependencies']['module'][] = 'datetime_range';
$data['content'][$field_datetime_range['field_name']] = array(
"weight"=> 102,
"label"=> "above",
"settings" => array("separator"=> "-", "format_type" => "medium", "timezone_override" => ""),
"third_party_settings" => array(),
"type" => "daterange_default",
"region" => "content"
);
$connection->update('config')
->fields([
'data' => serialize($data),
])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.page.default')
->execute();
$connection->insert('config')
->fields(array(
'collection',
'name',
'data',
))
->values(array(
'collection' => '',
'name' => 'field.field.' . $field_datetime_range['id'],
'data' => serialize($field_datetime_range),
))
->values(array(
'collection' => '',
'name' => 'field.storage.' . $field_storage_datetime_range['id'],
'data' => serialize($field_storage_datetime_range),
))
->values(array(
'collection' => '',
'name' => 'views.view.' . $views_datetime_range['id'],
'data' => serialize($views_datetime_range),
))
->execute();
// Update core.extension.
$extensions = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.extension')
->execute()
->fetchField();
$extensions = unserialize($extensions);
$extensions['module']['datetime_range'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
$connection->insert('key_value')
->fields(array(
'collection',
'name',
'value',
))
->values(array(
'collection' => 'config.entity.key_store.field_config',
'name' => 'uuid:87dc4221-8d56-4112-8a7f-7a855ac35d08',
'value' => 'a:1:{i:0;s:33:"field.field.' . $field_datetime_range['id'] . '";}',
))
->values(array(