CIVICRM-1749 Implement UI for timezone in Event management forms
authorFrancis Whittle <francis@agileware.com.au>
Wed, 28 Apr 2021 00:28:57 +0000 (10:28 +1000)
committerFrancis Whittle <francis@agileware.com.au>
Tue, 25 Jan 2022 06:07:09 +0000 (06:07 +0000)
CRM/Core/SelectValues.php
CRM/Event/BAO/Event.php
CRM/Event/Form/ManageEvent/EventInfo.php
CRM/Event/Form/ManageEvent/Registration.php
CRM/Event/Page/ManageEvent.php
CRM/Utils/Date.php
templates/CRM/Event/Form/ManageEvent/EventInfo.tpl
templates/CRM/Event/Form/ManageEvent/Registration.tpl
templates/CRM/Event/Page/ManageEvent.tpl

index 4666f0f9deda7f8a94bd5bf02b4a1b40f35fdbc5..c15c14f66283e3bbb3046c7f74efe8c246eab26b 100644 (file)
@@ -1114,7 +1114,7 @@ class CRM_Core_SelectValues {
   public static function timezone() {
     $tzlist = &Civi::$statics[__CLASS__]['tzlist'];
 
-    if(is_null($tzlist)) {
+    if (is_null($tzlist)) {
       $tzlist = [];
       foreach (timezone_identifiers_list() as $tz) {
         // Actual timezone keys for PHP are mapped to human parts.
index 20534481a0cd726567dbf5732a11899273fc31d7..1047dd8b420bab2c9a1730cd5893e1585f366136 100644 (file)
@@ -15,6 +15,7 @@
  * @copyright CiviCRM LLC https://civicrm.org/licensing
  */
 class CRM_Event_BAO_Event extends CRM_Event_DAO_Event {
+  const tz_fields = ['start_date', 'end_date', 'registration_start_date', 'registration_end_date'];
 
   /**
    * Fetch object based on array of properties.
@@ -2434,4 +2435,26 @@ LEFT  JOIN  civicrm_price_field_value value ON ( value.id = lineItem.price_field
     return $return;
   }
 
+  public static function setTimezones(CRM_Event_DAO_Event $event) {
+    // Pre-process time zoned fields into the PHP time zone, which should be the same as the database, to save as timestamp.
+    $timezone_event = ($event->event_tz ?: (!empty($event->id) ? CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $event->id, 'event_tz') : NULL));
+
+    foreach (self::tz_fields as $field) {
+      if(!empty($event->{$field})) {
+        $event->{$field} = CRM_Utils_Date::convertTimeZone($event->{$field}, NULL, $timezone_event);
+      }
+    }
+  }
+
+  public static function resetTimezones(CRM_Event_DAO_Event $event) {
+    // Process time zoned fields into their own time zone
+    $timezone_event = ($event->event_tz ?: (!empty($event->id) ? CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $event->id, 'event_tz') : NULL));
+
+    foreach (self::tz_fields as $field) {
+      if (!empty($event->{$field})) {
+        $event->{$field} = CRM_Utils_Date::convertTimeZone($event->{$field}, $timezone_event);
+      }
+    }
+  }
+
 }
index 9e1c5dfbf3068ce19c7e0d179761cb1ec0ce8972..cfaad1fa5685c5f0b14a3baaa8717c93502f0586 100644 (file)
@@ -100,6 +100,17 @@ class CRM_Event_Form_ManageEvent_EventInfo extends CRM_Event_Form_ManageEvent {
 
     $defaults['waitlist_text'] = CRM_Utils_Array::value('waitlist_text', $defaults, ts('This event is currently full. However you can register now and get added to a waiting list. You will be notified if spaces become available.'));
     $defaults['template_id'] = $this->_templateId;
+
+    $defaults['event_tz'] = CRM_Utils_Array::value('event_tz', $defaults, CRM_Core_Config::singleton()->userSystem->getTimeZoneString());
+
+    // Convert start and end date defaults to event time zone.
+    if (!empty($defaults['start_date'])) {
+      $defaults['start_date'] = CRM_Utils_Date::convertTimeZone($defaults['start_date'], $defaults['event_tz']);
+    }
+    if (!empty($defaults['end_date'])) {
+      $defaults['end_date'] = CRM_Utils_Date::convertTimeZone($defaults['end_date'], $defaults['event_tz']);
+    }
+
     return $defaults;
   }
 
@@ -159,6 +170,8 @@ class CRM_Event_Form_ManageEvent_EventInfo extends CRM_Event_Form_ManageEvent {
     $this->addElement('checkbox', 'is_share', ts('Add footer region with Twitter, Facebook and LinkedIn share buttons and scripts?'));
     $this->addElement('checkbox', 'is_map', ts('Include Map to Event Location'));
 
+    $this->addSelect('event_tz', ['placeholder' => ts('- Select time zone -')], TRUE);
+
     $this->add('datepicker', 'start_date', ts('Start'), [], !$this->_isTemplate, ['time' => TRUE]);
     $this->add('datepicker', 'end_date', ts('End'), [], FALSE, ['time' => TRUE]);
 
@@ -215,8 +228,8 @@ class CRM_Event_Form_ManageEvent_EventInfo extends CRM_Event_Form_ManageEvent {
     $params = array_merge($this->controller->exportValues($this->_name), $this->_submitValues);
 
     //format params
-    $params['start_date'] = $params['start_date'] ?? NULL;
-    $params['end_date'] = $params['end_date'] ?? NULL;
+    $params['start_date'] = !empty($params['start_date']) ? CRM_Utils_Date::convertTimeZone($params['start_date'], NULL, $params['event_tz'] ?? NULL) : $params['start_date'];
+    $params['end_date'] = !empty($params['end_date']) ? CRM_Utils_Date::convertTimeZone($params['end_date'], NULL, $params['event_tz'] ?? NULL) : $params['end_date'];
     $params['has_waitlist'] = CRM_Utils_Array::value('has_waitlist', $params, FALSE);
     $params['is_map'] = CRM_Utils_Array::value('is_map', $params, FALSE);
     $params['is_active'] = CRM_Utils_Array::value('is_active', $params, FALSE);
index 3eb2a7efa981cb512a3f3bf62fd130606aa7513d..bb6b64d870ae608c7761327fac151c9587e6c2fd 100644 (file)
@@ -29,6 +29,8 @@ class CRM_Event_Form_ManageEvent_Registration extends CRM_Event_Form_ManageEvent
   protected $_profilePostMultiple = [];
   protected $_profilePostMultipleAdd = [];
 
+  protected $_tz;
+
   /**
    * Set variables up before form is built.
    */
@@ -166,6 +168,14 @@ class CRM_Event_Form_ManageEvent_Registration extends CRM_Event_Form_ManageEvent
     $defaults['thankyou_title'] = CRM_Utils_Array::value('thankyou_title', $defaults, ts('Thank You for Registering'));
     $defaults['approval_req_text'] = CRM_Utils_Array::value('approval_req_text', $defaults, ts('Participation in this event requires approval. Submit your registration request here. Once approved, you will receive an email with a link to a web page where you can complete the registration process.'));
 
+    // Convert start and end date defaults to event time zone.
+    if (!empty($defaults['registration_start_date'])) {
+      $defaults['registration_start_date'] = CRM_Utils_Date::convertTimeZone($defaults['registration_start_date'], $this->_tz ?? NULL);
+    }
+    if (!empty($defaults['registration_end_date'])) {
+      $defaults['registration_end_date'] = CRM_Utils_Date::convertTimeZone($defaults['registration_end_date'], $this->_tz ?? NULL);
+    }
+
     return $defaults;
   }
 
@@ -228,6 +238,9 @@ class CRM_Event_Form_ManageEvent_Registration extends CRM_Event_Form_ManageEvent
     $this->add('text', 'registration_link_text', ts('Registration Link Text'));
 
     if (!$this->_isTemplate) {
+      $this->_tz = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $this->_id, 'event_tz') ?? CRM_Core_Config::singleton()->userSystem->getTimeZoneString();
+      $tz = CRM_Core_SelectValues::timezone()[$this->_tz];
+      $this->assign('event_tz', $tz);
       $this->add('datepicker', 'registration_start_date', ts('Registration Start Date'), [], FALSE, ['time' => TRUE]);
       $this->add('datepicker', 'registration_end_date', ts('Registration End Date'), [], FALSE, ['time' => TRUE]);
     }
@@ -776,6 +789,10 @@ class CRM_Event_Form_ManageEvent_Registration extends CRM_Event_Form_ManageEvent
 
     $params['id'] = $this->_id;
 
+    if (!isset($this->_tz)) {
+      $this->_tz = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $this->_id, 'event_tz') ?? CRM_Core_Config::singleton()->userSystem->getTimeZoneString();
+    }
+
     // format params
     $params['is_online_registration'] = CRM_Utils_Array::value('is_online_registration', $params, FALSE);
     // CRM-11182
@@ -783,6 +800,8 @@ class CRM_Event_Form_ManageEvent_Registration extends CRM_Event_Form_ManageEvent
     $params['is_multiple_registrations'] = CRM_Utils_Array::value('is_multiple_registrations', $params, FALSE);
     $params['allow_same_participant_emails'] = CRM_Utils_Array::value('allow_same_participant_emails', $params, FALSE);
     $params['requires_approval'] = CRM_Utils_Array::value('requires_approval', $params, FALSE);
+    $params['registration_start_date'] = !empty($params['registration_start_date']) ? CRM_Utils_Date::convertTimeZone($params['registration_start_date'], NULL, $this->_tz ?? NULL) : $params['registration_start_date'];
+    $params['registration_end_date'] = !empty($params['registration_end_date']) ? CRM_Utils_Date::convertTimeZone($params['registration_end_date'], NULL, $this->_tz ?? NULL) : $params['registration_end_date'];
 
     // reset is_email confirm if not online reg
     if (!$params['is_online_registration']) {
index a39eb17a6a32e0308a1fc11ed1bfe4254360d23f..c25dcf9400ac89e50373cfc112cc967f4bd4e85f 100644 (file)
@@ -361,6 +361,13 @@ ORDER BY start_date desc
         }
         CRM_Core_DAO::storeValues($dao, $manageEvent[$dao->id]);
 
+        if (!is_null($dao->event_tz) && $dao->event_tz != CRM_Core_Config::singleton()->userSystem->getTimeZoneString()) {
+          foreach (CRM_Event_BAO_Event::tz_fields as $field) {
+            $manageEvent[$dao->id][$field . '_with_tz'] = CRM_Utils_Date::convertTimeZone($dao->{$field} ?? '', $dao->event_tz);
+          }
+        }
+        $manageEvent[$dao->id]['event_tz'] = $dao->event_tz ? CRM_Core_SelectValues::timezone()[$dao->event_tz] : FALSE;
+
         // form all action links
         $action = array_sum(array_keys($this->links()));
 
index 1802632da5374e3c93340420e66463e580c64086..b050ace270347e45c9f6b338ccfe64c8eff6aa18 100644 (file)
@@ -2215,4 +2215,72 @@ class CRM_Utils_Date {
     return $dateObject->format($format);
   }
 
+  /**
+   * Convert a date string between time zones
+   *
+   * @param string $date - date string using $format
+   * @param string $tz_to - new time zone for date
+   * @param string $tz_from - current time zone for date
+   * @param string $format - format string specification as per DateTime::createFromFormat; defaults to 'Y-m-d H:i:s'
+   *
+   * @throws \CRM_Core_Exception
+   *
+   * @return string;
+   */
+  public static function convertTimeZone(string $date, string $tz_to = NULL, string $tz_from = NULL, $format = NULL) {
+    if (!$tz_from) {
+      $tz_from = CRM_Core_Config::singleton()->userSystem->getTimeZoneString();
+    }
+    if (!$tz_to) {
+      $tz_to = CRM_Core_Config::singleton()->userSystem->getTimeZoneString();
+    }
+
+    // Return early if both timezones are the same.
+    if ($tz_from == $tz_to) {
+      return $date;
+    }
+
+    $tz_from = new DateTimeZone($tz_from);
+    $tz_to = new DateTimeZone($tz_to);
+
+    if (is_null($format)) {
+      // Detect "mysql" format dates and adjust format accordingly
+      $format = preg_match('/^(\d{4})(\d{2}){0,5}$/', $date, $m) ? 'YmdHis' : 'Y-m-d H:i:s';
+    }
+
+    try {
+      $date_object = DateTime::createFromFormat('!' . $format, $date, $tz_from);
+      $dt_errors = DateTime::getLastErrors();
+
+      if (!$date_object) {
+        Civi::log()->warning(ts('Attempted to convert time zone with incorrect date format %1', ['%1' => $date]));
+
+        $dt_errors = DateTime::getLastErrors();
+
+        $date_object = new DateTime($date, $tz_from);
+      }
+      elseif ($dt_errors['warning_count'] && array_intersect($dt_errors['warnings'], ['The parsed date was invalid'])) {
+        throw new Exception('The parsed date was invalid');
+      }
+
+      $date_object->setTimezone($tz_to);
+
+      return $date_object->format($format);
+    }
+    catch (Exception $e) {
+      if ($dt_errors['error_count'] || $dt_errors['warning_count']) {
+        throw new CRM_Core_Exception(ts(
+          'Failed to parse date with %1 errors and %2 warnings',
+          [
+            '%1' => $dt_errors['error_count'],
+            '%2' => $dt_errors['warning_count'],
+          ]
+        ), 0, ['format' => $format, 'date' => $date] + $dt_errors);
+      }
+      else {
+        throw $e;
+      }
+    }
+  }
+
 }
index 5bb6b6a706d5bd1d22fb07352842a34fee2412a1..1877ae26ad38fa2f82d7a64af1d01d85526e452f 100644 (file)
       <td class="label">{$form.description.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_event' field='description' id=$eventID}{/if}</td>
       <td>{$form.description.html}</td>
     </tr>
+    <tr class="crm-event-manage-eventinfo-form-block-event_tz">
+      <td class="label">{$form.event_tz.label}</td>
+      <td>{$form.event_tz.html}</td>
+    </tr>
     {if !$isTemplate}
       <tr class="crm-event-manage-eventinfo-form-block-start_date">
         <td class="label">{$form.start_date.label}</td>
index c3db65bb90d1d8a47ff62dd04af838b59e86aee0..72f6888408c82e9ad8108cba281c575f7fe7fc87 100644 (file)
     <td>{$form.registration_link_text.html} {help id="id-link_text"}</td>
   </tr>
   {if !$isTemplate}
+    <tr class="crm-event-manage-registration-form-block-event_tz">
+      <td scope="row" class="label" width="20%">{ts}Event Time Zone{/ts}</label></td>
+      <td><output id="event-tz">{$event_tz}</output></td>
+    </tr>
     <tr class="crm-event-manage-registration-form-block-registration_start_date">
       <td scope="row" class="label" width="20%">{$form.registration_start_date.label}</td>
       <td>{$form.registration_start_date.html}</td>
index f0dbdea85c52fb52103ce8a64b9a6c312d0ecad2..124ba7f13f9f2b75c808e3dd8cbfcf8dd251f96f 100644 (file)
@@ -62,8 +62,8 @@
           <td class="crm-event-state_province">{$row.state_province}</td>
           <td class="crm-event-event_type">{$row.event_type}</td>
           <td class="crm-event-is_public">{if $row.is_public eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if}</td>
-          <td class="crm-event-start_date" data-order="{$row.start_date|crmDate:'%Y-%m-%d'}">{$row.start_date|crmDate:"%b %d, %Y %l:%M %P"}</td>
-          <td class="crm-event-end_date" data-order="{$row.end_date|crmDate:'%Y-%m-%d'}">{$row.end_date|crmDate:"%b %d, %Y %l:%M %P"}</td>
+          <td class="crm-event-start_date" data-order="{$row.start_date|crmDate:'%Y-%m-%d %H:%M'}">{$row.start_date|crmDate:"%b %d, %Y %l:%M %P"}{if $row.start_date_with_tz}<br />{$row.start_date_with_tz|crmDate:"%b %d, %Y %l:%M %P"} {$row.event_tz}{elseif !$row.event_tz} <span class="error-message">{ts 1='<i class="crm-i fa-warning"></i>'}%1 No timezone set{/ts}</span>{/if}</td>
+          <td class="crm-event-end_date" data-order="{$row.end_date|crmDate:'%Y-%m-%d %H:%M'}">{$row.end_date|crmDate:"%b %d, %Y %l:%M %P"}{if $row.end_date_with_tz}<br />{$row.end_date_with_tz|crmDate:"%b %d, %Y %l:%M %P"} {$row.event_tz}{/if}</td>
           {if call_user_func(array('CRM_Campaign_BAO_Campaign','isCampaignEnable'))}
             <td class="crm-event-campaign">{$row.campaign}</td>
           {/if}