@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Generate "stub" events earlier, so more infrastructure works with Calendar

Summary:
Ref T9275. When you create a recurring event which recurs forever, we want to avoid writing an infinite number of rows to the database.

Currently, we write a row to the database right before you edit the event. Until then, we refer to it as `E123/999` or whatever ("instance 999 of event 123").

This creates a big mess with trying to make recurring events work with EditEngine, Subscriptions, Projects, Flags, Tokens, etc -- all of this stuff assumes that whatever you're working with has a PHID.

I poked at letting this stuff work without a PHID a little bit, but that looked like a gigantic mess.

Instead, generate an event "stub" a little sooner (when you look at the event detail page). This is basically just an ID/PHID to refer to the instance.

Then, when you edit the stub, "materialize" it into a real event.

This still has some issues, but I think it's more promising than the other approach was.

Also:

- Removes dead user profile calendar controller.
- Replaces comments with EditEngine comments.

Test Plan:
- Commented on a recurring event.
- Awarded tokens to a recurring event.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T9275

Differential Revision: https://secure.phabricator.com/D16248

+400 -501
+2
resources/sql/autopatches/20160707.calendar.01.stub.sql
··· 1 + ALTER TABLE {$NAMESPACE}_calendar.calendar_event 2 + ADD isStub BOOL NOT NULL;
-4
src/__phutil_library_map__.php
··· 2022 2022 'PhabricatorCalendarEditEngine' => 'applications/calendar/editor/PhabricatorCalendarEditEngine.php', 2023 2023 'PhabricatorCalendarEvent' => 'applications/calendar/storage/PhabricatorCalendarEvent.php', 2024 2024 'PhabricatorCalendarEventCancelController' => 'applications/calendar/controller/PhabricatorCalendarEventCancelController.php', 2025 - 'PhabricatorCalendarEventCommentController' => 'applications/calendar/controller/PhabricatorCalendarEventCommentController.php', 2026 2025 'PhabricatorCalendarEventDragController' => 'applications/calendar/controller/PhabricatorCalendarEventDragController.php', 2027 2026 'PhabricatorCalendarEventEditController' => 'applications/calendar/controller/PhabricatorCalendarEventEditController.php', 2028 2027 'PhabricatorCalendarEventEditProController' => 'applications/calendar/controller/PhabricatorCalendarEventEditProController.php', ··· 2994 2993 'PhabricatorPeopleAnyOwnerDatasource' => 'applications/people/typeahead/PhabricatorPeopleAnyOwnerDatasource.php', 2995 2994 'PhabricatorPeopleApplication' => 'applications/people/application/PhabricatorPeopleApplication.php', 2996 2995 'PhabricatorPeopleApproveController' => 'applications/people/controller/PhabricatorPeopleApproveController.php', 2997 - 'PhabricatorPeopleCalendarController' => 'applications/people/controller/PhabricatorPeopleCalendarController.php', 2998 2996 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 2999 2997 'PhabricatorPeopleCreateController' => 'applications/people/controller/PhabricatorPeopleCreateController.php', 3000 2998 'PhabricatorPeopleDatasource' => 'applications/people/typeahead/PhabricatorPeopleDatasource.php', ··· 6633 6631 'PhabricatorFulltextInterface', 6634 6632 ), 6635 6633 'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController', 6636 - 'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController', 6637 6634 'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController', 6638 6635 'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController', 6639 6636 'PhabricatorCalendarEventEditProController' => 'ManiphestController', ··· 7741 7738 'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 7742 7739 'PhabricatorPeopleApplication' => 'PhabricatorApplication', 7743 7740 'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController', 7744 - 'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleProfileController', 7745 7741 'PhabricatorPeopleController' => 'PhabricatorController', 7746 7742 'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController', 7747 7743 'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',
+4 -4
src/applications/calendar/application/PhabricatorCalendarApplication.php
··· 40 40 41 41 public function getRoutes() { 42 42 return array( 43 - '/E(?P<id>[1-9]\d*)(?:/(?P<sequence>\d+))?' 43 + '/E(?P<id>[1-9]\d*)(?:/(?P<sequence>\d+)/)?' 44 44 => 'PhabricatorCalendarEventViewController', 45 45 '/calendar/' => array( 46 46 '(?:query/(?P<queryKey>[^/]+)/(?:(?P<year>\d+)/'. ··· 51 51 => 'PhabricatorCalendarEventEditProController', 52 52 'create/' 53 53 => 'PhabricatorCalendarEventEditController', 54 - 'edit/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?' 54 + 'edit/(?P<id>[1-9]\d*)/' 55 55 => 'PhabricatorCalendarEventEditController', 56 56 'drag/(?P<id>[1-9]\d*)/' 57 57 => 'PhabricatorCalendarEventDragController', 58 - 'cancel/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?' 58 + 'cancel/(?P<id>[1-9]\d*)/' 59 59 => 'PhabricatorCalendarEventCancelController', 60 60 '(?P<action>join|decline|accept)/(?P<id>[1-9]\d*)/' 61 61 => 'PhabricatorCalendarEventJoinController', 62 - 'comment/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?' 62 + 'comment/(?P<id>[1-9]\d*)/' 63 63 => 'PhabricatorCalendarEventCommentController', 64 64 ), 65 65 ),
-45
src/applications/calendar/controller/PhabricatorCalendarController.php
··· 30 30 return $crumbs; 31 31 } 32 32 33 - protected function getEventAtIndexForGhostPHID($viewer, $phid, $index) { 34 - $result = id(new PhabricatorCalendarEventQuery()) 35 - ->setViewer($viewer) 36 - ->withInstanceSequencePairs( 37 - array( 38 - array( 39 - $phid, 40 - $index, 41 - ), 42 - )) 43 - ->requireCapabilities( 44 - array( 45 - PhabricatorPolicyCapability::CAN_VIEW, 46 - PhabricatorPolicyCapability::CAN_EDIT, 47 - )) 48 - ->executeOne(); 49 - 50 - return $result; 51 - } 52 - 53 - protected function createEventFromGhost($viewer, $event, $index) { 54 - $invitees = $event->getInvitees(); 55 - 56 - $new_ghost = $event->generateNthGhost($index, $viewer); 57 - $new_ghost->attachParentEvent($event); 58 - 59 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 60 - $new_ghost 61 - ->setID(null) 62 - ->setPHID(null) 63 - ->removeViewerTimezone($viewer) 64 - ->setViewPolicy($event->getViewPolicy()) 65 - ->setEditPolicy($event->getEditPolicy()) 66 - ->save(); 67 - $ghost_invitees = array(); 68 - foreach ($invitees as $invitee) { 69 - $ghost_invitee = clone $invitee; 70 - $ghost_invitee 71 - ->setID(null) 72 - ->setEventPHID($new_ghost->getPHID()) 73 - ->save(); 74 - } 75 - unset($unguarded); 76 - return $new_ghost; 77 - } 78 33 }
+33 -46
src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php
··· 6 6 public function handleRequest(AphrontRequest $request) { 7 7 $viewer = $request->getViewer(); 8 8 $id = $request->getURIData('id'); 9 - $sequence = $request->getURIData('sequence'); 10 9 11 10 $event = id(new PhabricatorCalendarEventQuery()) 12 11 ->setViewer($viewer) ··· 17 16 PhabricatorPolicyCapability::CAN_EDIT, 18 17 )) 19 18 ->executeOne(); 20 - 21 - if ($sequence) { 22 - $parent_event = $event; 23 - $event = $parent_event->generateNthGhost($sequence, $viewer); 24 - $event->attachParentEvent($parent_event); 25 - } 26 - 27 19 if (!$event) { 28 20 return new Aphront404Response(); 29 21 } 30 22 31 - if (!$sequence) { 32 - $cancel_uri = '/E'.$event->getID(); 33 - } else { 34 - $cancel_uri = '/E'.$event->getID().'/'.$sequence; 35 - } 23 + $cancel_uri = $event->getURI(); 36 24 25 + $is_parent = $event->isParentEvent(); 26 + $is_child = $event->isChildEvent(); 37 27 $is_cancelled = $event->getIsCancelled(); 38 - $is_parent_cancelled = $event->getIsParentCancelled(); 39 - $is_parent = $event->getIsRecurrenceParent(); 40 28 41 - $validation_exception = null; 29 + if ($is_child) { 30 + $is_parent_cancelled = $event->getParentEvent()->getIsCancelled(); 31 + } else { 32 + $is_parent_cancelled = false; 33 + } 42 34 35 + $validation_exception = null; 43 36 if ($request->isFormPost()) { 44 - if ($is_cancelled && $sequence) { 45 - return id(new AphrontRedirectResponse())->setURI($cancel_uri); 46 - } else if ($sequence) { 47 - $event = $this->createEventFromGhost( 48 - $viewer, 49 - $event, 50 - $sequence); 51 - $event->applyViewerTimezone($viewer); 52 - } 53 - 54 37 $xactions = array(); 55 38 56 39 $xaction = id(new PhabricatorCalendarEventTransaction()) ··· 73 56 } 74 57 75 58 if ($is_cancelled) { 76 - if ($sequence || $is_parent_cancelled) { 59 + if ($is_parent_cancelled) { 77 60 $title = pht('Cannot Reinstate Instance'); 78 61 $paragraph = pht( 79 - 'Cannot reinstate an instance of a cancelled recurring event.'); 80 - $cancel = pht('Cancel'); 62 + 'You cannot reinstate an instance of a cancelled recurring event.'); 63 + $cancel = pht('Back'); 81 64 $submit = null; 65 + } else if ($is_child) { 66 + $title = pht('Reinstate Instance'); 67 + $paragraph = pht( 68 + 'Reinstate this instance of this recurring event?'); 69 + $cancel = pht('Back'); 70 + $submit = pht('Reinstate Instance'); 82 71 } else if ($is_parent) { 83 - $title = pht('Reinstate Recurrence'); 72 + $title = pht('Reinstate Recurring Event'); 84 73 $paragraph = pht( 85 - 'Reinstate all instances of this recurrence 86 - that have not been individually cancelled?'); 87 - $cancel = pht("Don't Reinstate Recurrence"); 88 - $submit = pht('Reinstate Recurrence'); 74 + 'Reinstate all instances of this recurring event which have not '. 75 + 'been individually cancelled?'); 76 + $cancel = pht('Back'); 77 + $submit = pht('Reinstate Recurring Event'); 89 78 } else { 90 79 $title = pht('Reinstate Event'); 91 80 $paragraph = pht('Reinstate this event?'); 92 - $cancel = pht("Don't Reinstate Event"); 81 + $cancel = pht('Back'); 93 82 $submit = pht('Reinstate Event'); 94 83 } 95 84 } else { 96 - if ($sequence) { 85 + if ($is_child) { 97 86 $title = pht('Cancel Instance'); 98 - $paragraph = pht( 99 - 'Cancel just this instance of a recurring event.'); 100 - $cancel = pht("Don't Cancel Instance"); 87 + $paragraph = pht('Cancel this instance of this recurring event?'); 88 + $cancel = pht('Back'); 101 89 $submit = pht('Cancel Instance'); 102 90 } else if ($is_parent) { 103 - $title = pht('Cancel Recurrence'); 104 - $paragraph = pht( 105 - 'Cancel the entire series of recurring events?'); 106 - $cancel = pht("Don't Cancel Recurrence"); 107 - $submit = pht('Cancel Recurrence'); 91 + $title = pht('Cancel Recurrin Event'); 92 + $paragraph = pht('Cancel this entire series of recurring events?'); 93 + $cancel = pht('Back'); 94 + $submit = pht('Cancel Recurring Event'); 108 95 } else { 109 96 $title = pht('Cancel Event'); 110 97 $paragraph = pht( 111 - 'You can always reinstate the event later.'); 112 - $cancel = pht("Don't Cancel Event"); 98 + 'Cancel this event? You can always reinstate the event later.'); 99 + $cancel = pht('Back'); 113 100 $submit = pht('Cancel Event'); 114 101 } 115 102 }
-81
src/applications/calendar/controller/PhabricatorCalendarEventCommentController.php
··· 1 - <?php 2 - 3 - final class PhabricatorCalendarEventCommentController 4 - extends PhabricatorCalendarController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - if (!$request->isFormPost()) { 8 - return new Aphront400Response(); 9 - } 10 - 11 - $viewer = $request->getViewer(); 12 - $id = $request->getURIData('id'); 13 - 14 - $is_preview = $request->isPreviewRequest(); 15 - $draft = PhabricatorDraft::buildFromRequest($request); 16 - 17 - $event = id(new PhabricatorCalendarEventQuery()) 18 - ->setViewer($viewer) 19 - ->withIDs(array($id)) 20 - ->executeOne(); 21 - if (!$event) { 22 - return new Aphront404Response(); 23 - } 24 - 25 - $index = $request->getURIData('sequence'); 26 - if ($index && !$is_preview) { 27 - $result = $this->getEventAtIndexForGhostPHID( 28 - $viewer, 29 - $event->getPHID(), 30 - $index); 31 - 32 - if ($result) { 33 - $event = $result; 34 - } else { 35 - $event = $this->createEventFromGhost( 36 - $viewer, 37 - $event, 38 - $index); 39 - $event->applyViewerTimezone($viewer); 40 - } 41 - } 42 - 43 - $view_uri = '/'.$event->getMonogram(); 44 - 45 - $xactions = array(); 46 - $xactions[] = id(new PhabricatorCalendarEventTransaction()) 47 - ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) 48 - ->attachComment( 49 - id(new PhabricatorCalendarEventTransactionComment()) 50 - ->setContent($request->getStr('comment'))); 51 - 52 - $editor = id(new PhabricatorCalendarEventEditor()) 53 - ->setActor($viewer) 54 - ->setContinueOnNoEffect($request->isContinueRequest()) 55 - ->setContentSourceFromRequest($request) 56 - ->setIsPreview($is_preview); 57 - 58 - try { 59 - $xactions = $editor->applyTransactions($event, $xactions); 60 - } catch (PhabricatorApplicationTransactionNoEffectException $ex) { 61 - return id(new PhabricatorApplicationTransactionNoEffectResponse()) 62 - ->setCancelURI($view_uri) 63 - ->setException($ex); 64 - } 65 - 66 - if ($draft) { 67 - $draft->replaceOrDelete(); 68 - } 69 - 70 - if ($request->isAjax() && $is_preview) { 71 - return id(new PhabricatorApplicationTransactionResponse()) 72 - ->setViewer($viewer) 73 - ->setTransactions($xactions) 74 - ->setIsPreview($is_preview); 75 - } else { 76 - return id(new AphrontRedirectResponse()) 77 - ->setURI($view_uri); 78 - } 79 - } 80 - 81 - }
+10 -33
src/applications/calendar/controller/PhabricatorCalendarEventEditController.php
··· 77 77 $cancel_uri = $this->getApplicationURI(); 78 78 } else { 79 79 $event = id(new PhabricatorCalendarEventQuery()) 80 - ->setViewer($viewer) 81 - ->withIDs(array($this->id)) 82 - ->requireCapabilities( 83 - array( 84 - PhabricatorPolicyCapability::CAN_VIEW, 85 - PhabricatorPolicyCapability::CAN_EDIT, 86 - )) 87 - ->executeOne(); 88 - 80 + ->setViewer($viewer) 81 + ->withIDs(array($this->id)) 82 + ->requireCapabilities( 83 + array( 84 + PhabricatorPolicyCapability::CAN_VIEW, 85 + PhabricatorPolicyCapability::CAN_EDIT, 86 + )) 87 + ->executeOne(); 89 88 if (!$event) { 90 89 return new Aphront404Response(); 91 90 } 92 91 93 - if ($request->getURIData('sequence')) { 94 - $index = $request->getURIData('sequence'); 95 - 96 - $result = $this->getEventAtIndexForGhostPHID( 97 - $viewer, 98 - $event->getPHID(), 99 - $index); 100 - 101 - if ($result) { 102 - return id(new AphrontRedirectResponse()) 103 - ->setURI('/calendar/event/edit/'.$result->getID().'/'); 104 - } 105 - 106 - $event = $this->createEventFromGhost( 107 - $viewer, 108 - $event, 109 - $index); 110 - 111 - return id(new AphrontRedirectResponse()) 112 - ->setURI('/calendar/event/edit/'.$event->getID().'/'); 113 - } 114 - 115 92 $end_value = AphrontFormDateControlValue::newFromEpoch( 116 93 $viewer, 117 94 $event->getDateTo()); ··· 137 114 } 138 115 } 139 116 140 - $cancel_uri = '/'.$event->getMonogram(); 117 + $cancel_uri = $event->getURI(); 141 118 } 142 119 143 120 if ($this->isCreate()) { ··· 153 130 $description = $event->getDescription(); 154 131 $is_all_day = $event->getIsAllDay(); 155 132 $is_recurring = $event->getIsRecurring(); 156 - $is_parent = $event->getIsRecurrenceParent(); 133 + $is_parent = $event->isParentEvent(); 157 134 $frequency = idx($event->getRecurrenceFrequency(), 'rule'); 158 135 $icon = $event->getIcon(); 159 136 $edit_policy = $event->getEditPolicy();
+1 -2
src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php
··· 20 20 ->setViewer($viewer) 21 21 ->withIDs(array($id)) 22 22 ->executeOne(); 23 - 24 23 if (!$event) { 25 24 return new Aphront404Response(); 26 25 } 27 26 28 - $cancel_uri = '/E'.$event->getID(); 27 + $cancel_uri = $event->getURI(); 29 28 $validation_exception = null; 30 29 31 30 $is_attending = $event->getIsUserAttending($viewer->getPHID());
+100 -95
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 9 9 10 10 public function handleRequest(AphrontRequest $request) { 11 11 $viewer = $request->getViewer(); 12 - $id = $request->getURIData('id'); 13 - $sequence = $request->getURIData('sequence'); 14 12 15 - $timeline = null; 16 - 17 - $event = id(new PhabricatorCalendarEventQuery()) 18 - ->setViewer($viewer) 19 - ->withIDs(array($id)) 20 - ->executeOne(); 13 + $event = $this->loadEvent(); 21 14 if (!$event) { 22 15 return new Aphront404Response(); 23 16 } 24 17 25 - if ($sequence) { 26 - $result = $this->getEventAtIndexForGhostPHID( 27 - $viewer, 28 - $event->getPHID(), 29 - $sequence); 30 - 31 - if ($result) { 32 - $parent_event = $event; 33 - $event = $result; 34 - $event->attachParentEvent($parent_event); 35 - return id(new AphrontRedirectResponse()) 36 - ->setURI('/E'.$result->getID()); 37 - } else if ($sequence && $event->getIsRecurring()) { 38 - $parent_event = $event; 39 - $event = $event->generateNthGhost($sequence, $viewer); 40 - $event->attachParentEvent($parent_event); 41 - } else if ($sequence) { 42 - return new Aphront404Response(); 43 - } 44 - 45 - $title = $event->getMonogram().' ('.$sequence.')'; 46 - $page_title = $title.' '.$event->getName(); 47 - $crumbs = $this->buildApplicationCrumbs(); 48 - $crumbs->addTextCrumb($title, '/'.$event->getMonogram().'/'.$sequence); 49 - 50 - 51 - } else { 52 - $title = 'E'.$event->getID(); 53 - $page_title = $title.' '.$event->getName(); 54 - $crumbs = $this->buildApplicationCrumbs(); 55 - $crumbs->addTextCrumb($title); 56 - $crumbs->setBorder(true); 18 + // If we looked up or generated a stub event, redirect to that event's 19 + // canonical URI. 20 + $id = $request->getURIData('id'); 21 + if ($event->getID() != $id) { 22 + $uri = $event->getURI(); 23 + return id(new AphrontRedirectResponse())->setURI($uri); 57 24 } 58 25 59 - if (!$event->getIsGhostEvent()) { 60 - $timeline = $this->buildTransactionTimeline( 61 - $event, 62 - new PhabricatorCalendarEventTransactionQuery()); 63 - } 26 + $monogram = $event->getMonogram(); 27 + $page_title = $monogram.' '.$event->getName(); 28 + $crumbs = $this->buildApplicationCrumbs(); 29 + $crumbs->addTextCrumb($monogram); 30 + $crumbs->setBorder(true); 31 + 32 + $timeline = $this->buildTransactionTimeline( 33 + $event, 34 + new PhabricatorCalendarEventTransactionQuery()); 64 35 65 36 $header = $this->buildHeaderView($event); 66 37 $curtain = $this->buildCurtain($event); 67 38 $details = $this->buildPropertySection($event); 68 39 $description = $this->buildDescriptionView($event); 69 40 70 - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 71 - $add_comment_header = $is_serious 72 - ? pht('Add Comment') 73 - : pht('Add To Plate'); 74 - $draft = PhabricatorDraft::newFromUserAndKey($viewer, $event->getPHID()); 75 - if ($sequence) { 76 - $comment_uri = $this->getApplicationURI( 77 - '/event/comment/'.$event->getID().'/'.$sequence.'/'); 78 - } else { 79 - $comment_uri = $this->getApplicationURI( 80 - '/event/comment/'.$event->getID().'/'); 81 - } 82 - $add_comment_form = id(new PhabricatorApplicationTransactionCommentView()) 83 - ->setUser($viewer) 84 - ->setObjectPHID($event->getPHID()) 85 - ->setDraft($draft) 86 - ->setHeaderText($add_comment_header) 87 - ->setAction($comment_uri) 88 - ->setSubmitButtonName(pht('Add Comment')); 41 + $comment_view = id(new PhabricatorCalendarEditEngine()) 42 + ->setViewer($viewer) 43 + ->buildEditEngineCommentView($event); 44 + 45 + $timeline->setQuoteRef($monogram); 46 + $comment_view->setTransactionTimeline($timeline); 89 47 90 48 $view = id(new PHUITwoColumnView()) 91 49 ->setHeader($header) 92 - ->setMainColumn(array( 50 + ->setMainColumn( 51 + array( 93 52 $timeline, 94 - $add_comment_form, 53 + $comment_view, 95 54 )) 96 55 ->setCurtain($curtain) 97 56 ->addPropertySection(pht('Details'), $details) ··· 101 60 ->setTitle($page_title) 102 61 ->setCrumbs($crumbs) 103 62 ->setPageObjectPHIDs(array($event->getPHID())) 104 - ->appendChild( 105 - array( 106 - $view, 107 - )); 63 + ->appendChild($view); 108 64 } 109 65 110 66 private function buildHeaderView( ··· 152 108 private function buildCurtain(PhabricatorCalendarEvent $event) { 153 109 $viewer = $this->getRequest()->getUser(); 154 110 $id = $event->getID(); 155 - $is_cancelled = $event->getIsCancelled(); 111 + $is_cancelled = $event->isCancelledEvent(); 156 112 $is_attending = $event->getIsUserAttending($viewer->getPHID()); 157 113 158 114 $can_edit = PhabricatorPolicyFilter::hasCapability( ··· 160 116 $event, 161 117 PhabricatorPolicyCapability::CAN_EDIT); 162 118 163 - $edit_label = false; 164 - $edit_uri = false; 165 - 166 - if ($event->getIsGhostEvent()) { 167 - $index = $event->getSequenceIndex(); 168 - $edit_label = pht('Edit This Instance'); 169 - $edit_uri = "event/edit/{$id}/{$index}/"; 170 - } else if ($event->getIsRecurrenceException()) { 119 + $edit_uri = "event/edit/{$id}/"; 120 + if ($event->isChildEvent()) { 171 121 $edit_label = pht('Edit This Instance'); 172 - $edit_uri = "event/edit/{$id}/"; 173 122 } else { 174 123 $edit_label = pht('Edit'); 175 - $edit_uri = "event/edit/{$id}/"; 176 124 } 177 125 178 126 $curtain = $this->newCurtainView($event); ··· 204 152 } 205 153 206 154 $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); 155 + $cancel_disabled = !$can_edit; 207 156 208 - if ($event->getIsGhostEvent()) { 209 - $index = $event->getSequenceIndex(); 210 - $can_reinstate = $event->getIsParentCancelled(); 211 - 157 + if ($event->isChildEvent()) { 212 158 $cancel_label = pht('Cancel This Instance'); 213 159 $reinstate_label = pht('Reinstate This Instance'); 214 - $cancel_disabled = (!$can_edit || $can_reinstate); 215 - $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/{$index}/"); 216 - } else if ($event->getIsRecurrenceException()) { 217 - $can_reinstate = $event->getIsParentCancelled(); 218 - $cancel_label = pht('Cancel This Instance'); 219 - $reinstate_label = pht('Reinstate This Instance'); 220 - $cancel_disabled = (!$can_edit || $can_reinstate); 221 - } else if ($event->getIsRecurrenceParent()) { 160 + 161 + if ($event->getParentEvent()->getIsCancelled()) { 162 + $cancel_disabled = true; 163 + } 164 + } else if ($event->isParentEvent()) { 222 165 $cancel_label = pht('Cancel All'); 223 166 $reinstate_label = pht('Reinstate All'); 224 - $cancel_disabled = !$can_edit; 225 167 } else { 226 168 $cancel_label = pht('Cancel Event'); 227 169 $reinstate_label = pht('Reinstate Event'); 228 - $cancel_disabled = !$can_edit; 229 170 } 230 171 231 172 if ($is_cancelled) { ··· 383 324 } 384 325 385 326 return null; 327 + } 328 + 329 + 330 + private function loadEvent() { 331 + $request = $this->getRequest(); 332 + $viewer = $this->getViewer(); 333 + 334 + $id = $request->getURIData('id'); 335 + $sequence = $request->getURIData('sequence'); 336 + 337 + // We're going to figure out which event you're trying to look at. Most of 338 + // the time this is simple, but you may be looking at an instance of a 339 + // recurring event which we haven't generated an object for. 340 + 341 + // If you are, we're going to generate a "stub" event so we have a real 342 + // ID and PHID to work with, since the rest of the infrastructure relies 343 + // on these identifiers existing. 344 + 345 + // Load the event identified by ID first. 346 + $event = id(new PhabricatorCalendarEventQuery()) 347 + ->setViewer($viewer) 348 + ->withIDs(array($id)) 349 + ->executeOne(); 350 + if (!$event) { 351 + return null; 352 + } 353 + 354 + // If we aren't looking at an instance of this event, this is a completely 355 + // normal request and we can just return this event. 356 + if (!$sequence) { 357 + return $event; 358 + } 359 + 360 + // When you view "E123/999", E123 is normally the parent event. However, 361 + // you might visit a different instance first instead and then fiddle 362 + // with the URI. If the event we're looking at is a child, we are going 363 + // to act on the parent instead. 364 + if ($event->isChildEvent()) { 365 + $event = $event->getParentEvent(); 366 + } 367 + 368 + // Try to load the instance. If it already exists, we're all done and 369 + // can just return it. 370 + $instance = id(new PhabricatorCalendarEventQuery()) 371 + ->setViewer($viewer) 372 + ->withInstanceSequencePairs( 373 + array( 374 + array($event->getPHID(), $sequence), 375 + )) 376 + ->executeOne(); 377 + if ($instance) { 378 + return $instance; 379 + } 380 + 381 + if (!$viewer->isLoggedIn()) { 382 + throw new Exception( 383 + pht( 384 + 'This event instance has not been created yet. Log in to create '. 385 + 'it.')); 386 + } 387 + 388 + $instance = $event->newStub($viewer, $sequence); 389 + 390 + return $instance; 386 391 } 387 392 388 393 }
+4
src/applications/calendar/editor/PhabricatorCalendarEditEngine.php
··· 55 55 return $object->getURI(); 56 56 } 57 57 58 + protected function getEditorURI() { 59 + return $this->getApplication()->getApplicationURI('event/editpro/'); 60 + } 61 + 58 62 protected function buildCustomEditFields($object) { 59 63 $fields = array( 60 64 id(new PhabricatorTextEditField())
+41 -9
src/applications/calendar/editor/PhabricatorCalendarEventEditor.php
··· 11 11 return pht('Calendar'); 12 12 } 13 13 14 + protected function shouldApplyInitialEffects( 15 + PhabricatorLiskDAO $object, 16 + array $xactions) { 17 + return true; 18 + } 19 + 20 + protected function applyInitialEffects( 21 + PhabricatorLiskDAO $object, 22 + array $xactions) { 23 + 24 + $actor = $this->requireActor(); 25 + $object->removeViewerTimezone($actor); 26 + 27 + if ($object->getIsStub()) { 28 + $this->materializeStub($object); 29 + } 30 + } 31 + 32 + private function materializeStub(PhabricatorCalendarEvent $event) { 33 + if (!$event->getIsStub()) { 34 + throw new Exception( 35 + pht('Can not materialize an event stub: this event is not a stub.')); 36 + } 37 + 38 + $actor = $this->getActor(); 39 + $event->copyFromParent($actor); 40 + $event->setIsStub(0); 41 + 42 + $invitees = $event->getParentEvent()->getInvitees(); 43 + foreach ($invitees as $invitee) { 44 + $invitee = id(new PhabricatorCalendarEventInvitee()) 45 + ->setEventPHID($event->getPHID()) 46 + ->setInviteePHID($invitee->getInviteePHID()) 47 + ->setInviterPHID($invitee->getInviterPHID()) 48 + ->setStatus($invitee->getStatus()) 49 + ->save(); 50 + } 51 + 52 + $event->save(); 53 + } 54 + 14 55 public function getTransactionTypes() { 15 56 $types = parent::getTransactionTypes(); 16 57 ··· 194 235 } 195 236 196 237 return parent::applyCustomExternalTransaction($object, $xaction); 197 - } 198 - 199 - protected function didApplyInternalEffects( 200 - PhabricatorLiskDAO $object, 201 - array $xactions) { 202 - 203 - $object->removeViewerTimezone($this->requireActor()); 204 - 205 - return $xactions; 206 238 } 207 239 208 240 protected function applyFinalEffects(
+64 -25
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 12 12 private $isCancelled; 13 13 private $eventsWithNoParent; 14 14 private $instanceSequencePairs; 15 + private $isStub; 15 16 16 17 private $generateGhosts = false; 17 18 ··· 52 53 53 54 public function withIsCancelled($is_cancelled) { 54 55 $this->isCancelled = $is_cancelled; 56 + return $this; 57 + } 58 + 59 + public function withIsStub($is_stub) { 60 + $this->isStub = $is_stub; 55 61 return $this; 56 62 } 57 63 ··· 183 189 $sequence_start = max(1, $sequence_start); 184 190 185 191 for ($index = $sequence_start; $index < $sequence_end; $index++) { 186 - $events[] = $event->generateNthGhost($index, $viewer); 192 + $events[] = $event->newGhost($viewer, $index); 187 193 } 188 194 189 195 // NOTE: We're slicing results every time because this makes it cheaper ··· 201 207 } 202 208 } 203 209 210 + // Now that we're done generating ghost events, we're going to remove any 211 + // ghosts that we have concrete events for (or which we can load the 212 + // concrete events for). These concrete events are generated when users 213 + // edit a ghost, and replace the ghost events. 214 + 215 + // First, generate a map of all concrete <parentPHID, sequence> events we 216 + // already loaded. We don't need to load these again. 217 + $have_pairs = array(); 218 + foreach ($events as $event) { 219 + if ($event->getIsGhostEvent()) { 220 + continue; 221 + } 222 + 223 + $parent_phid = $event->getInstanceOfEventPHID(); 224 + $sequence = $event->getSequenceIndex(); 225 + 226 + $have_pairs[$parent_phid][$sequence] = true; 227 + } 228 + 229 + // Now, generate a map of all <parentPHID, sequence> events we generated 230 + // ghosts for. We need to try to load these if we don't already have them. 204 231 $map = array(); 205 - $instance_sequence_pairs = array(); 232 + $parent_pairs = array(); 233 + foreach ($events as $key => $event) { 234 + if (!$event->getIsGhostEvent()) { 235 + continue; 236 + } 237 + 238 + $parent_phid = $event->getInstanceOfEventPHID(); 239 + $sequence = $event->getSequenceIndex(); 206 240 207 - foreach ($events as $key => $event) { 208 - if ($event->getIsGhostEvent()) { 209 - $index = $event->getSequenceIndex(); 210 - $instance_sequence_pairs[] = array($event->getPHID(), $index); 211 - $map[$event->getPHID()][$index] = $key; 241 + // We already loaded the concrete version of this event, so we can just 242 + // throw out the ghost and move on. 243 + if (isset($have_pairs[$parent_phid][$sequence])) { 244 + unset($events[$key]); 245 + continue; 212 246 } 247 + 248 + // We didn't load the concrete version of this event, so we need to 249 + // try to load it if it exists. 250 + $parent_pairs[] = array($parent_phid, $sequence); 251 + $map[$parent_phid][$sequence] = $key; 213 252 } 214 253 215 - if (count($instance_sequence_pairs) > 0) { 216 - $sub_query = id(new PhabricatorCalendarEventQuery()) 254 + if ($parent_pairs) { 255 + $instances = id(new self()) 217 256 ->setViewer($viewer) 218 257 ->setParentQuery($this) 219 - ->withInstanceSequencePairs($instance_sequence_pairs) 258 + ->withInstanceSequencePairs($parent_pairs) 220 259 ->execute(); 221 260 222 - foreach ($sub_query as $edited_ghost) { 223 - $indexes = idx($map, $edited_ghost->getInstanceOfEventPHID()); 224 - $key = idx($indexes, $edited_ghost->getSequenceIndex()); 225 - $events[$key] = $edited_ghost; 226 - } 261 + foreach ($instances as $instance) { 262 + $parent_phid = $instance->getInstanceOfEventPHID(); 263 + $sequence = $instance->getSequenceIndex(); 264 + 265 + $indexes = idx($map, $parent_phid); 266 + $key = idx($indexes, $sequence); 227 267 228 - $id_map = array(); 229 - foreach ($events as $key => $event) { 230 - if ($event->getIsGhostEvent()) { 231 - continue; 232 - } 233 - if (isset($id_map[$event->getID()])) { 234 - unset($events[$key]); 235 - } else { 236 - $id_map[$event->getID()] = true; 237 - } 268 + // Replace the ghost with the corresponding concrete event. 269 + $events[$key] = $instance; 238 270 } 239 271 } 240 272 ··· 327 359 $conn, 328 360 '%Q', 329 361 implode(' OR ', $sql)); 362 + } 363 + 364 + if ($this->isStub !== null) { 365 + $where[] = qsprintf( 366 + $conn, 367 + 'event.isStub = %d', 368 + (int)$this->isStub); 330 369 } 331 370 332 371 return $where;
+9 -1
src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php
··· 113 113 break; 114 114 } 115 115 116 - return $query->setGenerateGhosts(true); 116 + // Generate ghosts (and ignore stub events) if we aren't querying for 117 + // specific events. 118 + if (!$map['ids'] && !$map['phids']) { 119 + $query 120 + ->withIsStub(false) 121 + ->setGenerateGhosts(true); 122 + } 123 + 124 + return $query; 117 125 } 118 126 119 127 private function getQueryDateRange(
+130 -58
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 22 22 protected $isAllDay; 23 23 protected $icon; 24 24 protected $mailKey; 25 + protected $isStub; 25 26 26 27 protected $isRecurring = 0; 27 28 protected $recurrenceFrequency = array(); ··· 71 72 ->setUserPHID($actor->getPHID()) 72 73 ->setIsCancelled(0) 73 74 ->setIsAllDay(0) 75 + ->setIsStub(0) 74 76 ->setIsRecurring($is_recurring) 75 77 ->setIcon(self::DEFAULT_ICON) 76 78 ->setViewPolicy($view_policy) ··· 80 82 ->applyViewerTimezone($actor); 81 83 } 82 84 85 + private function newChild(PhabricatorUser $actor, $sequence) { 86 + if (!$this->isParentEvent()) { 87 + throw new Exception( 88 + pht( 89 + 'Unable to generate a new child event for an event which is not '. 90 + 'a recurring parent event!')); 91 + } 92 + 93 + $child = id(new self()) 94 + ->setIsCancelled(0) 95 + ->setIsStub(0) 96 + ->setInstanceOfEventPHID($this->getPHID()) 97 + ->setSequenceIndex($sequence) 98 + ->setIsRecurring(true) 99 + ->setRecurrenceFrequency($this->getRecurrenceFrequency()) 100 + ->attachParentEvent($this); 101 + 102 + return $child->copyFromParent($actor); 103 + } 104 + 105 + protected function readField($field) { 106 + static $inherit = array( 107 + 'userPHID' => true, 108 + 'isAllDay' => true, 109 + 'icon' => true, 110 + 'spacePHID' => true, 111 + 'viewPolicy' => true, 112 + 'editPolicy' => true, 113 + 'name' => true, 114 + 'description' => true, 115 + ); 116 + 117 + // Read these fields from the parent event instead of this event. For 118 + // example, we want any changes to the parent event's name to 119 + if (isset($inherit[$field])) { 120 + if ($this->getIsStub()) { 121 + // TODO: This should be unconditional, but the execution order of 122 + // CalendarEventQuery and applyViewerTimezone() are currently odd. 123 + if ($this->parentEvent !== self::ATTACHABLE) { 124 + return $this->getParentEvent()->readField($field); 125 + } 126 + } 127 + } 128 + 129 + return parent::readField($field); 130 + } 131 + 132 + 133 + public function copyFromParent(PhabricatorUser $actor) { 134 + if (!$this->isChildEvent()) { 135 + throw new Exception( 136 + pht( 137 + 'Unable to copy from parent event: this is not a child event.')); 138 + } 139 + 140 + $parent = $this->getParentEvent(); 141 + 142 + $this 143 + ->setUserPHID($parent->getUserPHID()) 144 + ->setIsAllDay($parent->getIsAllDay()) 145 + ->setIcon($parent->getIcon()) 146 + ->setSpacePHID($parent->getSpacePHID()) 147 + ->setViewPolicy($parent->getViewPolicy()) 148 + ->setEditPolicy($parent->getEditPolicy()) 149 + ->setName($parent->getName()) 150 + ->setDescription($parent->getDescription()); 151 + 152 + $frequency = $parent->getFrequencyUnit(); 153 + $modify_key = '+'.$this->getSequenceIndex().' '.$frequency; 154 + 155 + $date = $parent->getDateFrom(); 156 + $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); 157 + $date_time->modify($modify_key); 158 + $date = $date_time->format('U'); 159 + 160 + $duration = $parent->getDateTo() - $parent->getDateFrom(); 161 + 162 + $this 163 + ->setDateFrom($date) 164 + ->setDateTo($date + $duration); 165 + 166 + return $this; 167 + } 168 + 169 + public function newStub(PhabricatorUser $actor, $sequence) { 170 + $stub = $this->newChild($actor, $sequence); 171 + 172 + $stub->setIsStub(1); 173 + 174 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 175 + $stub->save(); 176 + unset($unguarded); 177 + 178 + $stub->applyViewerTimezone($actor); 179 + 180 + return $stub; 181 + } 182 + 183 + public function newGhost(PhabricatorUser $actor, $sequence) { 184 + $ghost = $this->newChild($actor, $sequence); 185 + 186 + $ghost 187 + ->setIsGhostEvent(true) 188 + ->makeEphemeral(); 189 + 190 + $ghost->applyViewerTimezone($actor); 191 + 192 + return $ghost; 193 + } 194 + 83 195 public function applyViewerTimezone(PhabricatorUser $viewer) { 84 196 if ($this->appliedViewer) { 85 197 throw new Exception(pht('Viewer timezone is already applied!')); ··· 211 323 'recurrenceEndDate' => 'epoch?', 212 324 'instanceOfEventPHID' => 'phid?', 213 325 'sequenceIndex' => 'uint32?', 326 + 'isStub' => 'bool', 214 327 ), 215 328 self::CONFIG_KEY_SCHEMA => array( 216 329 'userPHID_dateFrom' => array( ··· 285 398 return $this; 286 399 } 287 400 288 - public function generateNthGhost( 289 - $sequence_index, 290 - PhabricatorUser $actor) { 291 - 292 - $frequency = $this->getFrequencyUnit(); 293 - $modify_key = '+'.$sequence_index.' '.$frequency; 294 - 295 - $instance_of = ($this->getPHID()) ? 296 - $this->getPHID() : $this->instanceOfEventPHID; 297 - 298 - $date = $this->dateFrom; 299 - $date_time = PhabricatorTime::getDateTimeFromEpoch($date, $actor); 300 - $date_time->modify($modify_key); 301 - $date = $date_time->format('U'); 302 - 303 - $duration = $this->dateTo - $this->dateFrom; 304 - 305 - $edit_policy = PhabricatorPolicies::POLICY_NOONE; 306 - 307 - $ghost_event = id(clone $this) 308 - ->setIsGhostEvent(true) 309 - ->setDateFrom($date) 310 - ->setDateTo($date + $duration) 311 - ->setIsRecurring(true) 312 - ->setRecurrenceFrequency($this->recurrenceFrequency) 313 - ->setInstanceOfEventPHID($instance_of) 314 - ->setSequenceIndex($sequence_index) 315 - ->setEditPolicy($edit_policy); 316 - 317 - return $ghost_event; 318 - } 319 - 320 401 public function getFrequencyUnit() { 321 402 $frequency = idx($this->recurrenceFrequency, 'rule'); 322 403 ··· 335 416 } 336 417 337 418 public function getURI() { 338 - $uri = '/'.$this->getMonogram(); 339 - if ($this->isGhostEvent) { 340 - $uri = $uri.'/'.$this->sequenceIndex; 419 + if ($this->getIsGhostEvent()) { 420 + $base = $this->getParentEvent()->getURI(); 421 + $sequence = $this->getSequenceIndex(); 422 + return "{$base}/{$sequence}/"; 341 423 } 342 - return $uri; 424 + 425 + return '/'.$this->getMonogram(); 343 426 } 344 427 345 428 public function getParentEvent() { ··· 351 434 return $this; 352 435 } 353 436 354 - public function getIsCancelled() { 355 - $instance_of = $this->instanceOfEventPHID; 356 - if ($instance_of != null && $this->getIsParentCancelled()) { 357 - return true; 358 - } 359 - return $this->isCancelled; 437 + public function isParentEvent() { 438 + return ($this->isRecurring && !$this->instanceOfEventPHID); 360 439 } 361 440 362 - public function getIsRecurrenceParent() { 363 - if ($this->isRecurring && !$this->instanceOfEventPHID) { 364 - return true; 365 - } 366 - return false; 441 + public function isChildEvent() { 442 + return ($this->instanceOfEventPHID !== null); 367 443 } 368 444 369 - public function getIsRecurrenceException() { 370 - if ($this->instanceOfEventPHID && !$this->isGhostEvent) { 445 + public function isCancelledEvent() { 446 + if ($this->getIsCancelled()) { 371 447 return true; 372 448 } 373 - return false; 374 - } 375 449 376 - public function getIsParentCancelled() { 377 - if ($this->instanceOfEventPHID == null) { 378 - return false; 450 + if ($this->isChildEvent()) { 451 + if ($this->getParentEvent()->getIsCancelled()) { 452 + return true; 453 + } 379 454 } 380 455 381 - $recurring_event = $this->getParentEvent(); 382 - if ($recurring_event->getIsCancelled()) { 383 - return true; 384 - } 385 456 return false; 386 457 } 387 458 ··· 407 478 round($minutes, 0)); 408 479 } 409 480 } 481 + 410 482 411 483 /* -( Markup Interface )--------------------------------------------------- */ 412 484
+2
src/applications/conpherence/query/ConpherenceThreadQuery.php
··· 314 314 315 315 $events = array(); 316 316 if ($participant_phids) { 317 + // TODO: All of this Calendar code is probably extra-broken, but none 318 + // of it is currently reachable in the UI. 317 319 $events = id(new PhabricatorCalendarEventQuery()) 318 320 ->setViewer($this->getViewer()) 319 321 ->withInvitedPHIDs($participant_phids)
-1
src/applications/people/application/PhabricatorPeopleApplication.php
··· 69 69 '' => 'PhabricatorPeopleProfileViewController', 70 70 'panel/' 71 71 => $this->getPanelRouting('PhabricatorPeopleProfilePanelController'), 72 - 'calendar/' => 'PhabricatorPeopleCalendarController', 73 72 ), 74 73 ); 75 74 }
-97
src/applications/people/controller/PhabricatorPeopleCalendarController.php
··· 1 - <?php 2 - 3 - final class PhabricatorPeopleCalendarController 4 - extends PhabricatorPeopleProfileController { 5 - 6 - public function shouldAllowPublic() { 7 - return true; 8 - } 9 - 10 - public function handleRequest(AphrontRequest $request) { 11 - $viewer = $this->getViewer(); 12 - $username = $request->getURIData('username'); 13 - 14 - $user = id(new PhabricatorPeopleQuery()) 15 - ->setViewer($viewer) 16 - ->withUsernames(array($username)) 17 - ->needProfileImage(true) 18 - ->executeOne(); 19 - if (!$user) { 20 - return new Aphront404Response(); 21 - } 22 - 23 - $this->setUser($user); 24 - 25 - $picture = $user->getProfileImageURI(); 26 - 27 - $now = time(); 28 - $request = $this->getRequest(); 29 - $year_d = phabricator_format_local_time($now, $user, 'Y'); 30 - $year = $request->getInt('year', $year_d); 31 - $month_d = phabricator_format_local_time($now, $user, 'm'); 32 - $month = $request->getInt('month', $month_d); 33 - $day = phabricator_format_local_time($now, $user, 'j'); 34 - 35 - $start_epoch = strtotime("{$year}-{$month}-01"); 36 - $end_epoch = strtotime("{$year}-{$month}-01 next month"); 37 - 38 - $statuses = id(new PhabricatorCalendarEventQuery()) 39 - ->setViewer($user) 40 - ->withInvitedPHIDs(array($user->getPHID())) 41 - ->withDateRange( 42 - $start_epoch, 43 - $end_epoch) 44 - ->execute(); 45 - 46 - $start_range_value = AphrontFormDateControlValue::newFromEpoch( 47 - $user, 48 - $start_epoch); 49 - $end_range_value = AphrontFormDateControlValue::newFromEpoch( 50 - $user, 51 - $end_epoch); 52 - 53 - if ($month == $month_d && $year == $year_d) { 54 - $month_view = new PHUICalendarMonthView( 55 - $start_range_value, 56 - $end_range_value, 57 - $month, 58 - $year, 59 - $day); 60 - } else { 61 - $month_view = new PHUICalendarMonthView( 62 - $start_range_value, 63 - $end_range_value, 64 - $month, 65 - $year); 66 - } 67 - 68 - $month_view->setBrowseURI($request->getRequestURI()); 69 - $month_view->setUser($user); 70 - $month_view->setImage($picture); 71 - 72 - $phids = mpull($statuses, 'getUserPHID'); 73 - $handles = $this->loadViewerHandles($phids); 74 - 75 - foreach ($statuses as $status) { 76 - $event = new AphrontCalendarEventView(); 77 - $event->setEpochRange($status->getDateFrom(), $status->getDateTo()); 78 - $event->setUserPHID($status->getUserPHID()); 79 - $event->setName($status->getName()); 80 - $event->setDescription($status->getDescription()); 81 - $event->setEventID($status->getID()); 82 - $month_view->addEvent($event); 83 - } 84 - 85 - $nav = $this->getProfileMenu(); 86 - $nav->selectFilter('calendar'); 87 - 88 - $crumbs = $this->buildApplicationCrumbs(); 89 - $crumbs->addTextCrumb(pht('Calendar')); 90 - 91 - return $this->newPage() 92 - ->setTitle(pht('Calendar')) 93 - ->setNavigation($nav) 94 - ->setCrumbs($crumbs) 95 - ->appendChild($month_view); 96 - } 97 - }