diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index ea6c6a48ef..f12bf9a9c3 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1,524 +1,524 @@ setViewer($actor) ->withClasses(array('PhabricatorCalendarApplication')) ->executeOne(); $view_policy = null; $is_recurring = 0; if ($mode == 'public') { $view_policy = PhabricatorPolicies::getMostOpenPolicy(); } else if ($mode == 'recurring') { $is_recurring = true; } else { $view_policy = $actor->getPHID(); } return id(new PhabricatorCalendarEvent()) ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) ->setIsRecurring($is_recurring) ->setIcon(self::DEFAULT_ICON) ->setViewPolicy($view_policy) ->setEditPolicy($actor->getPHID()) ->attachInvitees(array()) ->applyViewerTimezone($actor); } public function applyViewerTimezone(PhabricatorUser $viewer) { if ($this->appliedViewer) { throw new Exception(pht('Viewer timezone is already applied!')); } $this->appliedViewer = $viewer; if (!$this->getIsAllDay()) { return $this; } $zone = $viewer->getTimeZone(); $this->setDateFrom( $this->getDateEpochForTimeZone( $this->getDateFrom(), new DateTimeZone('Pacific/Kiritimati'), 'Y-m-d', null, $zone)); $this->setDateTo( $this->getDateEpochForTimeZone( $this->getDateTo(), new DateTimeZone('Pacific/Midway'), 'Y-m-d 23:59:00', '-1 day', $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; } private function getDateEpochForTimeZone( $epoch, $src_zone, $format, $adjust, $dst_zone) { $src = new DateTime('@'.$epoch); $src->setTimeZone($src_zone); if (strlen($adjust)) { $adjust = ' '.$adjust; } $dst = new DateTime($src->format($format).$adjust, $dst_zone); return $dst->format('U'); } 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); } return parent::save(); } /** * 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 * 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')); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text', 'dateFrom' => 'epoch', 'dateTo' => 'epoch', 'description' => 'text', 'isCancelled' => 'bool', 'isAllDay' => 'bool', 'icon' => 'text32', 'mailKey' => 'bytes20', 'isRecurring' => 'bool', 'recurrenceEndDate' => 'epoch?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( 'columns' => array('userPHID', 'dateTo'), ), 'key_instance' => array( 'columns' => array('instanceOfEventPHID', 'sequenceIndex'), 'unique' => true, ), ), self::CONFIG_SERIALIZATION => array( 'recurrenceFrequency' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorCalendarEventPHIDType::TYPECONST); } public function getMonogram() { return 'E'.$this->getID(); } public function getInvitees() { return $this->assertAttached($this->invitees); } public function attachInvitees(array $invitees) { $this->invitees = $invitees; return $this; } public function getUserInviteStatus($phid) { $invitees = $this->getInvitees(); $invitees = mpull($invitees, null, 'getInviteePHID'); $invited = idx($invitees, $phid); if (!$invited) { return PhabricatorCalendarEventInvitee::STATUS_UNINVITED; } $invited = $invited->getStatus(); return $invited; } public function getIsUserAttending($phid) { $attending_status = PhabricatorCalendarEventInvitee::STATUS_ATTENDING; $old_status = $this->getUserInviteStatus($phid); $is_attending = ($old_status == $attending_status); return $is_attending; } public function getIsUserInvited($phid) { $uninvited_status = PhabricatorCalendarEventInvitee::STATUS_UNINVITED; $declined_status = PhabricatorCalendarEventInvitee::STATUS_DECLINED; $status = $this->getUserInviteStatus($phid); if ($status == $uninvited_status || $status == $declined_status) { return false; } return true; } public function getIsGhostEvent() { return $this->isGhostEvent; } public function setIsGhostEvent($is_ghost_event) { $this->isGhostEvent = $is_ghost_event; return $this; } public function generateNthGhost( $sequence_index, PhabricatorUser $actor) { $frequency = $this->getFrequencyUnit(); $modify_key = '+'.$sequence_index.' '.$frequency; $instance_of = ($this->getPHID()) ? $this->getPHID() : $this->instanceOfEventPHID; $date = $this->dateFrom; $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); $date_time->modify($modify_key); $date = $date_time->format('U'); $duration = $this->dateTo - $this->dateFrom; $edit_policy = PhabricatorPolicies::POLICY_NOONE; $ghost_event = id(clone $this) ->setIsGhostEvent(true) ->setDateFrom($date) ->setDateTo($date + $duration) ->setIsRecurring(true) ->setRecurrenceFrequency($this->recurrenceFrequency) ->setInstanceOfEventPHID($instance_of) ->setSequenceIndex($sequence_index) ->setEditPolicy($edit_policy); return $ghost_event; } public function getFrequencyUnit() { $frequency = idx($this->recurrenceFrequency, 'rule'); switch ($frequency) { case 'daily': return 'day'; case 'weekly': return 'week'; case 'monthly': return 'month'; case 'yearly': - return 'yearly'; + return 'year'; default: return 'day'; } } public function getURI() { $uri = '/'.$this->getMonogram(); if ($this->isGhostEvent) { $uri = $uri.'/'.$this->sequenceIndex; } return $uri; } public function getParentEvent() { return $this->assertAttached($this->parentEvent); } public function attachParentEvent($event) { $this->parentEvent = $event; return $this; } public function getIsCancelled() { $instance_of = $this->instanceOfEventPHID; if ($instance_of != null && $this->getIsParentCancelled()) { return true; } return $this->isCancelled; } public function getIsRecurrenceParent() { if ($this->isRecurring && !$this->instanceOfEventPHID) { return true; } return false; } public function getIsRecurrenceException() { if ($this->instanceOfEventPHID && !$this->isGhostEvent) { return true; } return false; } public function getIsParentCancelled() { if ($this->instanceOfEventPHID == null) { return false; } $recurring_event = $this->getParentEvent(); if ($recurring_event->getIsCancelled()) { return true; } return false; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "calendar:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newCalendarMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { // The owner of a task can always view and edit it. $user_phid = $this->getUserPHID(); if ($user_phid) { $viewer_phid = $viewer->getPHID(); if ($viewer_phid == $user_phid) { return true; } } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $status = $this->getUserInviteStatus($viewer->getPHID()); if ($status == PhabricatorCalendarEventInvitee::STATUS_INVITED || $status == PhabricatorCalendarEventInvitee::STATUS_ATTENDING || $status == PhabricatorCalendarEventInvitee::STATUS_DECLINED) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht('The owner of an event can always view and edit it, and invitees can always view it, except if the event is an instance of a recurring event.'); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorCalendarEventEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorCalendarEventTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return ($phid == $this->getUserPHID()); } public function shouldShowSubscribersProperty() { return true; } public function shouldAllowSubscription($phid) { return true; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return array($this->getUserPHID()); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } } diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index 5fbf825d22..45f9f3fa01 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -1,115 +1,106 @@ getUser(); $username = $user->getUsername(); $pref_time = PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT; $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $preferences = $user->loadPreferences(); $errors = array(); if ($request->isFormPost()) { $new_timezone = $request->getStr('timezone'); if (in_array($new_timezone, DateTimeZone::listIdentifiers(), true)) { $user->setTimezoneIdentifier($new_timezone); } else { $errors[] = pht('The selected timezone is not a valid timezone.'); } $preferences->setPreference( $pref_time, $request->getStr($pref_time)); $preferences->setPreference( $pref_week_start, $request->getStr($pref_week_start)); if (!$errors) { $preferences->save(); $user->save(); return id(new AphrontRedirectResponse()) ->setURI($this->getPanelURI('?saved=true')); } } $timezone_ids = DateTimeZone::listIdentifiers(); $timezone_id_map = array_fuse($timezone_ids); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Timezone')) ->setName('timezone') ->setOptions($timezone_id_map) ->setValue($user->getTimezoneIdentifier())) - ->appendRemarkupInstructions( - pht( - "**Custom Date and Time Formats**\n\n". - "You can specify custom formats which will be used when ". - "rendering dates and times of day. Examples:\n\n". - "| Format | Example | Notes |\n". - "| ------ | -------- | ----- |\n". - "| `g:i A` | 2:34 PM | Default 12-hour time. |\n". - "| `G.i a` | 02.34 pm | Alternate 12-hour time. |\n". - "| `H:i` | 14:34 | 24-hour time. |\n". - "\n\n". - "You can find a [[%s | full reference in the PHP manual]].", - 'http://www.php.net/manual/en/function.date.php')) ->appendChild( - id(new AphrontFormTextControl()) + id(new AphrontFormSelectControl()) ->setLabel(pht('Time-of-Day Format')) ->setName($pref_time) + ->setOptions(array( + 'g:i A' => pht('12-hour (2:34 PM)'), + 'H:i' => pht('24-hour (14:34)'), + )) ->setCaption( pht('Format used when rendering a time of day.')) ->setValue($preferences->getPreference($pref_time))) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Week Starts On')) ->setOptions($this->getWeekDays()) ->setName($pref_week_start) ->setCaption( pht('Calendar weeks will start with this day.')) ->setValue($preferences->getPreference($pref_week_start, 0))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Account Settings'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Date and Time Settings')) ->setFormSaved($request->getStr('saved')) ->setFormErrors($errors) ->setForm($form); return array( $form_box, ); } private function getWeekDays() { return array( pht('Sunday'), pht('Monday'), pht('Tuesday'), pht('Wednesday'), pht('Thursday'), pht('Friday'), pht('Saturday'), ); } }