From cedee7b080c23a12aa1ec9b64766ff92b2675a64 Mon Sep 17 00:00:00 2001 From: Francis Whittle Date: Wed, 28 Apr 2021 10:28:57 +1000 Subject: [PATCH] CIVICRM-1749 Implement UI for timezone in Event management forms --- CRM/Core/SelectValues.php | 2 +- CRM/Event/BAO/Event.php | 23 +++++++ CRM/Event/Form/ManageEvent/EventInfo.php | 17 ++++- CRM/Event/Form/ManageEvent/Registration.php | 19 ++++++ CRM/Event/Page/ManageEvent.php | 7 ++ CRM/Utils/Date.php | 68 +++++++++++++++++++ .../CRM/Event/Form/ManageEvent/EventInfo.tpl | 4 ++ .../Event/Form/ManageEvent/Registration.tpl | 4 ++ templates/CRM/Event/Page/ManageEvent.tpl | 4 +- 9 files changed, 143 insertions(+), 5 deletions(-) diff --git a/CRM/Core/SelectValues.php b/CRM/Core/SelectValues.php index 4666f0f9de..c15c14f662 100644 --- a/CRM/Core/SelectValues.php +++ b/CRM/Core/SelectValues.php @@ -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. diff --git a/CRM/Event/BAO/Event.php b/CRM/Event/BAO/Event.php index 20534481a0..1047dd8b42 100644 --- a/CRM/Event/BAO/Event.php +++ b/CRM/Event/BAO/Event.php @@ -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); + } + } + } + } diff --git a/CRM/Event/Form/ManageEvent/EventInfo.php b/CRM/Event/Form/ManageEvent/EventInfo.php index 9e1c5dfbf3..cfaad1fa56 100644 --- a/CRM/Event/Form/ManageEvent/EventInfo.php +++ b/CRM/Event/Form/ManageEvent/EventInfo.php @@ -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); diff --git a/CRM/Event/Form/ManageEvent/Registration.php b/CRM/Event/Form/ManageEvent/Registration.php index 3eb2a7efa9..bb6b64d870 100644 --- a/CRM/Event/Form/ManageEvent/Registration.php +++ b/CRM/Event/Form/ManageEvent/Registration.php @@ -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']) { diff --git a/CRM/Event/Page/ManageEvent.php b/CRM/Event/Page/ManageEvent.php index a39eb17a6a..c25dcf9400 100644 --- a/CRM/Event/Page/ManageEvent.php +++ b/CRM/Event/Page/ManageEvent.php @@ -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())); diff --git a/CRM/Utils/Date.php b/CRM/Utils/Date.php index 1802632da5..b050ace270 100644 --- a/CRM/Utils/Date.php +++ b/CRM/Utils/Date.php @@ -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; + } + } + } + } diff --git a/templates/CRM/Event/Form/ManageEvent/EventInfo.tpl b/templates/CRM/Event/Form/ManageEvent/EventInfo.tpl index 5bb6b6a706..1877ae26ad 100644 --- a/templates/CRM/Event/Form/ManageEvent/EventInfo.tpl +++ b/templates/CRM/Event/Form/ManageEvent/EventInfo.tpl @@ -59,6 +59,10 @@ {$form.description.label} {if $action == 2}{include file='CRM/Core/I18n/Dialog.tpl' table='civicrm_event' field='description' id=$eventID}{/if} {$form.description.html} + + {$form.event_tz.label} + {$form.event_tz.html} + {if !$isTemplate} {$form.start_date.label} diff --git a/templates/CRM/Event/Form/ManageEvent/Registration.tpl b/templates/CRM/Event/Form/ManageEvent/Registration.tpl index c3db65bb90..72f6888408 100644 --- a/templates/CRM/Event/Form/ManageEvent/Registration.tpl +++ b/templates/CRM/Event/Form/ManageEvent/Registration.tpl @@ -54,6 +54,10 @@ {$form.registration_link_text.html} {help id="id-link_text"} {if !$isTemplate} + + {ts}Event Time Zone{/ts} + {$event_tz} + {$form.registration_start_date.label} {$form.registration_start_date.html} diff --git a/templates/CRM/Event/Page/ManageEvent.tpl b/templates/CRM/Event/Page/ManageEvent.tpl index f0dbdea85c..124ba7f13f 100644 --- a/templates/CRM/Event/Page/ManageEvent.tpl +++ b/templates/CRM/Event/Page/ManageEvent.tpl @@ -62,8 +62,8 @@ {$row.state_province} {$row.event_type} {if $row.is_public eq 1} {ts}Yes{/ts} {else} {ts}No{/ts} {/if} - {$row.start_date|crmDate:"%b %d, %Y %l:%M %P"} - {$row.end_date|crmDate:"%b %d, %Y %l:%M %P"} + {$row.start_date|crmDate:"%b %d, %Y %l:%M %P"}{if $row.start_date_with_tz}
{$row.start_date_with_tz|crmDate:"%b %d, %Y %l:%M %P"} {$row.event_tz}{elseif !$row.event_tz} {ts 1=''}%1 No timezone set{/ts}{/if} + {$row.end_date|crmDate:"%b %d, %Y %l:%M %P"}{if $row.end_date_with_tz}
{$row.end_date_with_tz|crmDate:"%b %d, %Y %l:%M %P"} {$row.event_tz}{/if} {if call_user_func(array('CRM_Campaign_BAO_Campaign','isCampaignEnable'))} {$row.campaign} {/if} -- 2.25.1