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 @@
*/
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;
}
uuid: 87dc4221-8d56-4112-8a7f-7a855ac35d08
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_range
- node.type.page
module:
- datetime_range
id: node.page.field_range
field_name: field_range
entity_type: node
bundle: page
label: range
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: daterange
uuid: 2190ad8c-39dd-4eb1-b189-1bfc0c244a40
langcode: en
status: true
dependencies:
module:
- datetime_range
- node
id: node.field_range
field_name: field_range
entity_type: node
type: daterange
settings:
datetime_type: datetime
module: datetime_range
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
uuid: d20760b6-7cc4-4844-ae04-96da7225a46f
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_datetime_range_filter_values
label: test_datetime_range_filter_values
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
title:
id: title
table: node_field_data
field: title
entity_type: node
entity_field: title
label: ''
alter:
alter_text: false
make_link: false
absolute: false
trim: false
word_boundary: false
ellipsis: false
strip_tags: false
html: false
hide_empty: false
empty_zero: false
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
exclude: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
field_range_value:
id: field_range_value
table: node__field_range
field: field_range_value
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '2017'
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''
required: false
remember: false
multiple: false
remember_roles:
authenticated: authenticated
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: string
field_range_end_value:
id: field_range_end_value
table: node__field_range
field: field_range_end_value
relationship: none
group_type: group
admin_label: ''
operator: contains
value: ''
group: 1
exposed: false
expose:
operator_id: ''
label: ''
description: ''
use_operator: false
operator: ''
identifier: ''