From 46e6974d65d5d2b4f420492f378b509150f8e469 Mon Sep 17 00:00:00 2001
From: Bastien RIGON <bastien.rigon@smile.fr>
Date: Thu, 16 Jun 2022 10:06:41 +0200
Subject: [PATCH 1/3] Issue 2794481 - Allow en date to be optional. Change the
 "isEmpty" function because it did not save the field value if start_date was
 empty but end_date wasn't.


  25.0% core/modules/datetime_range/src/Plugin/Field/FieldType/
   6.8% core/modules/datetime_range/src/Plugin/Field/FieldWidget/
   5.9% core/modules/datetime_range/src/
  37.0% core/modules/datetime_range/tests/src/Kernel/
  25.2% core/modules/datetime_range/
diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
index f0f93259d3..f4765d37d5 100644
--- a/core/modules/datetime_range/config/schema/datetime_range.schema.yml
+++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
@@ -5,6 +5,10 @@
 field.storage_settings.daterange:
   type: field.storage_settings.datetime
   label: 'Date range settings'
+  mapping:
+    optional_end_date:
+      type: boolean
+      label: 'Optional end date'
 
 field.field_settings.daterange:
   type: field.field_settings.datetime
diff --git a/core/modules/datetime_range/datetime_range.install b/core/modules/datetime_range/datetime_range.install
new file mode 100644
index 0000000000..074d1cd9a5
--- /dev/null
+++ b/core/modules/datetime_range/datetime_range.install
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Datetime range module.
+ */
+
+/**
+ * Allow NULL in the field end_value column.
+ */
+function datetime_range_update_8900() {
+  $database_schema = \Drupal::database()->schema();
+  $storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
+  $configs = \Drupal::entityTypeManager()
+    ->getStorage('field_storage_config')
+    ->loadByProperties(['type' => 'daterange']);
+
+  foreach ($configs as $config) {
+    $schema_key = $config->getTargetEntityTypeId() . '.field_schema_data.' . $config->getName();
+    $tables = $storage_schema->get($schema_key);
+
+    $field_name = $config->getName() . '_end_value';
+    foreach ($tables as $table_name => &$table) {
+      $table['fields'][$field_name]['not null'] = FALSE;
+      $database_schema->changeField($table_name, $field_name, $field_name, $table['fields'][$field_name]);
+    }
+
+    $storage_schema->set($schema_key, $tables);
+  }
+}
diff --git a/core/modules/datetime_range/src/DateTimeRangeTrait.php b/core/modules/datetime_range/src/DateTimeRangeTrait.php
index 3f05b82189..806e93ff98 100644
--- a/core/modules/datetime_range/src/DateTimeRangeTrait.php
+++ b/core/modules/datetime_range/src/DateTimeRangeTrait.php
@@ -17,13 +17,13 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     $separator = $this->getSetting('separator');
 
     foreach ($items as $delta => $item) {
-      if (!empty($item->start_date) && !empty($item->end_date)) {
+      if (!empty($item->start_date)) {
         /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
         $start_date = $item->start_date;
         /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
         $end_date = $item->end_date;
 
-        if ($start_date->getTimestamp() !== $end_date->getTimestamp()) {
+        if ($end_date !== NULL && $start_date->getTimestamp() !== $end_date->getTimestamp()) {
           $elements[$delta] = [
             'start_date' => $this->buildDateWithIsoAttribute($start_date),
             'separator' => ['#plain_text' => ' ' . $separator . ' '],
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
index 7c34ed127c..f1fa526747 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -24,6 +24,15 @@
  */
 class DateRangeItem extends DateTimeItem {
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultStorageSettings() {
+    return [
+      'optional_end_date' => FALSE,
+    ] + parent::defaultStorageSettings();
+  }
+
   /**
    * Value for the 'datetime_type' setting: store a date and time.
    */
@@ -46,7 +55,7 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
 
     $properties['end_value'] = DataDefinition::create('datetime_iso8601')
       ->setLabel(t('End date value'))
-      ->setRequired(TRUE);
+      ->setRequired(FALSE);
 
     $properties['end_date'] = DataDefinition::create('any')
       ->setLabel(t('Computed end date'))
@@ -83,6 +92,12 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
 
     $element['datetime_type']['#options'][static::DATETIME_TYPE_ALLDAY] = $this->t('All Day');
 
+    $element['optional_end_date'] = [
+      '#type' => 'checkbox',
+      '#title' => t('Optional end date'),
+      '#default_value' => $this->getSetting('optional_end_date'),
+    ];
+
     return $element;
   }
 
@@ -94,7 +109,7 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin
 
     // Just pick a date in the past year. No guidance is provided by this Field
     // type.
-    $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400;
+    $start = \Drupal::time()->getRequestTime() - mt_rand(0, 86400 * 365) - 86400;
     $end = $start + 86400;
     if ($type == static::DATETIME_TYPE_DATETIME) {
       $values['value'] = gmdate(DateTimeItemInterface::DATETIME_STORAGE_FORMAT, $start);
@@ -130,4 +145,28 @@ public function onChange($property_name, $notify = TRUE) {
     parent::onChange($property_name, $notify);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraints() {
+    $constraint_manager = \Drupal::typedDataManager()
+      ->getValidationConstraintManager();
+
+    if (!empty($this->getSetting('optional_end_date'))) {
+      return parent::getConstraints();
+    }
+
+    $label = $this->getFieldDefinition()->getLabel();
+    $constraints[] = $constraint_manager
+      ->create('ComplexData', [
+        'end_value' => [
+          'NotNull' => [
+            'message' => t('The @title end date is required', ['@title' => $label]),
+          ],
+        ],
+      ]);
+
+    return $constraints;
+  }
+
 }
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
index b0f3ea7be7..09bcc80d15 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -19,6 +19,7 @@ class DateRangeWidgetBase extends DateTimeWidgetBase {
    */
   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
     $element = parent::formElement($items, $delta, $element, $form, $form_state);
+    $optional_end_date = $this->getFieldSetting('optional_end_date');
 
     // Wrap all of the select elements with a fieldset.
     $element['#theme_wrappers'][] = 'fieldset';
@@ -27,9 +28,13 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
     $element['value']['#title'] = $this->t('Start date');
 
     $element['end_value'] = [
-      '#title' => $this->t('End date'),
+      '#title' => $optional_end_date ? $this->t('End date (optional)') : $this->t('End date'),
     ] + $element['value'];
 
+    if ($element['#required'] && $optional_end_date) {
+      $element['end_value']['#required'] = FALSE;
+    }
+
     if ($items[$delta]->start_date) {
       /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
       $start_date = $items[$delta]->start_date;
diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php
index 636221f951..4b65dc45ad 100644
--- a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php
+++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php
@@ -139,7 +139,7 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
       unset($normalization[static::$fieldName][0]['end_value']);
       $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
       $response = $this->request($method, $url, $request_options);
-      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: This value should not be null.\n";
+      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: The field_daterange end date is required\n";
       $this->assertResourceErrorResponse(422, $message, $response);
 
       // DX: 422 when 'end_value' data type is incorrect.
diff --git a/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php b/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php
index 9620f5cf8d..0e7aacf899 100644
--- a/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php
+++ b/core/modules/datetime_range/tests/src/Kernel/DateRangeItemTest.php
@@ -103,4 +103,62 @@ public function testDateOnly() {
     $this->assertEquals('12:00:00', $end_date->format('H:i:s'));
   }
 
+  /**
+   * Test optional end date.
+   */
+  public function testOptionalEndDate() {
+    $field_name = $this->fieldStorage->getName();
+
+    $this->fieldStorage->setSettings([
+        'datetime_type' => DateRangeItem::DATETIME_TYPE_DATE,
+        'optional_end_date' => FALSE,
+      ])
+      ->save();
+
+    $value = [
+      'value' => '2016-09-21',
+      'end_value' => NULL,
+    ];
+
+    $end_date_without_start_date_value = [
+      'value' => NULL,
+      'end_value' => '2016-09-23',
+    ];
+
+    // Verify entity without optional_end_date enabled.
+    $entity = EntityTest::create([
+      'name' => $this->randomString(),
+      $field_name => $value,
+    ]);
+    $this->assertNotEquals(count($entity->validate()), 0);
+
+    // Verify entity with the optional_end_date enabled.
+    $this->fieldStorage->setSetting('optional_end_date', TRUE)
+      ->save();
+    $entity = EntityTest::create([
+      'name' => $this->randomString(),
+      $field_name => $value,
+    ]);
+    $this->entityValidateAndSave($entity);
+
+    // Verify changing the date value.
+    $this->assertEquals($entity->{$field_name}->end_value, $value['end_value']);
+
+    // Verify entity with the optional_end_date enabled when setting an
+    // end_date but no start date.
+    $this->fieldStorage->setSetting('optional_end_date', TRUE)
+      ->save();
+    $entity = EntityTest::create([
+      'name' => $this->randomString(),
+      $field_name => $end_date_without_start_date_value,
+    ]);
+    $this->entityValidateAndSave($entity);
+
+    // Verify changing the date and end_date value.
+    $this->assertEquals($entity->{$field_name}->end_value, $value['end_value']);
+    $this->assertEquals($entity->{$field_name}->value, $value['value']);
+  }
+
+
+
 }
-- 
2.25.1


From 55e4fa4944adb90fac27f03ced5ffa64ea3643ed Mon Sep 17 00:00:00 2001
From: Bastien RIGON <bastien.rigon@smile.fr>
Date: Thu, 16 Jun 2022 11:10:41 +0200
Subject: [PATCH 2/3] Issue 2794481 - Allow en date to be optional. Change the
 "isEmpty" function because it did not save the field value if start_date was
 empty but end_date wasn't.


 100.0% core/modules/datetime_range/
diff --git a/core/modules/datetime_range/datetime_range.install b/core/modules/datetime_range/datetime_range.install
index 074d1cd9a5..8f7504c481 100644
--- a/core/modules/datetime_range/datetime_range.install
+++ b/core/modules/datetime_range/datetime_range.install
@@ -28,3 +28,27 @@ function datetime_range_update_8900() {
     $storage_schema->set($schema_key, $tables);
   }
 }
+
+/**
+ * Allow NULL in the field value column, to allow having an end date only.
+ */
+function datetime_range_update_8901() {
+  $database_schema = \Drupal::database()->schema();
+  $storage_schema = \Drupal::keyValue('entity.storage_schema.sql');
+  $configs = \Drupal::entityTypeManager()
+    ->getStorage('field_storage_config')
+    ->loadByProperties(['type' => 'daterange']);
+
+  foreach ($configs as $config) {
+    $schema_key = $config->getTargetEntityTypeId() . '.field_schema_data.' . $config->getName();
+    $tables = $storage_schema->get($schema_key);
+
+    $field_name = $config->getName() . '_value';
+    foreach ($tables as $table_name => &$table) {
+      $table['fields'][$field_name]['not null'] = FALSE;
+      $database_schema->changeField($table_name, $field_name, $field_name, $table['fields'][$field_name]);
+    }
+
+    $storage_schema->set($schema_key, $tables);
+  }
+}
-- 
2.25.1


From 2bf5bef261b349693b9e2cd0cc6f28cfa341835e Mon Sep 17 00:00:00 2001
From: Bastien RIGON <bastien.rigon@smile.fr>
Date: Thu, 16 Jun 2022 11:14:53 +0200
Subject: [PATCH 3/3] Issue 2794481 - Allow en date to be optional. Change the
 "isEmpty" function because it did not save the field value if start_date was
 empty but end_date wasn't.


 100.0% core/modules/datetime_range/src/Plugin/Field/FieldType/
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
index f1fa526747..37a05f7690 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -44,7 +44,7 @@ public static function defaultStorageSettings() {
   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
     $properties['value'] = DataDefinition::create('datetime_iso8601')
       ->setLabel(t('Start date value'))
-      ->setRequired(TRUE);
+      ->setRequired(FALSE);
 
     $properties['start_date'] = DataDefinition::create('any')
       ->setLabel(t('Computed start date'))
-- 
2.25.1

