1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

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
This commit is contained in:
epriestley 2016-10-31 12:47:39 -07:00
parent 8e5437226f
commit 91089acbe5
6 changed files with 216 additions and 88 deletions

View file

@ -2049,6 +2049,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php', 'PhabricatorCalendarEventEmailCommand' => 'applications/calendar/command/PhabricatorCalendarEventEmailCommand.php',
'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php', 'PhabricatorCalendarEventEndDateTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventEndDateTransaction.php',
'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php', 'PhabricatorCalendarEventExportController' => 'applications/calendar/controller/PhabricatorCalendarEventExportController.php',
'PhabricatorCalendarEventForkTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventForkTransaction.php',
'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php', 'PhabricatorCalendarEventFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarEventFrequencyTransaction.php',
'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php', 'PhabricatorCalendarEventFulltextEngine' => 'applications/calendar/search/PhabricatorCalendarEventFulltextEngine.php',
'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php', 'PhabricatorCalendarEventHeraldAdapter' => 'applications/calendar/herald/PhabricatorCalendarEventHeraldAdapter.php',
@ -6889,6 +6890,7 @@ phutil_register_library_map(array(
'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorCalendarEventEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction', 'PhabricatorCalendarEventEndDateTransaction' => 'PhabricatorCalendarEventDateTransaction',
'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController', 'PhabricatorCalendarEventExportController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventForkTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType', 'PhabricatorCalendarEventFrequencyTransaction' => 'PhabricatorCalendarEventTransactionType',
'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorCalendarEventFulltextEngine' => 'PhabricatorFulltextEngine',
'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter', 'PhabricatorCalendarEventHeraldAdapter' => 'HeraldAdapter',

View file

@ -32,88 +32,152 @@ final class PhabricatorCalendarEventCancelController
$is_parent = $event->isParentEvent(); $is_parent = $event->isParentEvent();
$is_child = $event->isChildEvent(); $is_child = $event->isChildEvent();
$is_cancelled = $event->getIsCancelled();
if ($is_child) { $is_cancelled = $event->getIsCancelled();
$is_parent_cancelled = $event->getParentEvent()->getIsCancelled(); $is_recurring = $event->getIsRecurring();
} else {
$is_parent_cancelled = false;
}
$validation_exception = null; $validation_exception = null;
if ($request->isFormPost()) { if ($request->isFormPost()) {
$xactions = array();
$xaction = id(new PhabricatorCalendarEventTransaction()) $targets = array();
->setTransactionType( if ($is_recurring) {
PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE) $mode = $request->getStr('mode');
->setNewValue(!$is_cancelled); $is_future = ($mode == 'future');
$editor = id(new PhabricatorCalendarEventEditor()) // We need to fork the event if we're cancelling just the parent, or
->setActor($viewer) // are cancelling a child and all future events.
->setContentSourceFromRequest($request) $must_fork = ($is_child && $is_future) ||
->setContinueOnNoEffect(true) ($is_parent && !$is_future);
->setContinueOnMissingFields(true);
try { if ($must_fork) {
$editor->applyTransactions($event, array($xaction)); if ($is_child) {
$xactions = array();
$xaction = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventForkTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$editor = id(new PhabricatorCalendarEventEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$editor->applyTransactions($event, array($xaction));
$targets[] = $event;
} else {
// TODO: This is a huge mess; we need to load or generate the
// first child, then fork that, then apply the event to the
// parent. Just bail for now.
throw new Exception(
pht(
'Individual edits to parent events are not yet supported '.
'because they are real tricky to implement.'));
}
} else {
$targets[] = $event;
}
if ($is_future) {
// NOTE: If you can't edit some of the future events, we just
// don't try to update them. This seems like it's probably what
// users are likely to expect.
$future = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->withParentEventPHIDs(array($event->getPHID()))
->withUTCInitialEpochBetween($event->getUTCInitialEpoch(), null)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->execute();
foreach ($future as $future_event) {
$targets[] = $future_event;
}
}
} else {
$targets = array($event);
}
foreach ($targets as $target) {
$xactions = array();
$xaction = id(new PhabricatorCalendarEventTransaction())
->setTransactionType(
PhabricatorCalendarEventCancelTransaction::TRANSACTIONTYPE)
->setNewValue(!$is_cancelled);
$editor = id(new PhabricatorCalendarEventEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
try {
$editor->applyTransactions($target, array($xaction));
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
break;
}
}
if (!$validation_exception) {
return id(new AphrontRedirectResponse())->setURI($cancel_uri); return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex;
} }
} }
if ($is_cancelled) { if ($is_cancelled) {
if ($is_parent_cancelled) { $title = pht('Reinstate Event');
$title = pht('Cannot Reinstate Instance'); if ($is_recurring) {
$paragraph = pht( $body = pht(
'You cannot reinstate an instance of a cancelled recurring event.'); 'This event is part of a series. Which events do you want to '.
$cancel = pht('Back'); 'reinstate?');
$submit = null; $show_control = true;
} 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 Recurring Event');
$paragraph = pht(
'Reinstate all instances of this recurring event which have not '.
'been individually cancelled?');
$cancel = pht('Back');
$submit = pht('Reinstate Recurring Event');
} else { } else {
$title = pht('Reinstate Event'); $body = pht('Reinstate this event?');
$paragraph = pht('Reinstate this event?'); $show_control = false;
$cancel = pht('Back');
$submit = pht('Reinstate Event');
} }
$submit = pht('Reinstate Event');
} else { } else {
if ($is_child) { $title = pht('Cancel Event');
$title = pht('Cancel Instance'); if ($is_recurring) {
$paragraph = pht('Cancel this instance of this recurring event?'); $body = pht(
$cancel = pht('Back'); 'This event is part of a series. Which events do you want to '.
$submit = pht('Cancel Instance'); 'cancel?');
} else if ($is_parent) { $show_control = true;
$title = pht('Cancel Recurrin Event');
$paragraph = pht('Cancel this entire series of recurring events?');
$cancel = pht('Back');
$submit = pht('Cancel Recurring Event');
} else { } else {
$title = pht('Cancel Event'); $body = pht('Cancel this event?');
$paragraph = pht( $show_control = false;
'Cancel this event? You can always reinstate the event later.');
$cancel = pht('Back');
$submit = pht('Cancel Event');
} }
$submit = pht('Cancel Event');
} }
return $this->newDialog() $dialog = $this->newDialog()
->setTitle($title) ->setTitle($title)
->setValidationException($validation_exception) ->setValidationException($validation_exception)
->appendParagraph($paragraph) ->appendParagraph($body)
->addCancelButton($cancel_uri, $cancel) ->addCancelButton($cancel_uri, pht('Back'))
->addSubmitButton($submit); ->addSubmitButton($submit);
if ($show_control) {
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setLabel(pht('Cancel Events'))
->setName('mode')
->setOptions(
array(
'this' => pht('Only This Event'),
'future' => pht('All Future Events'),
)));
$dialog->appendForm($form);
}
return $dialog;
} }
} }

View file

@ -206,20 +206,8 @@ final class PhabricatorCalendarEventViewController
$cancel_uri = $this->getApplicationURI("event/cancel/{$id}/"); $cancel_uri = $this->getApplicationURI("event/cancel/{$id}/");
$cancel_disabled = !$can_edit; $cancel_disabled = !$can_edit;
if ($event->isChildEvent()) { $cancel_label = pht('Cancel Event');
$cancel_label = pht('Cancel This Instance'); $reinstate_label = pht('Reinstate Event');
$reinstate_label = pht('Reinstate This Instance');
if ($event->getParentEvent()->getIsCancelled()) {
$cancel_disabled = true;
}
} else if ($event->isParentEvent()) {
$cancel_label = pht('Cancel All');
$reinstate_label = pht('Reinstate All');
} else {
$cancel_label = pht('Cancel Event');
$reinstate_label = pht('Reinstate Event');
}
if ($event->isCancelledEvent()) { if ($event->isCancelledEvent()) {
$curtain->addAction( $curtain->addAction(

View file

@ -17,6 +17,8 @@ final class PhabricatorCalendarEventQuery
private $importSourcePHIDs; private $importSourcePHIDs;
private $importAuthorPHIDs; private $importAuthorPHIDs;
private $importUIDs; private $importUIDs;
private $utcInitialEpochMin;
private $utcInitialEpochMax;
private $generateGhosts = false; private $generateGhosts = false;
@ -45,6 +47,12 @@ final class PhabricatorCalendarEventQuery
return $this; return $this;
} }
public function withUTCInitialEpochBetween($min, $max) {
$this->utcInitialEpochMin = $min;
$this->utcInitialEpochMax = $max;
return $this;
}
public function withInvitedPHIDs(array $phids) { public function withInvitedPHIDs(array $phids) {
$this->inviteePHIDs = $phids; $this->inviteePHIDs = $phids;
return $this; return $this;
@ -371,6 +379,20 @@ final class PhabricatorCalendarEventQuery
$this->rangeEnd + phutil_units('16 hours in seconds')); $this->rangeEnd + phutil_units('16 hours in seconds'));
} }
if ($this->utcInitialEpochMin !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch >= %d',
$this->utcInitialEpochMin);
}
if ($this->utcInitialEpochMax !== null) {
$where[] = qsprintf(
$conn,
'event.utcInitialEpoch <= %d',
$this->utcInitialEpochMax);
}
if ($this->inviteePHIDs !== null) { if ($this->inviteePHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,

View file

@ -165,6 +165,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'editPolicy' => true, 'editPolicy' => true,
'name' => true, 'name' => true,
'description' => true, 'description' => true,
'isCancelled' => true,
); );
// Read these fields from the parent event instead of this event. For // Read these fields from the parent event instead of this event. For
@ -204,7 +205,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setViewPolicy($parent->getViewPolicy()) ->setViewPolicy($parent->getViewPolicy())
->setEditPolicy($parent->getEditPolicy()) ->setEditPolicy($parent->getEditPolicy())
->setName($parent->getName()) ->setName($parent->getName())
->setDescription($parent->getDescription()); ->setDescription($parent->getDescription())
->setIsCancelled($parent->getIsCancelled());
if ($start) { if ($start) {
$start_datetime = $start; $start_datetime = $start;
@ -569,7 +571,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
return $this->assertAttached($this->parentEvent); return $this->assertAttached($this->parentEvent);
} }
public function attachParentEvent($event) { public function attachParentEvent(PhabricatorCalendarEvent $event = null) {
$this->parentEvent = $event; $this->parentEvent = $event;
return $this; return $this;
} }
@ -583,17 +585,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
} }
public function isCancelledEvent() { public function isCancelledEvent() {
if ($this->getIsCancelled()) { // TODO: Remove this wrapper.
return true; return $this->getIsCancelled();
}
if ($this->isChildEvent()) {
if ($this->getParentEvent()->getIsCancelled()) {
return true;
}
}
return false;
} }
public function renderEventDate( public function renderEventDate(

View file

@ -0,0 +1,59 @@
<?php
final class PhabricatorCalendarEventForkTransaction
extends PhabricatorCalendarEventTransactionType {
const TRANSACTIONTYPE = 'calendar.fork';
public function generateOldValue($object) {
return false;
}
public function shouldHide() {
// This transaction is purely an internal implementation detail which
// supports editing groups of events like "All Future Events".
return true;
}
public function applyInternalEffects($object, $value) {
$parent = $object->getParentEvent();
$object->setInstanceOfEventPHID(null);
$object->attachParentEvent(null);
$rrule = $parent->newRecurrenceRule();
$object->setRecurrenceRule($rrule);
$until = $parent->newUntilDateTime();
if ($until) {
$object->setUntilDateTime($until);
}
$old_sequence_index = $object->getSequenceIndex();
$object->setSequenceIndex(0);
// Stop the parent event from recurring after the start date of this event.
$parent->setUntilDateTime($object->newStartDateTime());
$parent->save();
// NOTE: If we implement "COUNT" on editable events, we need to adjust
// the "COUNT" here and divide it up between the parent and the fork.
// Make all following children of the old parent children of this node
// instead.
$conn = $object->establishConnection('w');
queryfx(
$conn,
'UPDATE %T SET
instanceOfEventPHID = %s,
sequenceIndex = (sequenceIndex - %d)
WHERE instanceOfEventPHID = %s
AND utcInstanceEpoch > %d',
$object->getTableName(),
$object->getPHID(),
$old_sequence_index,
$parent->getPHID(),
$object->getUTCInstanceEpoch());
}
}