1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-09 16:32:39 +01:00

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
This commit is contained in:
epriestley 2016-07-07 07:19:58 -07:00
parent 91a8a6d618
commit 3ab6a7e19f
17 changed files with 404 additions and 505 deletions

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_calendar.calendar_event
ADD isStub BOOL NOT NULL;

View file

@ -2022,7 +2022,6 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'PhabricatorFulltextInterface',
),
'PhabricatorCalendarEventCancelController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventCommentController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventDragController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditController' => 'PhabricatorCalendarController',
'PhabricatorCalendarEventEditProController' => 'ManiphestController',
@ -7741,7 +7738,6 @@ phutil_register_library_map(array(
'PhabricatorPeopleAnyOwnerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorPeopleApplication' => 'PhabricatorApplication',
'PhabricatorPeopleApproveController' => 'PhabricatorPeopleController',
'PhabricatorPeopleCalendarController' => 'PhabricatorPeopleProfileController',
'PhabricatorPeopleController' => 'PhabricatorController',
'PhabricatorPeopleCreateController' => 'PhabricatorPeopleController',
'PhabricatorPeopleDatasource' => 'PhabricatorTypeaheadDatasource',

View file

@ -40,7 +40,7 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
public function getRoutes() {
return array(
'/E(?P<id>[1-9]\d*)(?:/(?P<sequence>\d+))?'
'/E(?P<id>[1-9]\d*)(?:/(?P<sequence>\d+)/)?'
=> 'PhabricatorCalendarEventViewController',
'/calendar/' => array(
'(?:query/(?P<queryKey>[^/]+)/(?:(?P<year>\d+)/'.
@ -51,15 +51,15 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication {
=> 'PhabricatorCalendarEventEditProController',
'create/'
=> 'PhabricatorCalendarEventEditController',
'edit/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?'
'edit/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarEventEditController',
'drag/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarEventDragController',
'cancel/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?'
'cancel/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarEventCancelController',
'(?P<action>join|decline|accept)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarEventJoinController',
'comment/(?P<id>[1-9]\d*)/(?:(?P<sequence>\d+)/)?'
'comment/(?P<id>[1-9]\d*)/'
=> 'PhabricatorCalendarEventCommentController',
),
),

View file

@ -30,49 +30,4 @@ abstract class PhabricatorCalendarController extends PhabricatorController {
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;
}
}

View file

@ -6,7 +6,6 @@ final class PhabricatorCalendarEventCancelController
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 @@ final class PhabricatorCalendarEventCancelController
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();
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 @@ final class PhabricatorCalendarEventCancelController
}
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_parent) {
$title = pht('Reinstate Recurrence');
} else if ($is_child) {
$title = pht('Reinstate Instance');
$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 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 {
$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');
}
}

View file

@ -1,81 +0,0 @@
<?php
final class PhabricatorCalendarEventCommentController
extends PhabricatorCalendarController {
public function handleRequest(AphrontRequest $request) {
if (!$request->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);
}
}
}

View file

@ -85,33 +85,10 @@ final class PhabricatorCalendarEventEditController
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 @@ final class PhabricatorCalendarEventEditController
}
}
$cancel_uri = '/'.$event->getMonogram();
$cancel_uri = $event->getURI();
}
if ($this->isCreate()) {
@ -153,7 +130,7 @@ final class PhabricatorCalendarEventEditController
$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();

View file

@ -20,12 +20,11 @@ final class PhabricatorCalendarEventJoinController
->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());

View file

@ -9,89 +9,48 @@ final class PhabricatorCalendarEventViewController
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();
// 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);
}
$title = $event->getMonogram().' ('.$sequence.')';
$page_title = $title.' '.$event->getName();
$monogram = $event->getMonogram();
$page_title = $monogram.' '.$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->addTextCrumb($monogram);
$crumbs->setBorder(true);
}
if (!$event->getIsGhostEvent()) {
$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 @@ final class PhabricatorCalendarEventViewController
->setTitle($page_title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($event->getPHID()))
->appendChild(
array(
$view,
));
->appendChild($view);
}
private function buildHeaderView(
@ -152,7 +108,7 @@ final class PhabricatorCalendarEventViewController
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 @@ final class PhabricatorCalendarEventViewController
$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_label = pht('Edit This Instance');
$edit_uri = "event/edit/{$id}/";
if ($event->isChildEvent()) {
$edit_label = pht('Edit This Instance');
} else {
$edit_label = pht('Edit');
$edit_uri = "event/edit/{$id}/";
}
$curtain = $this->newCurtainView($event);
@ -204,28 +152,21 @@ final class PhabricatorCalendarEventViewController
}
$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 @@ final class PhabricatorCalendarEventViewController
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;
}
}

View file

@ -55,6 +55,10 @@ final class PhabricatorCalendarEditEngine
return $object->getURI();
}
protected function getEditorURI() {
return $this->getApplication()->getApplicationURI('event/editpro/');
}
protected function buildCustomEditFields($object) {
$fields = array(
id(new PhabricatorTextEditField())

View file

@ -11,6 +11,47 @@ final class PhabricatorCalendarEventEditor
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 @@ final class PhabricatorCalendarEventEditor
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) {

View file

@ -12,6 +12,7 @@ final class PhabricatorCalendarEventQuery
private $isCancelled;
private $eventsWithNoParent;
private $instanceSequencePairs;
private $isStub;
private $generateGhosts = false;
@ -55,6 +56,11 @@ final class PhabricatorCalendarEventQuery
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 @@ final class PhabricatorCalendarEventQuery
$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 @@ final class PhabricatorCalendarEventQuery
}
}
$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) {
if ($event->getIsGhostEvent()) {
$index = $event->getSequenceIndex();
$instance_sequence_pairs[] = array($event->getPHID(), $index);
$map[$event->getPHID()][$index] = $key;
}
}
if (count($instance_sequence_pairs) > 0) {
$sub_query = id(new PhabricatorCalendarEventQuery())
->setViewer($viewer)
->setParentQuery($this)
->withInstanceSequencePairs($instance_sequence_pairs)
->execute();
foreach ($sub_query as $edited_ghost) {
$indexes = idx($map, $edited_ghost->getInstanceOfEventPHID());
$key = idx($indexes, $edited_ghost->getSequenceIndex());
$events[$key] = $edited_ghost;
}
$id_map = array();
foreach ($events as $key => $event) {
// First, generate a map of all concrete <parentPHID, sequence> events we
// already loaded. We don't need to load these again.
$have_pairs = array();
foreach ($events as $event) {
if ($event->getIsGhostEvent()) {
continue;
}
if (isset($id_map[$event->getID()])) {
unset($events[$key]);
} else {
$id_map[$event->getID()] = true;
$parent_phid = $event->getInstanceOfEventPHID();
$sequence = $event->getSequenceIndex();
$have_pairs[$parent_phid][$sequence] = true;
}
// Now, generate a map of all <parentPHID, sequence> 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 ($parent_pairs) {
$instances = id(new self())
->setViewer($viewer)
->setParentQuery($this)
->withInstanceSequencePairs($parent_pairs)
->execute();
foreach ($instances as $instance) {
$parent_phid = $instance->getInstanceOfEventPHID();
$sequence = $instance->getSequenceIndex();
$indexes = idx($map, $parent_phid);
$key = idx($indexes, $sequence);
// Replace the ghost with the corresponding concrete event.
$events[$key] = $instance;
}
}
@ -329,6 +361,13 @@ final class PhabricatorCalendarEventQuery
implode(' OR ', $sql));
}
if ($this->isStub !== null) {
$where[] = qsprintf(
$conn,
'event.isStub = %d',
(int)$this->isStub);
}
return $where;
}

View file

@ -113,7 +113,15 @@ final class PhabricatorCalendarEventSearchEngine
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(

View file

@ -22,6 +22,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
protected $isAllDay;
protected $icon;
protected $mailKey;
protected $isStub;
protected $isRecurring = 0;
protected $recurrenceFrequency = array();
@ -71,6 +72,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->setUserPHID($actor->getPHID())
->setIsCancelled(0)
->setIsAllDay(0)
->setIsStub(0)
->setIsRecurring($is_recurring)
->setIcon(self::DEFAULT_ICON)
->setViewPolicy($view_policy)
@ -80,6 +82,116 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
->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 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
'recurrenceEndDate' => 'epoch?',
'instanceOfEventPHID' => 'phid?',
'sequenceIndex' => 'uint32?',
'isStub' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'userPHID_dateFrom' => array(
@ -285,38 +398,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
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 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
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 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
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;
}
$recurring_event = $this->getParentEvent();
if ($recurring_event->getIsCancelled()) {
if ($this->isChildEvent()) {
if ($this->getParentEvent()->getIsCancelled()) {
return true;
}
}
return false;
}
@ -408,6 +479,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO
}
}
/* -( Markup Interface )--------------------------------------------------- */

View file

@ -314,6 +314,8 @@ final class ConpherenceThreadQuery
$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)

View file

@ -69,7 +69,6 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication {
'' => 'PhabricatorPeopleProfileViewController',
'panel/'
=> $this->getPanelRouting('PhabricatorPeopleProfilePanelController'),
'calendar/' => 'PhabricatorPeopleCalendarController',
),
);
}

View file

@ -1,97 +0,0 @@
<?php
final class PhabricatorPeopleCalendarController
extends PhabricatorPeopleProfileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->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);
}
}