diff --git a/resources/sql/autopatches/20161003.cal.01.utcepoch.sql b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20161003.cal.01.utcepoch.sql @@ -0,0 +1,8 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcInitialEpoch INT UNSIGNED NOT NULL; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcUntilEpoch INT UNSIGNED; + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD utcInstanceEpoch INT UNSIGNED; diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -41,11 +41,16 @@ protected $spacePHID; + protected $utcInitialEpoch; + protected $utcUntilEpoch; + protected $utcInstanceEpoch; + private $parentEvent = self::ATTACHABLE; private $invitees = self::ATTACHABLE; private $viewerDateFrom; private $viewerDateTo; + private $viewerTimezone; // Frequency Constants const FREQUENCY_DAILY = 'daily'; @@ -298,6 +303,8 @@ $zone); } + $this->viewerTimezone = $viewer->getTimezoneIdentifier(); + return $this; } @@ -323,11 +330,62 @@ return $dst->format('U'); } + public function updateUTCEpochs() { + // The "intitial" epoch is the start time of the event, in UTC. + $start_date = $this->newStartDateTime() + ->setViewerTimezone('UTC'); + $start_epoch = $start_date->getEpoch(); + $this->setUTCInitialEpoch($start_epoch); + + // The "until" epoch is the last UTC epoch on which any instance of this + // event occurs. For infinitely recurring events, it is `null`. + + if (!$this->getIsRecurring()) { + $end_date = $this->newEndDateTime() + ->setViewerTimezone('UTC'); + $until_epoch = $end_date->getEpoch(); + } else { + $until_epoch = null; + $until_date = $this->newUntilDateTime() + ->setViewerTimezone('UTC'); + if ($until_date) { + $duration = $this->newDuration(); + $until_epoch = id(new PhutilCalendarRelativeDateTime()) + ->setOrigin($until_date) + ->setDuration($duration) + ->getEpoch(); + } + } + $this->setUTCUntilEpoch($until_epoch); + + // The "instance" epoch is a property of instances of recurring events. + // It's the original UTC epoch on which the instance started. Usually that + // is the same as the start date, but they may be different if the instance + // has been edited. + + // The ICS format uses this value (original start time) to identify event + // instances, and must do so because it allows additional arbitrary + // instances to be added (with "RDATE"). + + $instance_epoch = null; + $instance_date = $this->newInstanceDateTime(); + if ($instance_date) { + $instance_epoch = $instance_date + ->setViewerTimezone('UTC') + ->getEpoch(); + } + $this->setUTCInstanceEpoch($instance_epoch); + + return $this; + } + public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } + $this->updateUTCEpochs(); + return parent::save(); } @@ -363,6 +421,9 @@ 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', 'isStub' => 'bool', + 'utcInitialEpoch' => 'epoch', + 'utcUntilEpoch' => 'epoch?', + 'utcInstanceEpoch' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_date' => array( @@ -372,6 +433,13 @@ 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), + 'key_epoch' => array( + 'columns' => array('utcInitialEpoch', 'utcUntilEpoch'), + ), + 'key_rdate' => array( + 'columns' => array('instanceOfEventPHID', 'utcInstanceEpoch'), + 'unique' => true, + ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, @@ -641,11 +709,8 @@ $modified = $this->getDateModified(); $modified = PhutilCalendarAbsoluteDateTime::newFromEpoch($modified); - $date_start = $this->getDateFrom(); - $date_start = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_start); - - $date_end = $this->getDateTo(); - $date_end = PhutilCalendarAbsoluteDateTime::newFromEpoch($date_end); + $date_start = $this->newStartDateTime(); + $date_end = $this->newEndDateTime(); if ($this->getIsAllDay()) { $date_start->setIsAllDay(true); @@ -719,6 +784,57 @@ return $node; } + public function newStartDateTime() { + $epoch = $this->getDateFrom(); + return $this->newDateTimeFromEpoch($epoch); + } + + public function newEndDateTime() { + $epoch = $this->getDateTo(); + return $this->newDateTimeFromEpoch($epoch); + } + + public function newUntilDateTime() { + $epoch = $this->getRecurrenceEndDate(); + if (!$epoch) { + return null; + } + return $this->newDateTimeFromEpoch($epoch); + } + + public function newDuration() { + return id(new PhutilCalendarDuration()) + ->setSeconds($this->getDuration()); + } + + public function newInstanceDateTime() { + if (!$this->getIsRecurring()) { + return null; + } + + $epochs = $this->getParent()->getSequenceIndexEpochs( + new PhabricatorUser(), + $this->getSequenceIndex(), + $this->getDuration()); + + $epoch = $epochs['dateFrom']; + return $this->newDateTimeFromEpoch($epoch); + } + + private function newDateTimeFromEpoch($epoch) { + $datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch($epoch); + + $viewer_timezone = $this->viewerTimezone; + if ($viewer_timezone) { + $datetime->setViewerTimezone($viewer_timezone); + } + + if ($this->getIsAllDay()) { + $datetime->setIsAllDay(true); + } + + return $datetime; + } /* -( Markup Interface )--------------------------------------------------- */