diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index a17a96784f..6cba6d8fa7 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -1,72 +1,76 @@ setUser($this->getViewer()) ->addAction( id(new PhabricatorActionView()) - ->setName(pht('Create Private Event')) - ->setHref('/calendar/event/create/?mode=private')) + ->setName(pht('Create Event')) + ->setHref('/calendar/event/create/')) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Public Event')) - ->setHref('/calendar/event/create/?mode=public')); + ->setHref('/calendar/event/create/?mode=public')) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Create Recurring Event')) + ->setHref('/calendar/event/create/?mode=recurring')); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create Event')) ->setHref($this->getApplicationURI().'event/create/') ->setIcon('fa-plus-square') ->setDropdownMenu($actions)); return $crumbs; } protected function getEventAtIndexForGhostPHID($viewer, $phid, $index) { $result = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withInstanceSequencePairs( array( array( $phid, $index, ), )) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); return $result; } protected function createEventFromGhost($viewer, $event, $index) { $invitees = $event->getInvitees(); $new_ghost = $event->generateNthGhost($index, $viewer); $new_ghost->attachParentEvent($event); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $new_ghost ->setID(null) ->setPHID(null) ->removeViewerTimezone($viewer) ->save(); $ghost_invitees = array(); foreach ($invitees as $invitee) { $ghost_invitee = clone $invitee; $ghost_invitee ->setID(null) ->setEventPHID($new_ghost->getPHID()) ->save(); } unset($unguarded); return $new_ghost; } } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index f12bf9a9c3..2b3fc4bdb8 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') { + } + + 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 '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(); } }