@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Store "All Day" events in a way that is compatible with EditEngine

Summary:
Ref T11326. Normally, events occur at a specific epoch, independent of the viewer. For example, if we're having a meeting in 35 hours, every user who looks at the event will see that it starts 35 hours from now.

But when an event is "All Day", the start time and end time depend on the //viewer//. A day like "Christmas" does not start at the same time for everyone: it starts sooner if you're in a more-eastern timezone. Baiscally, an event on "July 15th" starts whenever "July 15th" starts for whoever is looking at it.

Previously, we stored these events by using the western-most and eastern-most timezones as the start and end times (the earliest possible start and latest possible end).

This worked OK, but we get into a bunch of trouble with EditEngine, mostly because each field can be updated individually now. We can't easily tell if an event is all-day or not when reading or updating the start time and end time, and making that easier would introduce a huge amount of complexity.

Instead, when we update the start or end time, we write //two// times:

- The epoch timestamp of the time the user entered, which is the start time we will use if the event is a normal event.
- The epoch timestamp of 12:00 AM in UTC on the same date as the //local// date the user entered. This is pretty much like just storing the date the user actually typed. This is what w'ell use if the event is an all-day event.

Then, no matter whether the event is later made all-day or not, we have all the information we need to display it correctly.

Test Plan:
- Created and edited all-day events.
- Migrated existing all-day events, which appeared to survive without problems. (Note that events all-day which were created or edited in the last couple of days `master` won't survive this mutation correctly and will need to be fixed.)
- Created and edited normal, recurring, and recurring all-day events.
- Swapped back to `stable`, created an event, specifically migrated it forward, made sure it survived with times intact.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11326

Differential Revision: https://secure.phabricator.com/D16305

+107 -7
+2
resources/sql/autopatches/20160715.event.01.alldayfrom.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 2 + ADD allDayDateFrom INT UNSIGNED NOT NULL;
+2
resources/sql/autopatches/20160715.event.02.alldayto.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 2 + ADD allDayDateTo INT UNSIGNED NOT NULL;
+52
resources/sql/autopatches/20160715.event.03.allday.php
··· 1 + <?php 2 + 3 + $table = new PhabricatorCalendarEvent(); 4 + $conn = $table->establishConnection('w'); 5 + 6 + // Previously, "All Day" events were stored with a start and end date set to 7 + // the earliest possible start and end seconds for the corresponding days. We 8 + // now store all day events with their "date" epochs as UTC, separate from 9 + // individual event times. 10 + $zone_min = new DateTimeZone('Pacific/Midway'); 11 + $zone_max = new DateTimeZone('Pacific/Kiritimati'); 12 + $zone_utc = new DateTimeZone('UTC'); 13 + 14 + foreach (new LiskMigrationIterator($table) as $event) { 15 + // If this event has already migrated, skip it. 16 + if ($event->getAllDayDateFrom()) { 17 + continue; 18 + } 19 + 20 + $is_all_day = $event->getIsAllDay(); 21 + 22 + $epoch_min = $event->getDateFrom(); 23 + $epoch_max = $event->getDateTo(); 24 + 25 + $date_min = new DateTime('@'.$epoch_min); 26 + $date_max = new DateTime('@'.$epoch_max); 27 + 28 + if ($is_all_day) { 29 + $date_min->setTimeZone($zone_min); 30 + $date_min->modify('+2 days'); 31 + $date_max->setTimeZone($zone_max); 32 + $date_max->modify('-2 days'); 33 + } else { 34 + $date_min->setTimeZone($zone_utc); 35 + $date_max->setTimeZone($zone_utc); 36 + } 37 + 38 + $string_min = $date_min->format('Y-m-d'); 39 + $string_max = $date_max->format('Y-m-d 23:59:00'); 40 + 41 + $allday_min = id(new DateTime($string_min, $zone_utc))->format('U'); 42 + $allday_max = id(new DateTime($string_max, $zone_utc))->format('U'); 43 + 44 + queryfx( 45 + $conn, 46 + 'UPDATE %T SET allDayDateFrom = %d, allDayDateTo = %d 47 + WHERE id = %d', 48 + $table->getTableName(), 49 + $allday_min, 50 + $allday_max, 51 + $event->getID()); 52 + }
+31 -7
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 19 19 protected $hostPHID; 20 20 protected $dateFrom; 21 21 protected $dateTo; 22 + protected $allDayDateFrom; 23 + protected $allDayDateTo; 22 24 protected $description; 23 25 protected $isCancelled; 24 26 protected $isAllDay; ··· 62 64 $view_policy = $app->getPolicy($view_default); 63 65 $edit_policy = $app->getPolicy($edit_default); 64 66 65 - $start = new DateTime('@'.PhabricatorTime::getNow()); 67 + $now = PhabricatorTime::getNow(); 68 + 69 + $start = new DateTime('@'.$now); 66 70 $start->setTimeZone($actor->getTimeZone()); 67 71 68 72 $start->setTime($start->format('H'), 0, 0); ··· 71 75 72 76 $epoch_min = $start->format('U'); 73 77 $epoch_max = $end->format('U'); 78 + 79 + $now_date = new DateTime('@'.$now); 80 + $now_min = id(clone $now_date)->setTime(0, 0)->format('U'); 81 + $now_max = id(clone $now_date)->setTime(23, 59)->format('U'); 74 82 75 83 $default_icon = 'fa-calendar'; 76 84 ··· 91 99 ->attachInvitees(array()) 92 100 ->setDateFrom($epoch_min) 93 101 ->setDateTo($epoch_max) 102 + ->setAllDayDateFrom($now_min) 103 + ->setAllDayDateTo($now_max) 94 104 ->applyViewerTimezone($actor); 95 105 } 96 106 ··· 171 181 172 182 $duration = $this->getDuration(); 173 183 184 + $utc = new DateTimeZone('UTC'); 185 + 186 + $allday_from = $parent->getAllDayDateFrom(); 187 + $allday_date = new DateTime('@'.$allday_from, $utc); 188 + $allday_date->setTimeZone($utc); 189 + $allday_date->modify($modify_key); 190 + 191 + $allday_min = $allday_date->format('U'); 192 + $allday_duration = ($parent->getAllDayDateTo() - $allday_from); 193 + 174 194 $this 175 195 ->setDateFrom($date) 176 - ->setDateTo($date + $duration); 196 + ->setDateTo($date + $duration) 197 + ->setAllDayDateFrom($allday_min) 198 + ->setAllDayDateTo($allday_min + $allday_duration); 177 199 178 200 return $this; 179 201 } ··· 227 249 } else { 228 250 $zone = $viewer->getTimeZone(); 229 251 230 - $this->viewerDateFrom = $this->getDateEpochForTimeZone( 231 - $this->getDateFrom(), 252 + $this->viewerDateFrom = $this->getDateEpochForTimezone( 253 + $this->getAllDayDateFrom(), 232 254 new DateTimeZone('UTC'), 233 255 'Y-m-d', 234 256 null, 235 257 $zone); 236 258 237 - $this->viewerDateTo = $this->getDateEpochForTimeZone( 238 - $this->getDateTo(), 259 + $this->viewerDateTo = $this->getDateEpochForTimezone( 260 + $this->getAllDayDateTo(), 239 261 new DateTimeZone('UTC'), 240 262 'Y-m-d 23:59:00', 241 263 null, ··· 249 271 return $this->getDateTo() - $this->getDateFrom(); 250 272 } 251 273 252 - private function getDateEpochForTimeZone( 274 + public function getDateEpochForTimezone( 253 275 $epoch, 254 276 $src_zone, 255 277 $format, ··· 295 317 'name' => 'text', 296 318 'dateFrom' => 'epoch', 297 319 'dateTo' => 'epoch', 320 + 'allDayDateFrom' => 'epoch', 321 + 'allDayDateTo' => 'epoch', 298 322 'description' => 'text', 299 323 'isCancelled' => 'bool', 300 324 'isAllDay' => 'bool',
+10
src/applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php
··· 10 10 } 11 11 12 12 public function applyInternalEffects($object, $value) { 13 + $actor = $this->getActor(); 14 + 13 15 $object->setDateTo($value); 16 + 17 + $object->setAllDayDateTo( 18 + $object->getDateEpochForTimezone( 19 + $value, 20 + $actor->getTimeZone(), 21 + 'Y-m-d 23:59:00', 22 + null, 23 + new DateTimeZone('UTC'))); 14 24 } 15 25 16 26 public function getTitle() {
+10
src/applications/calendar/xaction/PhabricatorCalendarEventStartDateTransaction.php
··· 10 10 } 11 11 12 12 public function applyInternalEffects($object, $value) { 13 + $actor = $this->getActor(); 14 + 13 15 $object->setDateFrom($value); 16 + 17 + $object->setAllDayDateFrom( 18 + $object->getDateEpochForTimezone( 19 + $value, 20 + $actor->getTimeZone(), 21 + 'Y-m-d', 22 + null, 23 + new DateTimeZone('UTC'))); 14 24 } 15 25 16 26 public function getTitle() {