diff --git a/resources/sql/autopatches/20160707.calendar.01.stub.sql b/resources/sql/autopatches/20160707.calendar.01.stub.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20160707.calendar.01.stub.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD isStub BOOL NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2022,7 +2022,6 @@ 'PhabricatorCalendarEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEditEngine.php', 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', - 'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php', 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 'PhabricatorCalendarEventEditProController' => 'applications/calendar/controller/PhabricatorCalendarEventEditProController.php', @@ -2994,7 +2993,6 @@ 'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php', 'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php', 'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php', - 'PhabricatorPeopleCalendarController' => 'applications/people/controller/PhabricatorPeopleCalendarController.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php', 'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php', @@ -6633,7 +6631,6 @@ 'PhabricatorFulltextInterface', ), 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', - 'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventEditProController' => 'ManiphestController', @@ -7741,7 +7738,6 @@ 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorPeopleApplication' => 'PhabricatorApplication', 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', - 'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleProfileController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController', 'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -40,7 +40,7 @@ public function getRoutes() { return array( - '/E(?P[1-9]\d*)(?:/(?P\d+))?' + '/E(?P[1-9]\d*)(?:/(?P\d+)/)?' => 'PhabricatorCalendarEventViewController', '/calendar/' => array( '(?:query/(?P[^/]+)/(?:(?P\d+)/'. @@ -51,15 +51,15 @@ => 'PhabricatorCalendarEventEditProController', 'create/' => 'PhabricatorCalendarEventEditController', - 'edit/(?P[1-9]\d*)/(?:(?P\d+)/)?' + 'edit/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventEditController', 'drag/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventDragController', - 'cancel/(?P[1-9]\d*)/(?:(?P\d+)/)?' + 'cancel/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCancelController', '(?Pjoin|decline|accept)/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventJoinController', - 'comment/(?P[1-9]\d*)/(?:(?P\d+)/)?' + 'comment/(?P[1-9]\d*)/' => 'PhabricatorCalendarEventCommentController', ), ), diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -30,49 +30,4 @@ 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) - ->setViewPolicy($event->getViewPolicy()) - ->setEditPolicy($event->getEditPolicy()) - ->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/controller/PhabricatorCalendarEventCancelController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php @@ -6,7 +6,6 @@ public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); - $sequence = $request->getURIData('sequence'); $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) @@ -17,40 +16,24 @@ PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); - - if ($sequence) { - $parent_event = $event; - $event = $parent_event->generateNthGhost($sequence, $viewer); - $event->attachParentEvent($parent_event); - } - if (!$event) { return new Aphront404Response(); } - if (!$sequence) { - $cancel_uri = '/E'.$event->getID(); - } else { - $cancel_uri = '/E'.$event->getID().'/'.$sequence; - } + $cancel_uri = $event->getURI(); + $is_parent = $event->isParentEvent(); + $is_child = $event->isChildEvent(); $is_cancelled = $event->getIsCancelled(); - $is_parent_cancelled = $event->getIsParentCancelled(); - $is_parent = $event->getIsRecurrenceParent(); - $validation_exception = null; + if ($is_child) { + $is_parent_cancelled = $event->getParentEvent()->getIsCancelled(); + } else { + $is_parent_cancelled = false; + } + $validation_exception = null; if ($request->isFormPost()) { - if ($is_cancelled && $sequence) { - return id(new AphrontRedirectResponse())->setURI($cancel_uri); - } else if ($sequence) { - $event = $this->createEventFromGhost( - $viewer, - $event, - $sequence); - $event->applyViewerTimezone($viewer); - } - $xactions = array(); $xaction = id(new PhabricatorCalendarEventTransaction()) @@ -73,43 +56,47 @@ } if ($is_cancelled) { - if ($sequence || $is_parent_cancelled) { + if ($is_parent_cancelled) { $title = pht('Cannot Reinstate Instance'); $paragraph = pht( - 'Cannot reinstate an instance of a cancelled recurring event.'); - $cancel = pht('Cancel'); + 'You cannot reinstate an instance of a cancelled recurring event.'); + $cancel = pht('Back'); $submit = null; + } else if ($is_child) { + $title = pht('Reinstate Instance'); + $paragraph = pht( + 'Reinstate this instance of this recurring event?'); + $cancel = pht('Back'); + $submit = pht('Reinstate Instance'); } else if ($is_parent) { - $title = pht('Reinstate Recurrence'); + $title = pht('Reinstate Recurring Event'); $paragraph = pht( - 'Reinstate all instances of this recurrence - that have not been individually cancelled?'); - $cancel = pht("Don't Reinstate Recurrence"); - $submit = pht('Reinstate Recurrence'); + 'Reinstate all instances of this recurring event which have not '. + 'been individually cancelled?'); + $cancel = pht('Back'); + $submit = pht('Reinstate Recurring Event'); } else { $title = pht('Reinstate Event'); $paragraph = pht('Reinstate this event?'); - $cancel = pht("Don't Reinstate Event"); + $cancel = pht('Back'); $submit = pht('Reinstate Event'); } } else { - if ($sequence) { + if ($is_child) { $title = pht('Cancel Instance'); - $paragraph = pht( - 'Cancel just this instance of a recurring event.'); - $cancel = pht("Don't Cancel Instance"); + $paragraph = pht('Cancel this instance of this recurring event?'); + $cancel = pht('Back'); $submit = pht('Cancel Instance'); } else if ($is_parent) { - $title = pht('Cancel Recurrence'); - $paragraph = pht( - 'Cancel the entire series of recurring events?'); - $cancel = pht("Don't Cancel Recurrence"); - $submit = pht('Cancel Recurrence'); + $title = pht('Cancel Recurrin Event'); + $paragraph = pht('Cancel this entire series of recurring events?'); + $cancel = pht('Back'); + $submit = pht('Cancel Recurring Event'); } else { $title = pht('Cancel Event'); $paragraph = pht( - 'You can always reinstate the event later.'); - $cancel = pht("Don't Cancel Event"); + 'Cancel this event? You can always reinstate the event later.'); + $cancel = pht('Back'); $submit = pht('Cancel Event'); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php deleted file mode 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php +++ /dev/null @@ -1,81 +0,0 @@ -isFormPost()) { - return new Aphront400Response(); - } - - $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - - $is_preview = $request->isPreviewRequest(); - $draft = PhabricatorDraft::buildFromRequest($request); - - $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$event) { - return new Aphront404Response(); - } - - $index = $request->getURIData('sequence'); - if ($index && !$is_preview) { - $result = $this->getEventAtIndexForGhostPHID( - $viewer, - $event->getPHID(), - $index); - - if ($result) { - $event = $result; - } else { - $event = $this->createEventFromGhost( - $viewer, - $event, - $index); - $event->applyViewerTimezone($viewer); - } - } - - $view_uri = '/'.$event->getMonogram(); - - $xactions = array(); - $xactions[] = id(new PhabricatorCalendarEventTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) - ->attachComment( - id(new PhabricatorCalendarEventTransactionComment()) - ->setContent($request->getStr('comment'))); - - $editor = id(new PhabricatorCalendarEventEditor()) - ->setActor($viewer) - ->setContinueOnNoEffect($request->isContinueRequest()) - ->setContentSourceFromRequest($request) - ->setIsPreview($is_preview); - - try { - $xactions = $editor->applyTransactions($event, $xactions); - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { - return id(new PhabricatorApplicationTransactionNoEffectResponse()) - ->setCancelURI($view_uri) - ->setException($ex); - } - - if ($draft) { - $draft->replaceOrDelete(); - } - - if ($request->isAjax() && $is_preview) { - return id(new PhabricatorApplicationTransactionResponse()) - ->setViewer($viewer) - ->setTransactions($xactions) - ->setIsPreview($is_preview); - } else { - return id(new AphrontRedirectResponse()) - ->setURI($view_uri); - } - } - -} 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 @@ -77,41 +77,18 @@ $cancel_uri = $this->getApplicationURI(); } else { $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withIDs(array($this->id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$event) { return new Aphront404Response(); } - if ($request->getURIData('sequence')) { - $index = $request->getURIData('sequence'); - - $result = $this->getEventAtIndexForGhostPHID( - $viewer, - $event->getPHID(), - $index); - - if ($result) { - return id(new AphrontRedirectResponse()) - ->setURI('/calendar/event/edit/'.$result->getID().'/'); - } - - $event = $this->createEventFromGhost( - $viewer, - $event, - $index); - - return id(new AphrontRedirectResponse()) - ->setURI('/calendar/event/edit/'.$event->getID().'/'); - } - $end_value = AphrontFormDateControlValue::newFromEpoch( $viewer, $event->getDateTo()); @@ -137,7 +114,7 @@ } } - $cancel_uri = '/'.$event->getMonogram(); + $cancel_uri = $event->getURI(); } if ($this->isCreate()) { @@ -153,7 +130,7 @@ $description = $event->getDescription(); $is_all_day = $event->getIsAllDay(); $is_recurring = $event->getIsRecurring(); - $is_parent = $event->getIsRecurrenceParent(); + $is_parent = $event->isParentEvent(); $frequency = idx($event->getRecurrenceFrequency(), 'rule'); $icon = $event->getIcon(); $edit_policy = $event->getEditPolicy(); diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php --- a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php @@ -20,12 +20,11 @@ ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); - if (!$event) { return new Aphront404Response(); } - $cancel_uri = '/E'.$event->getID(); + $cancel_uri = $event->getURI(); $validation_exception = null; $is_attending = $event->getIsUserAttending($viewer->getPHID()); 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 @@ -9,89 +9,48 @@ public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - $sequence = $request->getURIData('sequence'); - $timeline = null; - - $event = id(new PhabricatorCalendarEventQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); + $event = $this->loadEvent(); if (!$event) { return new Aphront404Response(); } - if ($sequence) { - $result = $this->getEventAtIndexForGhostPHID( - $viewer, - $event->getPHID(), - $sequence); - - if ($result) { - $parent_event = $event; - $event = $result; - $event->attachParentEvent($parent_event); - return id(new AphrontRedirectResponse()) - ->setURI('/E'.$result->getID()); - } else if ($sequence && $event->getIsRecurring()) { - $parent_event = $event; - $event = $event->generateNthGhost($sequence, $viewer); - $event->attachParentEvent($parent_event); - } else if ($sequence) { - return new Aphront404Response(); - } - - $title = $event->getMonogram().' ('.$sequence.')'; - $page_title = $title.' '.$event->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title, '/'.$event->getMonogram().'/'.$sequence); - - - } else { - $title = 'E'.$event->getID(); - $page_title = $title.' '.$event->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title); - $crumbs->setBorder(true); + // If we looked up or generated a stub event, redirect to that event's + // canonical URI. + $id = $request->getURIData('id'); + if ($event->getID() != $id) { + $uri = $event->getURI(); + return id(new AphrontRedirectResponse())->setURI($uri); } - if (!$event->getIsGhostEvent()) { - $timeline = $this->buildTransactionTimeline( - $event, - new PhabricatorCalendarEventTransactionQuery()); - } + $monogram = $event->getMonogram(); + $page_title = $monogram.' '.$event->getName(); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($monogram); + $crumbs->setBorder(true); + + $timeline = $this->buildTransactionTimeline( + $event, + new PhabricatorCalendarEventTransactionQuery()); $header = $this->buildHeaderView($event); $curtain = $this->buildCurtain($event); $details = $this->buildPropertySection($event); $description = $this->buildDescriptionView($event); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - $add_comment_header = $is_serious - ? pht('Add Comment') - : pht('Add To Plate'); - $draft = PhabricatorDraft::newFromUserAndKey($viewer, $event->getPHID()); - if ($sequence) { - $comment_uri = $this->getApplicationURI( - '/event/comment/'.$event->getID().'/'.$sequence.'/'); - } else { - $comment_uri = $this->getApplicationURI( - '/event/comment/'.$event->getID().'/'); - } - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($viewer) - ->setObjectPHID($event->getPHID()) - ->setDraft($draft) - ->setHeaderText($add_comment_header) - ->setAction($comment_uri) - ->setSubmitButtonName(pht('Add Comment')); + $comment_view = id(new PhabricatorCalendarEditEngine()) + ->setViewer($viewer) + ->buildEditEngineCommentView($event); + + $timeline->setQuoteRef($monogram); + $comment_view->setTransactionTimeline($timeline); $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setMainColumn(array( + ->setMainColumn( + array( $timeline, - $add_comment_form, + $comment_view, )) ->setCurtain($curtain) ->addPropertySection(pht('Details'), $details) @@ -101,10 +60,7 @@ ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($event->getPHID())) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } private function buildHeaderView( @@ -152,7 +108,7 @@ private function buildCurtain(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); - $is_cancelled = $event->getIsCancelled(); + $is_cancelled = $event->isCancelledEvent(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -160,19 +116,11 @@ $event, PhabricatorPolicyCapability::CAN_EDIT); - $edit_label = false; - $edit_uri = false; - - if ($event->getIsGhostEvent()) { - $index = $event->getSequenceIndex(); - $edit_label = pht('Edit This Instance'); - $edit_uri = "event/edit/{$id}/{$index}/"; - } else if ($event->getIsRecurrenceException()) { + $edit_uri = "event/edit/{$id}/"; + if ($event->isChildEvent()) { $edit_label = pht('Edit This Instance'); - $edit_uri = "event/edit/{$id}/"; } else { $edit_label = pht('Edit'); - $edit_uri = "event/edit/{$id}/"; } $curtain = $this->newCurtainView($event); @@ -204,28 +152,21 @@ } $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); + $cancel_disabled = !$can_edit; - if ($event->getIsGhostEvent()) { - $index = $event->getSequenceIndex(); - $can_reinstate = $event->getIsParentCancelled(); - + if ($event->isChildEvent()) { $cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate This Instance'); - $cancel_disabled = (!$can_edit || $can_reinstate); - $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/{$index}/"); - } else if ($event->getIsRecurrenceException()) { - $can_reinstate = $event->getIsParentCancelled(); - $cancel_label = pht('Cancel This Instance'); - $reinstate_label = pht('Reinstate This Instance'); - $cancel_disabled = (!$can_edit || $can_reinstate); - } else if ($event->getIsRecurrenceParent()) { + + if ($event->getParentEvent()->getIsCancelled()) { + $cancel_disabled = true; + } + } else if ($event->isParentEvent()) { $cancel_label = pht('Cancel All'); $reinstate_label = pht('Reinstate All'); - $cancel_disabled = !$can_edit; } else { $cancel_label = pht('Cancel Event'); $reinstate_label = pht('Reinstate Event'); - $cancel_disabled = !$can_edit; } if ($is_cancelled) { @@ -385,4 +326,68 @@ return null; } + + private function loadEvent() { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $id = $request->getURIData('id'); + $sequence = $request->getURIData('sequence'); + + // We're going to figure out which event you're trying to look at. Most of + // the time this is simple, but you may be looking at an instance of a + // recurring event which we haven't generated an object for. + + // If you are, we're going to generate a "stub" event so we have a real + // ID and PHID to work with, since the rest of the infrastructure relies + // on these identifiers existing. + + // Load the event identified by ID first. + $event = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$event) { + return null; + } + + // If we aren't looking at an instance of this event, this is a completely + // normal request and we can just return this event. + if (!$sequence) { + return $event; + } + + // When you view "E123/999", E123 is normally the parent event. However, + // you might visit a different instance first instead and then fiddle + // with the URI. If the event we're looking at is a child, we are going + // to act on the parent instead. + if ($event->isChildEvent()) { + $event = $event->getParentEvent(); + } + + // Try to load the instance. If it already exists, we're all done and + // can just return it. + $instance = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withInstanceSequencePairs( + array( + array($event->getPHID(), $sequence), + )) + ->executeOne(); + if ($instance) { + return $instance; + } + + if (!$viewer->isLoggedIn()) { + throw new Exception( + pht( + 'This event instance has not been created yet. Log in to create '. + 'it.')); + } + + $instance = $event->newStub($viewer, $sequence); + + return $instance; + } + } diff --git a/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php --- a/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEditEngine.php @@ -55,6 +55,10 @@ return $object->getURI(); } + protected function getEditorURI() { + return $this->getApplication()->getApplicationURI('event/editpro/'); + } + protected function buildCustomEditFields($object) { $fields = array( id(new PhabricatorTextEditField()) 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 @@ -11,6 +11,47 @@ return pht('Calendar'); } + protected function shouldApplyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + + protected function applyInitialEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + $actor = $this->requireActor(); + $object->removeViewerTimezone($actor); + + if ($object->getIsStub()) { + $this->materializeStub($object); + } + } + + private function materializeStub(PhabricatorCalendarEvent $event) { + if (!$event->getIsStub()) { + throw new Exception( + pht('Can not materialize an event stub: this event is not a stub.')); + } + + $actor = $this->getActor(); + $event->copyFromParent($actor); + $event->setIsStub(0); + + $invitees = $event->getParentEvent()->getInvitees(); + foreach ($invitees as $invitee) { + $invitee = id(new PhabricatorCalendarEventInvitee()) + ->setEventPHID($event->getPHID()) + ->setInviteePHID($invitee->getInviteePHID()) + ->setInviterPHID($invitee->getInviterPHID()) + ->setStatus($invitee->getStatus()) + ->save(); + } + + $event->save(); + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); @@ -196,15 +237,6 @@ return parent::applyCustomExternalTransaction($object, $xaction); } - protected function didApplyInternalEffects( - PhabricatorLiskDAO $object, - array $xactions) { - - $object->removeViewerTimezone($this->requireActor()); - - return $xactions; - } - protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { 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 @@ -12,6 +12,7 @@ private $isCancelled; private $eventsWithNoParent; private $instanceSequencePairs; + private $isStub; private $generateGhosts = false; @@ -55,6 +56,11 @@ return $this; } + public function withIsStub($is_stub) { + $this->isStub = $is_stub; + return $this; + } + public function withEventsWithNoParent($events_with_no_parent) { $this->eventsWithNoParent = $events_with_no_parent; return $this; @@ -183,7 +189,7 @@ $sequence_start = max(1, $sequence_start); for ($index = $sequence_start; $index < $sequence_end; $index++) { - $events[] = $event->generateNthGhost($index, $viewer); + $events[] = $event->newGhost($viewer, $index); } // NOTE: We're slicing results every time because this makes it cheaper @@ -201,40 +207,66 @@ } } - $map = array(); - $instance_sequence_pairs = array(); + // Now that we're done generating ghost events, we're going to remove any + // ghosts that we have concrete events for (or which we can load the + // concrete events for). These concrete events are generated when users + // edit a ghost, and replace the ghost events. - foreach ($events as $key => $event) { + // First, generate a map of all concrete events we + // already loaded. We don't need to load these again. + $have_pairs = array(); + foreach ($events as $event) { if ($event->getIsGhostEvent()) { - $index = $event->getSequenceIndex(); - $instance_sequence_pairs[] = array($event->getPHID(), $index); - $map[$event->getPHID()][$index] = $key; + continue; + } + + $parent_phid = $event->getInstanceOfEventPHID(); + $sequence = $event->getSequenceIndex(); + + $have_pairs[$parent_phid][$sequence] = true; + } + + // Now, generate a map of all events we generated + // ghosts for. We need to try to load these if we don't already have them. + $map = array(); + $parent_pairs = array(); + foreach ($events as $key => $event) { + if (!$event->getIsGhostEvent()) { + continue; + } + + $parent_phid = $event->getInstanceOfEventPHID(); + $sequence = $event->getSequenceIndex(); + + // We already loaded the concrete version of this event, so we can just + // throw out the ghost and move on. + if (isset($have_pairs[$parent_phid][$sequence])) { + unset($events[$key]); + continue; } + + // We didn't load the concrete version of this event, so we need to + // try to load it if it exists. + $parent_pairs[] = array($parent_phid, $sequence); + $map[$parent_phid][$sequence] = $key; } - if (count($instance_sequence_pairs) > 0) { - $sub_query = id(new PhabricatorCalendarEventQuery()) + if ($parent_pairs) { + $instances = id(new self()) ->setViewer($viewer) ->setParentQuery($this) - ->withInstanceSequencePairs($instance_sequence_pairs) + ->withInstanceSequencePairs($parent_pairs) ->execute(); - foreach ($sub_query as $edited_ghost) { - $indexes = idx($map, $edited_ghost->getInstanceOfEventPHID()); - $key = idx($indexes, $edited_ghost->getSequenceIndex()); - $events[$key] = $edited_ghost; - } + foreach ($instances as $instance) { + $parent_phid = $instance->getInstanceOfEventPHID(); + $sequence = $instance->getSequenceIndex(); - $id_map = array(); - foreach ($events as $key => $event) { - if ($event->getIsGhostEvent()) { - continue; - } - if (isset($id_map[$event->getID()])) { - unset($events[$key]); - } else { - $id_map[$event->getID()] = true; - } + $indexes = idx($map, $parent_phid); + $key = idx($indexes, $sequence); + + // Replace the ghost with the corresponding concrete event. + $events[$key] = $instance; } } @@ -329,6 +361,13 @@ implode(' OR ', $sql)); } + if ($this->isStub !== null) { + $where[] = qsprintf( + $conn, + 'event.isStub = %d', + (int)$this->isStub); + } + return $where; } 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 @@ -113,7 +113,15 @@ break; } - return $query->setGenerateGhosts(true); + // Generate ghosts (and ignore stub events) if we aren't querying for + // specific events. + if (!$map['ids'] && !$map['phids']) { + $query + ->withIsStub(false) + ->setGenerateGhosts(true); + } + + return $query; } private function getQueryDateRange( 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 @@ -22,6 +22,7 @@ protected $isAllDay; protected $icon; protected $mailKey; + protected $isStub; protected $isRecurring = 0; protected $recurrenceFrequency = array(); @@ -71,6 +72,7 @@ ->setUserPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) + ->setIsStub(0) ->setIsRecurring($is_recurring) ->setIcon(self::DEFAULT_ICON) ->setViewPolicy($view_policy) @@ -80,6 +82,116 @@ ->applyViewerTimezone($actor); } + private function newChild(PhabricatorUser $actor, $sequence) { + if (!$this->isParentEvent()) { + throw new Exception( + pht( + 'Unable to generate a new child event for an event which is not '. + 'a recurring parent event!')); + } + + $child = id(new self()) + ->setIsCancelled(0) + ->setIsStub(0) + ->setInstanceOfEventPHID($this->getPHID()) + ->setSequenceIndex($sequence) + ->setIsRecurring(true) + ->setRecurrenceFrequency($this->getRecurrenceFrequency()) + ->attachParentEvent($this); + + return $child->copyFromParent($actor); + } + + protected function readField($field) { + static $inherit = array( + 'userPHID' => true, + 'isAllDay' => true, + 'icon' => true, + 'spacePHID' => true, + 'viewPolicy' => true, + 'editPolicy' => true, + 'name' => true, + 'description' => true, + ); + + // Read these fields from the parent event instead of this event. For + // example, we want any changes to the parent event's name to + if (isset($inherit[$field])) { + if ($this->getIsStub()) { + // TODO: This should be unconditional, but the execution order of + // CalendarEventQuery and applyViewerTimezone() are currently odd. + if ($this->parentEvent !== self::ATTACHABLE) { + return $this->getParentEvent()->readField($field); + } + } + } + + return parent::readField($field); + } + + + public function copyFromParent(PhabricatorUser $actor) { + if (!$this->isChildEvent()) { + throw new Exception( + pht( + 'Unable to copy from parent event: this is not a child event.')); + } + + $parent = $this->getParentEvent(); + + $this + ->setUserPHID($parent->getUserPHID()) + ->setIsAllDay($parent->getIsAllDay()) + ->setIcon($parent->getIcon()) + ->setSpacePHID($parent->getSpacePHID()) + ->setViewPolicy($parent->getViewPolicy()) + ->setEditPolicy($parent->getEditPolicy()) + ->setName($parent->getName()) + ->setDescription($parent->getDescription()); + + $frequency = $parent->getFrequencyUnit(); + $modify_key = '+'.$this->getSequenceIndex().' '.$frequency; + + $date = $parent->getDateFrom(); + $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); + $date_time->modify($modify_key); + $date = $date_time->format('U'); + + $duration = $parent->getDateTo() - $parent->getDateFrom(); + + $this + ->setDateFrom($date) + ->setDateTo($date + $duration); + + return $this; + } + + public function newStub(PhabricatorUser $actor, $sequence) { + $stub = $this->newChild($actor, $sequence); + + $stub->setIsStub(1); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $stub->save(); + unset($unguarded); + + $stub->applyViewerTimezone($actor); + + return $stub; + } + + public function newGhost(PhabricatorUser $actor, $sequence) { + $ghost = $this->newChild($actor, $sequence); + + $ghost + ->setIsGhostEvent(true) + ->makeEphemeral(); + + $ghost->applyViewerTimezone($actor); + + return $ghost; + } + public function applyViewerTimezone(PhabricatorUser $viewer) { if ($this->appliedViewer) { throw new Exception(pht('Viewer timezone is already applied!')); @@ -211,6 +323,7 @@ 'recurrenceEndDate' => 'epoch?', 'instanceOfEventPHID' => 'phid?', 'sequenceIndex' => 'uint32?', + 'isStub' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'userPHID_dateFrom' => array( @@ -285,38 +398,6 @@ 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'); @@ -335,11 +416,13 @@ } public function getURI() { - $uri = '/'.$this->getMonogram(); - if ($this->isGhostEvent) { - $uri = $uri.'/'.$this->sequenceIndex; + if ($this->getIsGhostEvent()) { + $base = $this->getParentEvent()->getURI(); + $sequence = $this->getSequenceIndex(); + return "{$base}/{$sequence}/"; } - return $uri; + + return '/'.$this->getMonogram(); } public function getParentEvent() { @@ -351,37 +434,25 @@ return $this; } - public function getIsCancelled() { - $instance_of = $this->instanceOfEventPHID; - if ($instance_of != null && $this->getIsParentCancelled()) { - return true; - } - return $this->isCancelled; + public function isParentEvent() { + return ($this->isRecurring && !$this->instanceOfEventPHID); } - public function getIsRecurrenceParent() { - if ($this->isRecurring && !$this->instanceOfEventPHID) { - return true; - } - return false; + public function isChildEvent() { + return ($this->instanceOfEventPHID !== null); } - public function getIsRecurrenceException() { - if ($this->instanceOfEventPHID && !$this->isGhostEvent) { + public function isCancelledEvent() { + if ($this->getIsCancelled()) { return true; } - return false; - } - public function getIsParentCancelled() { - if ($this->instanceOfEventPHID == null) { - return false; + if ($this->isChildEvent()) { + if ($this->getParentEvent()->getIsCancelled()) { + return true; + } } - $recurring_event = $this->getParentEvent(); - if ($recurring_event->getIsCancelled()) { - return true; - } return false; } @@ -408,6 +479,7 @@ } } + /* -( Markup Interface )--------------------------------------------------- */ diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -314,6 +314,8 @@ $events = array(); if ($participant_phids) { + // TODO: All of this Calendar code is probably extra-broken, but none + // of it is currently reachable in the UI. $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($this->getViewer()) ->withInvitedPHIDs($participant_phids) diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -69,7 +69,6 @@ '' => 'PhabricatorPeopleProfileViewController', 'panel/' => $this->getPanelRouting('PhabricatorPeopleProfilePanelController'), - 'calendar/' => 'PhabricatorPeopleCalendarController', ), ); } diff --git a/src/applications/people/controller/PhabricatorPeopleCalendarController.php b/src/applications/people/controller/PhabricatorPeopleCalendarController.php deleted file mode 100644 --- a/src/applications/people/controller/PhabricatorPeopleCalendarController.php +++ /dev/null @@ -1,97 +0,0 @@ -getViewer(); - $username = $request->getURIData('username'); - - $user = id(new PhabricatorPeopleQuery()) - ->setViewer($viewer) - ->withUsernames(array($username)) - ->needProfileImage(true) - ->executeOne(); - if (!$user) { - return new Aphront404Response(); - } - - $this->setUser($user); - - $picture = $user->getProfileImageURI(); - - $now = time(); - $request = $this->getRequest(); - $year_d = phabricator_format_local_time($now, $user, 'Y'); - $year = $request->getInt('year', $year_d); - $month_d = phabricator_format_local_time($now, $user, 'm'); - $month = $request->getInt('month', $month_d); - $day = phabricator_format_local_time($now, $user, 'j'); - - $start_epoch = strtotime("{$year}-{$month}-01"); - $end_epoch = strtotime("{$year}-{$month}-01 next month"); - - $statuses = id(new PhabricatorCalendarEventQuery()) - ->setViewer($user) - ->withInvitedPHIDs(array($user->getPHID())) - ->withDateRange( - $start_epoch, - $end_epoch) - ->execute(); - - $start_range_value = AphrontFormDateControlValue::newFromEpoch( - $user, - $start_epoch); - $end_range_value = AphrontFormDateControlValue::newFromEpoch( - $user, - $end_epoch); - - if ($month == $month_d && $year == $year_d) { - $month_view = new PHUICalendarMonthView( - $start_range_value, - $end_range_value, - $month, - $year, - $day); - } else { - $month_view = new PHUICalendarMonthView( - $start_range_value, - $end_range_value, - $month, - $year); - } - - $month_view->setBrowseURI($request->getRequestURI()); - $month_view->setUser($user); - $month_view->setImage($picture); - - $phids = mpull($statuses, 'getUserPHID'); - $handles = $this->loadViewerHandles($phids); - - foreach ($statuses as $status) { - $event = new AphrontCalendarEventView(); - $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); - $event->setUserPHID($status->getUserPHID()); - $event->setName($status->getName()); - $event->setDescription($status->getDescription()); - $event->setEventID($status->getID()); - $month_view->addEvent($event); - } - - $nav = $this->getProfileMenu(); - $nav->selectFilter('calendar'); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Calendar')); - - return $this->newPage() - ->setTitle(pht('Calendar')) - ->setNavigation($nav) - ->setCrumbs($crumbs) - ->appendChild($month_view); - } -}