@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.

Begin navigating the mess that is edits to recurring events

Summary:
Ref T11804. This puts us on a path toward some kind of reasonable behavior here.

Currently, cancelling recurring events makes approximately zero sense ever in any situation.

Instead, give users the choice to cancel just the instance, or all future events. This is similar to Calendar.app. (Google Calendar has a third option, "All Events", which I may implement).

When the user picks something, basically do that.

The particulars of "do that" are messy. We have to split the series into two different series, stop the first series early, then edit the second series. Then we need to update any concrete events that are now part of the second series.

This code will get less junk in the next couple of diffs (I hope?) since I need to make it apply to edits, too, but this was a little easier to get started with.

Test Plan:
Cancelled an instance of an event; cancelled "All future events".

Both of them more or less worked in a reasonble way.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T11804

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

+216 -88
+2
src/__phutil_library_map__.php
··· 2049 2049 'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php', 2050 2050 'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php', 2051 2051 'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php', 2052 + 'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php', 2052 2053 'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php', 2053 2054 'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php', 2054 2055 'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php', ··· 6889 6890 'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand', 6890 6891 'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 6891 6892 'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController', 6893 + 'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType', 6892 6894 'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType', 6893 6895 'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine', 6894 6896 'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',
+125 -61
src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php
··· 32 32 33 33 $is_parent = $event->isParentEvent(); 34 34 $is_child = $event->isChildEvent(); 35 - $is_cancelled = $event->getIsCancelled(); 36 35 37 - if ($is_child) { 38 - $is_parent_cancelled = $event->getParentEvent()->getIsCancelled(); 39 - } else { 40 - $is_parent_cancelled = false; 41 - } 36 + $is_cancelled = $event->getIsCancelled(); 37 + $is_recurring = $event->getIsRecurring(); 42 38 43 39 $validation_exception = null; 44 40 if ($request->isFormPost()) { 45 - $xactions = array(); 41 + 42 + $targets = array(); 43 + if ($is_recurring) { 44 + $mode = $request->getStr('mode'); 45 + $is_future = ($mode == 'future'); 46 + 47 + // We need to fork the event if we're cancelling just the parent, or 48 + // are cancelling a child and all future events. 49 + $must_fork = ($is_child && $is_future) || 50 + ($is_parent && !$is_future); 51 + 52 + if ($must_fork) { 53 + if ($is_child) { 54 + $xactions = array(); 55 + 56 + $xaction = id(new PhabricatorCalendarEventTransaction()) 57 + ->setTransactionType( 58 + PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE) 59 + ->setNewValue(true); 60 + 61 + $editor = id(new PhabricatorCalendarEventEditor()) 62 + ->setActor($viewer) 63 + ->setContentSourceFromRequest($request) 64 + ->setContinueOnNoEffect(true) 65 + ->setContinueOnMissingFields(true); 66 + 67 + $editor->applyTransactions($event, array($xaction)); 68 + 69 + $targets[] = $event; 70 + } else { 71 + // TODO: This is a huge mess; we need to load or generate the 72 + // first child, then fork that, then apply the event to the 73 + // parent. Just bail for now. 74 + throw new Exception( 75 + pht( 76 + 'Individual edits to parent events are not yet supported '. 77 + 'because they are real tricky to implement.')); 78 + } 79 + } else { 80 + $targets[] = $event; 81 + } 46 82 47 - $xaction = id(new PhabricatorCalendarEventTransaction()) 48 - ->setTransactionType( 49 - PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) 50 - ->setNewValue(!$is_cancelled); 83 + if ($is_future) { 84 + // NOTE: If you can't edit some of the future events, we just 85 + // don't try to update them. This seems like it's probably what 86 + // users are likely to expect. 87 + $future = id(new PhabricatorCalendarEventQuery()) 88 + ->setViewer($viewer) 89 + ->withParentEventPHIDs(array($event->getPHID())) 90 + ->withUTCInitialEpochBetween($event->getUTCInitialEpoch(), null) 91 + ->requireCapabilities( 92 + array( 93 + PhabricatorPolicyCapability::CAN_VIEW, 94 + PhabricatorPolicyCapability::CAN_EDIT, 95 + )) 96 + ->execute(); 97 + foreach ($future as $future_event) { 98 + $targets[] = $future_event; 99 + } 100 + } 101 + } else { 102 + $targets = array($event); 103 + } 51 104 52 - $editor = id(new PhabricatorCalendarEventEditor()) 53 - ->setActor($viewer) 54 - ->setContentSourceFromRequest($request) 55 - ->setContinueOnNoEffect(true) 56 - ->setContinueOnMissingFields(true); 105 + foreach ($targets as $target) { 106 + $xactions = array(); 107 + 108 + $xaction = id(new PhabricatorCalendarEventTransaction()) 109 + ->setTransactionType( 110 + PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) 111 + ->setNewValue(!$is_cancelled); 57 112 58 - try { 59 - $editor->applyTransactions($event, array($xaction)); 113 + $editor = id(new PhabricatorCalendarEventEditor()) 114 + ->setActor($viewer) 115 + ->setContentSourceFromRequest($request) 116 + ->setContinueOnNoEffect(true) 117 + ->setContinueOnMissingFields(true); 118 + 119 + try { 120 + $editor->applyTransactions($target, array($xaction)); 121 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 122 + $validation_exception = $ex; 123 + break; 124 + } 125 + 126 + } 127 + 128 + if (!$validation_exception) { 60 129 return id(new AphrontRedirectResponse())->setURI($cancel_uri); 61 - } catch (PhabricatorApplicationTransactionValidationException $ex) { 62 - $validation_exception = $ex; 63 130 } 64 131 } 65 132 66 133 if ($is_cancelled) { 67 - if ($is_parent_cancelled) { 68 - $title = pht('Cannot Reinstate Instance'); 69 - $paragraph = pht( 70 - 'You cannot reinstate an instance of a cancelled recurring event.'); 71 - $cancel = pht('Back'); 72 - $submit = null; 73 - } else if ($is_child) { 74 - $title = pht('Reinstate Instance'); 75 - $paragraph = pht( 76 - 'Reinstate this instance of this recurring event?'); 77 - $cancel = pht('Back'); 78 - $submit = pht('Reinstate Instance'); 79 - } else if ($is_parent) { 80 - $title = pht('Reinstate Recurring Event'); 81 - $paragraph = pht( 82 - 'Reinstate all instances of this recurring event which have not '. 83 - 'been individually cancelled?'); 84 - $cancel = pht('Back'); 85 - $submit = pht('Reinstate Recurring Event'); 134 + $title = pht('Reinstate Event'); 135 + if ($is_recurring) { 136 + $body = pht( 137 + 'This event is part of a series. Which events do you want to '. 138 + 'reinstate?'); 139 + $show_control = true; 86 140 } else { 87 - $title = pht('Reinstate Event'); 88 - $paragraph = pht('Reinstate this event?'); 89 - $cancel = pht('Back'); 90 - $submit = pht('Reinstate Event'); 141 + $body = pht('Reinstate this event?'); 142 + $show_control = false; 91 143 } 144 + $submit = pht('Reinstate Event'); 92 145 } else { 93 - if ($is_child) { 94 - $title = pht('Cancel Instance'); 95 - $paragraph = pht('Cancel this instance of this recurring event?'); 96 - $cancel = pht('Back'); 97 - $submit = pht('Cancel Instance'); 98 - } else if ($is_parent) { 99 - $title = pht('Cancel Recurrin Event'); 100 - $paragraph = pht('Cancel this entire series of recurring events?'); 101 - $cancel = pht('Back'); 102 - $submit = pht('Cancel Recurring Event'); 146 + $title = pht('Cancel Event'); 147 + if ($is_recurring) { 148 + $body = pht( 149 + 'This event is part of a series. Which events do you want to '. 150 + 'cancel?'); 151 + $show_control = true; 103 152 } else { 104 - $title = pht('Cancel Event'); 105 - $paragraph = pht( 106 - 'Cancel this event? You can always reinstate the event later.'); 107 - $cancel = pht('Back'); 108 - $submit = pht('Cancel Event'); 153 + $body = pht('Cancel this event?'); 154 + $show_control = false; 109 155 } 156 + $submit = pht('Cancel Event'); 110 157 } 111 158 112 - return $this->newDialog() 159 + $dialog = $this->newDialog() 113 160 ->setTitle($title) 114 161 ->setValidationException($validation_exception) 115 - ->appendParagraph($paragraph) 116 - ->addCancelButton($cancel_uri, $cancel) 162 + ->appendParagraph($body) 163 + ->addCancelButton($cancel_uri, pht('Back')) 117 164 ->addSubmitButton($submit); 165 + 166 + if ($show_control) { 167 + $form = id(new AphrontFormView()) 168 + ->setViewer($viewer) 169 + ->appendControl( 170 + id(new AphrontFormSelectControl()) 171 + ->setLabel(pht('Cancel Events')) 172 + ->setName('mode') 173 + ->setOptions( 174 + array( 175 + 'this' => pht('Only This Event'), 176 + 'future' => pht('All Future Events'), 177 + ))); 178 + $dialog->appendForm($form); 179 + } 180 + 181 + return $dialog; 118 182 } 119 183 }
+2 -14
src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
··· 206 206 $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); 207 207 $cancel_disabled = !$can_edit; 208 208 209 - if ($event->isChildEvent()) { 210 - $cancel_label = pht('Cancel This Instance'); 211 - $reinstate_label = pht('Reinstate This Instance'); 212 - 213 - if ($event->getParentEvent()->getIsCancelled()) { 214 - $cancel_disabled = true; 215 - } 216 - } else if ($event->isParentEvent()) { 217 - $cancel_label = pht('Cancel All'); 218 - $reinstate_label = pht('Reinstate All'); 219 - } else { 220 - $cancel_label = pht('Cancel Event'); 221 - $reinstate_label = pht('Reinstate Event'); 222 - } 209 + $cancel_label = pht('Cancel Event'); 210 + $reinstate_label = pht('Reinstate Event'); 223 211 224 212 if ($event->isCancelledEvent()) { 225 213 $curtain->addAction(
+22
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 17 17 private $importSourcePHIDs; 18 18 private $importAuthorPHIDs; 19 19 private $importUIDs; 20 + private $utcInitialEpochMin; 21 + private $utcInitialEpochMax; 20 22 21 23 private $generateGhosts = false; 22 24 ··· 42 44 public function withDateRange($begin, $end) { 43 45 $this->rangeBegin = $begin; 44 46 $this->rangeEnd = $end; 47 + return $this; 48 + } 49 + 50 + public function withUTCInitialEpochBetween($min, $max) { 51 + $this->utcInitialEpochMin = $min; 52 + $this->utcInitialEpochMax = $max; 45 53 return $this; 46 54 } 47 55 ··· 369 377 $conn, 370 378 'event.utcInitialEpoch <= %d', 371 379 $this->rangeEnd + phutil_units('16 hours in seconds')); 380 + } 381 + 382 + if ($this->utcInitialEpochMin !== null) { 383 + $where[] = qsprintf( 384 + $conn, 385 + 'event.utcInitialEpoch >= %d', 386 + $this->utcInitialEpochMin); 387 + } 388 + 389 + if ($this->utcInitialEpochMax !== null) { 390 + $where[] = qsprintf( 391 + $conn, 392 + 'event.utcInitialEpoch <= %d', 393 + $this->utcInitialEpochMax); 372 394 } 373 395 374 396 if ($this->inviteePHIDs !== null) {
+6 -13
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 165 165 'editPolicy' => true, 166 166 'name' => true, 167 167 'description' => true, 168 + 'isCancelled' => true, 168 169 ); 169 170 170 171 // Read these fields from the parent event instead of this event. For ··· 204 205 ->setViewPolicy($parent->getViewPolicy()) 205 206 ->setEditPolicy($parent->getEditPolicy()) 206 207 ->setName($parent->getName()) 207 - ->setDescription($parent->getDescription()); 208 + ->setDescription($parent->getDescription()) 209 + ->setIsCancelled($parent->getIsCancelled()); 208 210 209 211 if ($start) { 210 212 $start_datetime = $start; ··· 569 571 return $this->assertAttached($this->parentEvent); 570 572 } 571 573 572 - public function attachParentEvent($event) { 574 + public function attachParentEvent(PhabricatorCalendarEvent $event = null) { 573 575 $this->parentEvent = $event; 574 576 return $this; 575 577 } ··· 583 585 } 584 586 585 587 public function isCancelledEvent() { 586 - if ($this->getIsCancelled()) { 587 - return true; 588 - } 589 - 590 - if ($this->isChildEvent()) { 591 - if ($this->getParentEvent()->getIsCancelled()) { 592 - return true; 593 - } 594 - } 595 - 596 - return false; 588 + // TODO: Remove this wrapper. 589 + return $this->getIsCancelled(); 597 590 } 598 591 599 592 public function renderEventDate(
+59
src/applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorCalendarEventForkTransaction 4 + extends PhabricatorCalendarEventTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'calendar.fork'; 7 + 8 + public function generateOldValue($object) { 9 + return false; 10 + } 11 + 12 + public function shouldHide() { 13 + // This transaction is purely an internal implementation detail which 14 + // supports editing groups of events like "All Future Events". 15 + return true; 16 + } 17 + 18 + public function applyInternalEffects($object, $value) { 19 + $parent = $object->getParentEvent(); 20 + 21 + $object->setInstanceOfEventPHID(null); 22 + $object->attachParentEvent(null); 23 + 24 + $rrule = $parent->newRecurrenceRule(); 25 + $object->setRecurrenceRule($rrule); 26 + 27 + $until = $parent->newUntilDateTime(); 28 + if ($until) { 29 + $object->setUntilDateTime($until); 30 + } 31 + 32 + $old_sequence_index = $object->getSequenceIndex(); 33 + $object->setSequenceIndex(0); 34 + 35 + // Stop the parent event from recurring after the start date of this event. 36 + $parent->setUntilDateTime($object->newStartDateTime()); 37 + $parent->save(); 38 + 39 + // NOTE: If we implement "COUNT" on editable events, we need to adjust 40 + // the "COUNT" here and divide it up between the parent and the fork. 41 + 42 + // Make all following children of the old parent children of this node 43 + // instead. 44 + $conn = $object->establishConnection('w'); 45 + queryfx( 46 + $conn, 47 + 'UPDATE %T SET 48 + instanceOfEventPHID = %s, 49 + sequenceIndex = (sequenceIndex - %d) 50 + WHERE instanceOfEventPHID = %s 51 + AND utcInstanceEpoch > %d', 52 + $object->getTableName(), 53 + $object->getPHID(), 54 + $old_sequence_index, 55 + $parent->getPHID(), 56 + $object->getUTCInstanceEpoch()); 57 + } 58 + 59 + }