Last active
July 22, 2025 17:31
-
-
Save junaidpv/324c14aa89670c9de82d1721b53f7bb4 to your computer and use it in GitHub Desktop.
Modified version of https://www.drupal.org/project/drupal/issues/2868014#comment-16201906 to apply on top of https://gist.github.com/junaidpv/06fa01aab594afcdc38f30635c9641a1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/core/modules/datetime/src/Plugin/views/filter/Date.php b/core/modules/datetime/src/Plugin/views/filter/Date.php | |
index 39cb6d931..0c7d3b91c 100644 | |
--- a/core/modules/datetime/src/Plugin/views/filter/Date.php | |
+++ b/core/modules/datetime/src/Plugin/views/filter/Date.php | |
@@ -58,6 +58,20 @@ class Date extends NumericDate implements ContainerFactoryPluginInterface { | |
*/ | |
protected $requestStack; | |
+ /** | |
+ * Mapping of granularity values to their corresponding date formats. | |
+ * | |
+ * @var array | |
+ */ | |
+ protected $dateFormats = [ | |
+ 'second' => 'Y-m-d\TH:i:s', | |
+ 'minute' => 'Y-m-d\TH:i', | |
+ 'hour' => 'Y-m-d\TH', | |
+ 'day' => 'Y-m-d', | |
+ 'month' => 'Y-m', | |
+ 'year' => 'Y', | |
+ ]; | |
+ | |
/** | |
* Constructs a new Date handler. | |
* | |
@@ -149,6 +163,61 @@ public function validateExposed(&$form, FormStateInterface $form_state): void { | |
} | |
} | |
+ /** | |
+ * {@inheritdoc} | |
+ */ | |
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) { | |
+ parent::buildOptionsForm($form, $form_state); | |
+ | |
+ $options = []; | |
+ // Only show granularity options for times if the field supports a time. | |
+ if ($this->fieldStorageDefinition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATETIME) { | |
+ $options = [ | |
+ 'second' => $this->t('Second'), | |
+ 'minute' => $this->t('Minute'), | |
+ 'hour' => $this->t('Hour'), | |
+ ]; | |
+ } | |
+ // All fields (datetime or date-only) will need these options. | |
+ $options['day'] = $this->t('Day'); | |
+ $options['month'] = $this->t('Month'); | |
+ $options['year'] = $this->t('Year'); | |
+ | |
+ $form['granularity'] = [ | |
+ '#type' => 'radios', | |
+ '#title' => $this->t('Granularity'), | |
+ '#options' => $options, | |
+ '#description' => $this->t('The granularity is the smallest unit to use when determining whether two dates are the same; for example, if the granularity is "Year" then all dates in 1999, regardless of when they fall in 1999, will be considered the same date.'), | |
+ '#default_value' => $this->options['granularity'], | |
+ ]; | |
+ } | |
+ | |
+ /** | |
+ * {@inheritdoc} | |
+ */ | |
+ protected function defineOptions() { | |
+ $options = parent::defineOptions(); | |
+ | |
+ $options['granularity'] = [ | |
+ // The default depends on if the field is date-only or includes time. | |
+ 'default' => $this->fieldStorageDefinition->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATETIME ? 'second' : 'day', | |
+ ]; | |
+ | |
+ return $options; | |
+ } | |
+ | |
+ /** | |
+ * {@inheritdoc} | |
+ */ | |
+ public function query() { | |
+ // Set the date format based on granularity. | |
+ if (isset($this->dateFormats[$this->options['granularity']])) { | |
+ $this->dateFormat = $this->dateFormats[$this->options['granularity']]; | |
+ } | |
+ | |
+ parent::query(); | |
+ } | |
+ | |
/** | |
* Override parent method, which deals with dates as integers. | |
*/ | |
@@ -165,12 +234,42 @@ protected function opBetween($field) { | |
$this->value['max'] .= ' +1 day'; | |
} | |
+ $max = $this->value['max']; | |
+ | |
+ // If year granularity is specified, then suffix with month and day to | |
+ // force DateTimePlus to treat the input as a proper date value. | |
+ if ($this->options['granularity'] == 'year') { | |
+ $min = preg_replace('/^(\d{4})$/', '$1-01-01', $min); | |
+ $max = preg_replace('/^(\d{4})$/', '$1-01-01', $max); | |
+ } | |
+ | |
// Convert to ISO format and format for query. UTC timezone is used since | |
// dates are stored in UTC. | |
$a = new DateTimePlus($min, new \DateTimeZone($timezone)); | |
- $a = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($a->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); | |
- $b = new DateTimePlus($this->value['max'], new \DateTimeZone($timezone)); | |
- $b = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($b->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); | |
+ $b = new DateTimePlus($max, new \DateTimeZone($timezone)); | |
+ | |
+ // If hour/minute/second granularity is specified, then consider timezone | |
+ // offset when building the query value. | |
+ if (in_array($this->options['granularity'], ['hour', 'minute', 'second'])) { | |
+ $a_timestamp = $a->getTimestamp() + $origin_offset; | |
+ $b_timestamp = $b->getTimestamp() + $origin_offset; | |
+ $calculate_offset = $this->calculateOffset; | |
+ } | |
+ // Otherwise, do not apply any timezone offset, because day/month/year | |
+ // granularity will effectively cause the provided datetime to have a | |
+ // "midnight" hour value, and further adjustments could cause the query to | |
+ // return results from one day prior to the expected day. | |
+ // The query will still apply any applicable timezone offset to the values | |
+ // of the datetime field in the database before performing a comparison | |
+ // (see below). | |
+ else { | |
+ $a_timestamp = $a->getTimestamp(); | |
+ $b_timestamp = $b->getTimestamp(); | |
+ $calculate_offset = FALSE; | |
+ } | |
+ | |
+ $a = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($a_timestamp, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $calculate_offset), $this->dateFormat, TRUE); | |
+ $b = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($b_timestamp, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $calculate_offset), $this->dateFormat, TRUE); | |
// This is safe because we are manually scrubbing the values. | |
$operator = strtoupper($this->operator); | |
@@ -185,9 +284,34 @@ protected function opSimple($field) { | |
$timezone = $this->getTimezone(); | |
$origin_offset = $this->getOffset($this->value['value'], $timezone); | |
+ // If year granularity is specified, then suffix with month and day to | |
+ // force DateTimePlus to treat the input as a proper date value. | |
+ $date_value = $this->value['value']; | |
+ if ($this->options['granularity'] == 'year') { | |
+ $date_value = preg_replace('/^(\d{4})$/', '$1-01-01', $date_value); | |
+ } | |
+ | |
// Convert to ISO. UTC timezone is used since dates are stored in UTC. | |
- $value = new DateTimePlus($this->value['value'], new \DateTimeZone($timezone)); | |
- $value = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($value->getTimestamp() + $origin_offset, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $this->calculateOffset), $this->dateFormat, TRUE); | |
+ $value = new DateTimePlus($date_value, new \DateTimeZone($timezone)); | |
+ // If hour/minute/second granularity is specified, then consider timezone | |
+ // offset when building the query value. | |
+ if (in_array($this->options['granularity'], ['hour', 'minute', 'second'])) { | |
+ $timestamp = $value->getTimestamp() + $origin_offset; | |
+ $calculate_offset = $this->calculateOffset; | |
+ } | |
+ // Otherwise, do not apply any timezone offset, because day/month/year | |
+ // granularity will effectively cause the provided datetime to have a | |
+ // "midnight" hour value, and further adjustments could cause the query to | |
+ // return results from one day prior to the expected day. | |
+ // The query will still apply any applicable timezone offset to the values | |
+ // of the datetime field in the database before performing a comparison | |
+ // (see below). | |
+ else { | |
+ $timestamp = $value->getTimestamp(); | |
+ $calculate_offset = FALSE; | |
+ } | |
+ | |
+ $value = $this->query->getDateFormat($this->query->getDateField("'" . $this->dateFormatter->format($timestamp, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, DateTimeItemInterface::STORAGE_TIMEZONE) . "'", TRUE, $calculate_offset), $this->dateFormat, TRUE); | |
// This is safe because we are manually scrubbing the value. | |
$field = $this->query->getDateFormat($this->query->getDateField($field, TRUE, $this->calculateOffset), $this->dateFormat, TRUE); | |
diff --git a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php | |
index 4d4997b2b..a9efead82 100644 | |
--- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php | |
+++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTest.php | |
@@ -112,7 +112,7 @@ public function testDateOffsets(): void { | |
['nid' => $this->nodes[0]->id()], | |
['nid' => $this->nodes[1]->id()], | |
]; | |
- $this->assertIdenticalResultset($view, $expected_result, $this->map); | |
+ $this->assertIdenticalResultset($view, $expected_result, $this->map, "Unexpected result set for $timezone"); | |
$view->destroy(); | |
// Only dates in the past. | |
@@ -125,7 +125,7 @@ public function testDateOffsets(): void { | |
$expected_result = [ | |
['nid' => $this->nodes[2]->id()], | |
]; | |
- $this->assertIdenticalResultset($view, $expected_result, $this->map); | |
+ $this->assertIdenticalResultset($view, $expected_result, $this->map, "Unexpected result set for $timezone"); | |
$view->destroy(); | |
// Test offset for between operator. Only 'tomorrow' node should appear. | |
@@ -139,7 +139,7 @@ public function testDateOffsets(): void { | |
$expected_result = [ | |
['nid' => $this->nodes[0]->id()], | |
]; | |
- $this->assertIdenticalResultset($view, $expected_result, $this->map); | |
+ $this->assertIdenticalResultset($view, $expected_result, $this->map, "Unexpected result set for $timezone"); | |
$view->destroy(); | |
// Test the empty operator. | |
@@ -150,7 +150,7 @@ public function testDateOffsets(): void { | |
$expected_result = [ | |
['nid' => $this->nodes[3]->id()], | |
]; | |
- $this->assertIdenticalResultset($view, $expected_result, $this->map); | |
+ $this->assertIdenticalResultset($view, $expected_result, $this->map, "Unexpected result set for $timezone"); | |
$view->destroy(); | |
// Test the not empty operator. | |
@@ -163,7 +163,7 @@ public function testDateOffsets(): void { | |
['nid' => $this->nodes[1]->id()], | |
['nid' => $this->nodes[2]->id()], | |
]; | |
- $this->assertIdenticalResultset($view, $expected_result, $this->map); | |
+ $this->assertIdenticalResultset($view, $expected_result, $this->map, "Unexpected result set for $timezone"); | |
$view->destroy(); | |
} | |
} | |
diff --git a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php | |
index 5861acf8c..a791922e7 100644 | |
--- a/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php | |
+++ b/core/modules/datetime/tests/src/Kernel/Views/FilterDateTimeTest.php | |
@@ -189,6 +189,7 @@ protected function _testExact() { | |
$view->filter[$field]->operator = '='; | |
$view->filter[$field]->value['min'] = ''; | |
$view->filter[$field]->value['max'] = ''; | |
+ $view->filter[$field]->options['granularity'] = 'second'; | |
// Use the date from node 3. Use the site timezone (mimics a value entered | |
// through the UI). | |
$view->filter[$field]->value['value'] = \Drupal::service('date.formatter')->format(static::$date, 'custom', DateTimeItemInterface::DATETIME_STORAGE_FORMAT, static::$timezone); | |
diff --git a/core/modules/views/config/schema/views.filter.schema.yml b/core/modules/views/config/schema/views.filter.schema.yml | |
index 6eb22b82f..11fb9947b 100644 | |
--- a/core/modules/views/config/schema/views.filter.schema.yml | |
+++ b/core/modules/views/config/schema/views.filter.schema.yml | |
@@ -202,6 +202,9 @@ views.filter.date: | |
type: | |
type: string | |
label: 'Type' | |
+ granularity: | |
+ type: string | |
+ label: 'Granularity' | |
views.filter_value.in_operator: | |
type: sequence |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment