diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php b/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventDragController.php @@ -29,7 +29,7 @@ $xactions = array(); - $duration = $event->getDateTo() - $event->getDateFrom(); + $duration = $event->getDuration(); $start = $request->getInt('start'); $start_value = id(AphrontFormDateControlValue::newFromEpoch( @@ -50,7 +50,6 @@ ->setTransactionType(PhabricatorCalendarEventTransaction::TYPE_END_DATE) ->setNewValue($end_value); - $editor = id(new PhabricatorCalendarEventEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -91,10 +91,10 @@ $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, - $event->getDateTo()); + $event->getViewerDateTo()); $start_value = AphrontFormDateControlValue::newFromEpoch( $viewer, - $event->getDateFrom()); + $event->getViewerDateFrom()); $recurrence_end_date_value = id(clone $end_value) ->setOptional(true); @@ -137,7 +137,17 @@ $view_policy = $event->getViewPolicy(); $space = $event->getSpacePHID(); + if ($request->isFormPost()) { + $is_all_day = $request->getStr('isAllDay'); + + if ($is_all_day) { + // TODO: This is a very gross temporary hack to get this working + // reasonably: if this is an all day event, force the viewer's + // timezone to UTC so the date controls get interpreted as UTC. + $viewer->overrideTimezoneIdentifier('UTC'); + } + $xactions = array(); $name = $request->getStr('name'); @@ -159,7 +169,6 @@ $space = $request->getStr('spacePHID'); $is_recurring = $request->getStr('isRecurring') ? 1 : 0; $frequency = $request->getStr('frequency'); - $is_all_day = $request->getStr('isAllDay'); $icon = $request->getStr('icon'); $invitees = $request->getArr('invitees'); @@ -192,7 +201,7 @@ $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE) - ->setNewValue($recurrence_end_date_value); + ->setNewValue($recurrence_end_date_value->getEpoch()); } } @@ -210,12 +219,12 @@ $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_START_DATE) - ->setNewValue($start_value); + ->setNewValue($start_value->getEpoch()); $xactions[] = id(new PhabricatorCalendarEventTransaction()) ->setTransactionType( PhabricatorCalendarEventTransaction::TYPE_END_DATE) - ->setNewValue($end_value); + ->setNewValue($end_value->getEpoch()); } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -198,29 +198,29 @@ ->setUser($viewer); if ($event->getIsAllDay()) { - $date_start = phabricator_date($event->getDateFrom(), $viewer); - $date_end = phabricator_date($event->getDateTo(), $viewer); + $date_start = phabricator_date($event->getViewerDateFrom(), $viewer); + $date_end = phabricator_date($event->getViewerDateTo(), $viewer); if ($date_start == $date_end) { $properties->addProperty( pht('Time'), - phabricator_date($event->getDateFrom(), $viewer)); + phabricator_date($event->getViewerDateFrom(), $viewer)); } else { $properties->addProperty( pht('Starts'), - phabricator_date($event->getDateFrom(), $viewer)); + phabricator_date($event->getViewerDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), - phabricator_date($event->getDateTo(), $viewer)); + phabricator_date($event->getViewerDateTo(), $viewer)); } } else { $properties->addProperty( pht('Starts'), - phabricator_datetime($event->getDateFrom(), $viewer)); + phabricator_datetime($event->getViewerDateFrom(), $viewer)); $properties->addProperty( pht('Ends'), - phabricator_datetime($event->getDateTo(), $viewer)); + phabricator_datetime($event->getViewerDateTo(), $viewer)); } if ($event->getIsRecurring()) { diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -22,8 +22,6 @@ array $xactions) { $actor = $this->requireActor(); - $object->removeViewerTimezone($actor); - if ($object->getIsStub()) { $this->materializeStub($object); } @@ -151,7 +149,7 @@ case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: case PhabricatorCalendarEventTransaction::TYPE_START_DATE: case PhabricatorCalendarEventTransaction::TYPE_END_DATE: - return $xaction->getNewValue()->getEpoch(); + return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -308,6 +306,8 @@ } if ($phids) { + $object->applyViewerTimezone($this->getActor()); + $user = new PhabricatorUser(); $conn_w = $user->establishConnection('w'); queryfx( @@ -344,15 +344,16 @@ foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $start_date_xaction) { - $start_date = $xaction->getNewValue()->getEpoch(); + $start_date = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $end_date_xaction) { - $end_date = $xaction->getNewValue()->getEpoch(); + $end_date = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $recurrence_end_xaction) { $recurrence_end = $xaction->getNewValue(); } else if ($xaction->getTransactionType() == $is_recurrence_xaction) { $is_recurring = $xaction->getNewValue(); } } + if ($start_date > $end_date) { $type = PhabricatorCalendarEventTransaction::TYPE_END_DATE; $errors[] = new PhabricatorApplicationTransactionValidationError( @@ -399,20 +400,6 @@ $errors[] = $error; } break; - case PhabricatorCalendarEventTransaction::TYPE_RECURRENCE_END_DATE: - case PhabricatorCalendarEventTransaction::TYPE_START_DATE: - case PhabricatorCalendarEventTransaction::TYPE_END_DATE: - foreach ($xactions as $xaction) { - $date_value = $xaction->getNewValue(); - if (!$date_value->isValid()) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Invalid date.'), - $xaction); - } - } - break; } return $errors; diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -90,7 +90,7 @@ protected function getPagingValueMap($cursor, array $keys) { $event = $this->loadCursorObject($cursor); return array( - 'start' => $event->getDateFrom(), + 'start' => $event->getViewerDateFrom(), 'id' => $event->getID(), ); } @@ -121,7 +121,7 @@ foreach ($events as $key => $event) { $sequence_start = 0; $sequence_end = null; - $duration = $event->getDateTo() - $event->getDateFrom(); + $duration = $event->getDuration(); $end = null; $instance_of = $event->getInstanceOfEventPHID(); @@ -137,9 +137,10 @@ $frequency = $event->getFrequencyUnit(); $modify_key = '+1 '.$frequency; - if ($this->rangeBegin && $this->rangeBegin > $event->getDateFrom()) { + if (($this->rangeBegin !== null) && + ($this->rangeBegin > $event->getViewerDateFrom())) { $max_date = $this->rangeBegin - $duration; - $date = $event->getDateFrom(); + $date = $event->getViewerDateFrom(); $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); while ($date < $max_date) { @@ -151,7 +152,7 @@ $start = $this->rangeBegin; } else { - $start = $event->getDateFrom() - $duration; + $start = $event->getViewerDateFrom() - $duration; } $date = $start; @@ -199,9 +200,9 @@ if ($raw_limit) { if (count($events) >= $raw_limit) { - $events = msort($events, 'getDateFrom'); + $events = msort($events, 'getViewerDateFrom'); $events = array_slice($events, 0, $raw_limit, true); - $enforced_end = last($events)->getDateFrom(); + $enforced_end = last($events)->getViewerDateFrom(); } } } @@ -303,18 +304,22 @@ $this->phids); } + // NOTE: The date ranges we query for are larger than the requested ranges + // because we need to catch all-day events. We'll refine this range later + // after adjusting the visible range of events we load. + if ($this->rangeBegin) { $where[] = qsprintf( $conn, 'event.dateTo >= %d OR event.isRecurring = 1', - $this->rangeBegin); + $this->rangeBegin - phutil_units('16 hours in seconds')); } if ($this->rangeEnd) { $where[] = qsprintf( $conn, 'event.dateFrom <= %d', - $this->rangeEnd); + $this->rangeEnd + phutil_units('16 hours in seconds')); } if ($this->inviteePHIDs !== null) { @@ -399,8 +404,8 @@ $viewer = $this->getViewer(); foreach ($events as $key => $event) { - $event_start = $event->getDateFrom(); - $event_end = $event->getDateTo(); + $event_start = $event->getViewerDateFrom(); + $event_end = $event->getViewerDateTo(); if ($range_start && $event_end < $range_start) { unset($events[$key]); @@ -466,7 +471,7 @@ } } - $events = msort($events, 'getDateFrom'); + $events = msort($events, 'getViewerDateFrom'); return $events; } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -311,11 +311,10 @@ $item->addAttribute($attending); } - if (strlen($event->getDuration()) > 0) { + if ($event->getDuration()) { $duration = pht( 'Duration: %s', - $event->getDuration()); - + $event->getDisplayDuration()); $item->addIcon('none', $duration); } @@ -370,7 +369,9 @@ $viewer_is_invited = $status->getIsUserInvited($viewer->getPHID()); $event = new AphrontCalendarEventView(); - $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); + $event->setEpochRange( + $status->getViewerDateFrom(), + $status->getViewerDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); @@ -434,7 +435,9 @@ $event = new AphrontCalendarEventView(); $event->setCanEdit($can_edit); $event->setEventID($status->getID()); - $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); + $event->setEpochRange( + $status->getViewerDateFrom(), + $status->getViewerDateTo()); $event->setIsAllDay($status->getIsAllDay()); $event->setIcon($status->getIcon()); $event->setViewerIsInvited($viewer_is_invited); @@ -553,10 +556,10 @@ $viewer = $this->requireViewer(); $from_datetime = PhabricatorTime::getDateTimeFromEpoch( - $event->getDateFrom(), + $event->getViewerDateFrom(), $viewer); $to_datetime = PhabricatorTime::getDateTimeFromEpoch( - $event->getDateTo(), + $event->getViewerDateTo(), $viewer); $from_date_formatted = $from_datetime->format('Y m d'); @@ -566,23 +569,23 @@ if ($from_date_formatted == $to_date_formatted) { return pht( '%s, All Day', - phabricator_date($event->getDateFrom(), $viewer)); + phabricator_date($event->getViewerDateFrom(), $viewer)); } else { return pht( '%s - %s, All Day', - phabricator_date($event->getDateFrom(), $viewer), - phabricator_date($event->getDateTo(), $viewer)); + phabricator_date($event->getViewerDateFrom(), $viewer), + phabricator_date($event->getViewerDateTo(), $viewer)); } } else if ($from_date_formatted == $to_date_formatted) { return pht( '%s - %s', - phabricator_datetime($event->getDateFrom(), $viewer), - phabricator_time($event->getDateTo(), $viewer)); + phabricator_datetime($event->getViewerDateFrom(), $viewer), + phabricator_time($event->getViewerDateTo(), $viewer)); } else { return pht( '%s - %s', - phabricator_datetime($event->getDateFrom(), $viewer), - phabricator_datetime($event->getDateTo(), $viewer)); + phabricator_datetime($event->getViewerDateFrom(), $viewer), + phabricator_datetime($event->getViewerDateTo(), $viewer)); } } } 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,7 +41,9 @@ private $parentEvent = self::ATTACHABLE; private $invitees = self::ATTACHABLE; - private $appliedViewer; + + private $viewerDateFrom; + private $viewerDateTo; // Frequency Constants const FREQUENCY_DAILY = 'daily'; @@ -157,7 +159,7 @@ $date_time->modify($modify_key); $date = $date_time->format('U'); - $duration = $parent->getDateTo() - $parent->getDateFrom(); + $duration = $this->getDuration(); $this ->setDateFrom($date) @@ -192,74 +194,49 @@ return $ghost; } - public function applyViewerTimezone(PhabricatorUser $viewer) { - if ($this->appliedViewer) { - throw new Exception(pht('Viewer timezone is already applied!')); + public function getViewerDateFrom() { + if ($this->viewerDateFrom === null) { + throw new PhutilInvalidStateException('applyViewerTimezone'); } - $this->appliedViewer = $viewer; + return $this->viewerDateFrom; + } - if (!$this->getIsAllDay()) { - return $this; + public function getViewerDateTo() { + if ($this->viewerDateTo === null) { + throw new PhutilInvalidStateException('applyViewerTimezone'); } - $zone = $viewer->getTimeZone(); + return $this->viewerDateTo; + } + public function applyViewerTimezone(PhabricatorUser $viewer) { + if (!$this->getIsAllDay()) { + $this->viewerDateFrom = $this->getDateFrom(); + $this->viewerDateTo = $this->getDateTo(); + } else { + $zone = $viewer->getTimeZone(); - $this->setDateFrom( - $this->getDateEpochForTimeZone( + $this->viewerDateFrom = $this->getDateEpochForTimeZone( $this->getDateFrom(), - new DateTimeZone('Pacific/Kiritimati'), + new DateTimeZone('UTC'), 'Y-m-d', null, - $zone)); + $zone); - $this->setDateTo( - $this->getDateEpochForTimeZone( + $this->viewerDateTo = $this->getDateEpochForTimeZone( $this->getDateTo(), - new DateTimeZone('Pacific/Midway'), + new DateTimeZone('UTC'), 'Y-m-d 23:59:00', - '-1 day', - $zone)); + null, + $zone); + } return $this; } - - public function removeViewerTimezone(PhabricatorUser $viewer) { - if (!$this->appliedViewer) { - throw new Exception(pht('Viewer timezone is not applied!')); - } - - if ($viewer->getPHID() != $this->appliedViewer->getPHID()) { - throw new Exception(pht('Removed viewer must match applied viewer!')); - } - - $this->appliedViewer = null; - - if (!$this->getIsAllDay()) { - return $this; - } - - $zone = $viewer->getTimeZone(); - - $this->setDateFrom( - $this->getDateEpochForTimeZone( - $this->getDateFrom(), - $zone, - 'Y-m-d', - null, - new DateTimeZone('Pacific/Kiritimati'))); - - $this->setDateTo( - $this->getDateEpochForTimeZone( - $this->getDateTo(), - $zone, - 'Y-m-d', - '+1 day', - new DateTimeZone('Pacific/Midway'))); - - return $this; + public function getDuration() { + return $this->getDateTo() - $this->getDateFrom(); } private function getDateEpochForTimeZone( @@ -281,12 +258,6 @@ } public function save() { - if ($this->appliedViewer) { - throw new Exception( - pht( - 'Can not save event with viewer timezone still applied!')); - } - if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } @@ -298,13 +269,13 @@ * Get the event start epoch for evaluating invitee availability. * * When assessing availability, we pretend events start earlier than they - * really. This allows us to mark users away for the entire duration of a + * really do. This allows us to mark users away for the entire duration of a * series of back-to-back meetings, even if they don't strictly overlap. * * @return int Event start date for availability caches. */ public function getDateFromForCache() { - return ($this->getDateFrom() - phutil_units('15 minutes in seconds')); + return ($this->getViewerDateFrom() - phutil_units('15 minutes in seconds')); } protected function getConfiguration() { @@ -456,8 +427,8 @@ return false; } - public function getDuration() { - $seconds = $this->dateTo - $this->dateFrom; + public function getDisplayDuration() { + $seconds = $this->getDuration(); $minutes = round($seconds / 60, 1); $hours = round($minutes / 60, 3); $days = round($hours / 24, 2); @@ -470,12 +441,12 @@ round($days, 1)); } else if ($hours >= 1) { return pht( - '%s hour(s)', - round($hours, 1)); + '%s hour(s)', + round($hours, 1)); } else if ($minutes >= 1) { return pht( - '%s minute(s)', - round($minutes, 0)); + '%s minute(s)', + round($minutes, 0)); } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -189,41 +189,44 @@ $range_start = $midnight->format('U'); $range_end = $week_end->format('U'); - $query = id(new PhabricatorCalendarEventQuery()) + $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withDateRange($range_start, $range_end) ->withInvitedPHIDs(array($user->getPHID())) - ->withIsCancelled(false); - - $statuses = $query->execute(); - $phids = mpull($statuses, 'getUserPHID'); - $events = array(); + ->withIsCancelled(false) + ->execute(); - foreach ($statuses as $status) { - $viewer_is_invited = $status->getIsUserInvited($user->getPHID()); + $event_views = array(); + foreach ($events as $event) { + $viewer_is_invited = $event->getIsUserInvited($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, - $status, + $event, PhabricatorPolicyCapability::CAN_EDIT); - $event = id(new AphrontCalendarEventView()) + $epoch_min = $event->getViewerDateFrom(); + $epoch_max = $event->getViewerDateTo(); + + $event_view = id(new AphrontCalendarEventView()) ->setCanEdit($can_edit) - ->setEventID($status->getID()) - ->setEpochRange($status->getDateFrom(), $status->getDateTo()) - ->setIsAllDay($status->getIsAllDay()) - ->setIcon($status->getIcon()) + ->setEventID($event->getID()) + ->setEpochRange($epoch_min, $epoch_max) + ->setIsAllDay($event->getIsAllDay()) + ->setIcon($event->getIcon()) ->setViewerIsInvited($viewer_is_invited) - ->setName($status->getName()) - ->setURI($status->getURI()); - $events[] = $event; + ->setName($event->getName()) + ->setURI($event->getURI()); + + $event_views[] = $event_view; } - $events = msort($events, 'getEpochStart'); + $event_views = msort($event_views, 'getEpochStart'); + $day_view = id(new PHUICalendarWeekView()) ->setViewer($viewer) ->setView('week') - ->setEvents($events) + ->setEvents($event_views) ->setWeekLength(3) ->render(); diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -413,6 +413,13 @@ foreach ($rebuild as $phid => $user) { $events = idx($map, $phid, array()); + // We loaded events with the omnipotent user, but want to shift them + // into the user's timezone before building the cache because they will + // be unavailable during their own local day. + foreach ($events as $event) { + $event->applyViewerTimezone($user); + } + $cursor = $min_range; if ($events) { // Find the next time when the user has no meetings. If we move forward @@ -420,7 +427,7 @@ while (true) { foreach ($events as $event) { $from = $event->getDateFromForCache(); - $to = $event->getDateTo(); + $to = $event->getViewerDateTo(); if (($from <= $cursor) && ($to > $cursor)) { $cursor = $to; continue 2;