3 final class PhabricatorCalendarEventQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery
{
10 private $inviteePHIDs;
13 private $eventsWithNoParent;
14 private $instanceSequencePairs;
16 private $parentEventPHIDs;
17 private $importSourcePHIDs;
18 private $importAuthorPHIDs;
20 private $utcInitialEpochMin;
21 private $utcInitialEpochMax;
25 private $generateGhosts = false;
27 public function newResultObject() {
28 return new PhabricatorCalendarEvent();
31 public function setGenerateGhosts($generate_ghosts) {
32 $this->generateGhosts
= $generate_ghosts;
36 public function withIDs(array $ids) {
41 public function withPHIDs(array $phids) {
42 $this->phids
= $phids;
46 public function withDateRange($begin, $end) {
47 $this->rangeBegin
= $begin;
48 $this->rangeEnd
= $end;
52 public function withUTCInitialEpochBetween($min, $max) {
53 $this->utcInitialEpochMin
= $min;
54 $this->utcInitialEpochMax
= $max;
58 public function withInvitedPHIDs(array $phids) {
59 $this->inviteePHIDs
= $phids;
63 public function withHostPHIDs(array $phids) {
64 $this->hostPHIDs
= $phids;
68 public function withIsCancelled($is_cancelled) {
69 $this->isCancelled
= $is_cancelled;
73 public function withIsStub($is_stub) {
74 $this->isStub
= $is_stub;
78 public function withEventsWithNoParent($events_with_no_parent) {
79 $this->eventsWithNoParent
= $events_with_no_parent;
83 public function withInstanceSequencePairs(array $pairs) {
84 $this->instanceSequencePairs
= $pairs;
88 public function withParentEventPHIDs(array $parent_phids) {
89 $this->parentEventPHIDs
= $parent_phids;
93 public function withImportSourcePHIDs(array $import_phids) {
94 $this->importSourcePHIDs
= $import_phids;
98 public function withImportAuthorPHIDs(array $author_phids) {
99 $this->importAuthorPHIDs
= $author_phids;
103 public function withImportUIDs(array $uids) {
104 $this->importUIDs
= $uids;
108 public function withIsImported($is_imported) {
109 $this->isImported
= $is_imported;
113 public function needRSVPs(array $phids) {
114 $this->needRSVPs
= $phids;
118 protected function getDefaultOrderVector() {
119 return array('start', 'id');
122 public function getBuiltinOrders() {
125 'vector' => array('start', 'id'),
126 'name' => pht('Event Start'),
128 ) + parent
::getBuiltinOrders();
131 public function getOrderableColumns() {
134 'table' => $this->getPrimaryTableAlias(),
135 'column' => 'utcInitialEpoch',
140 ) + parent
::getOrderableColumns();
143 protected function newPagingMapFromPartialObject($object) {
145 'id' => (int)$object->getID(),
146 'start' => (int)$object->getStartDateTimeEpoch(),
150 protected function shouldLimitResults() {
151 // When generating ghosts, we can't rely on database ordering because
152 // MySQL can't predict the ghost start times. We'll just load all matching
153 // events, then generate results from there.
154 if ($this->generateGhosts
) {
161 protected function loadPage() {
162 $events = $this->loadStandardPage($this->newResultObject());
164 $viewer = $this->getViewer();
165 foreach ($events as $event) {
166 $event->applyViewerTimezone($viewer);
169 if (!$this->generateGhosts
) {
173 $raw_limit = $this->getRawResultLimit();
174 if (!$raw_limit && !$this->rangeEnd
) {
177 'Event queries which generate ghost events must include either a '.
178 'result limit or an end date, because they may otherwise generate '.
179 'an infinite number of results. This query has neither.'));
182 foreach ($events as $key => $event) {
184 $sequence_end = null;
187 $instance_of = $event->getInstanceOfEventPHID();
189 if ($instance_of == null && $this->isCancelled
!== null) {
190 if ($event->getIsCancelled() != $this->isCancelled
) {
191 unset($events[$key]);
197 // Pull out all of the parents first. We may discard them as we begin
198 // generating ghost events, but we still want to process all of them.
200 foreach ($events as $key => $event) {
201 if ($event->isParentEvent()) {
202 $parents[$key] = $event;
206 // Now that we've picked out all the parent events, we can immediately
207 // discard anything outside of the time window.
208 $events = $this->getEventsInRange($events);
210 $generate_from = $this->rangeBegin
;
211 $generate_until = $this->rangeEnd
;
212 foreach ($parents as $key => $event) {
213 $duration = $event->getDuration();
215 $start_date = $this->getRecurrenceWindowStart(
217 $generate_from - $duration);
219 $end_date = $this->getRecurrenceWindowEnd(
223 $limit = $this->getRecurrenceLimit($event, $raw_limit);
225 $set = $event->newRecurrenceSet();
227 $recurrences = $set->getEventsBetween(
232 // We're generating events from the beginning and then filtering them
233 // here (instead of only generating events starting at the start date)
234 // because we need to know the proper sequence indexes to generate ghost
235 // events. This may change after RDATE support.
237 $start_epoch = $start_date->getEpoch();
242 foreach ($recurrences as $sequence_index => $sequence_datetime) {
243 if (!$sequence_index) {
244 // This is the parent event, which we already have.
249 if ($sequence_datetime->getEpoch() < $start_epoch) {
254 $events[] = $event->newGhost(
260 // NOTE: We're slicing results every time because this makes it cheaper
261 // to generate future ghosts. If we already have 100 events that occur
262 // before July 1, we know we never need to generate ghosts after that
263 // because they couldn't possibly ever appear in the result set.
266 if (count($events) > $raw_limit) {
267 $events = msort($events, 'getStartDateTimeEpoch');
268 $events = array_slice($events, 0, $raw_limit, true);
269 $generate_until = last($events)->getEndDateTimeEpoch();
274 // Now that we're done generating ghost events, we're going to remove any
275 // ghosts that we have concrete events for (or which we can load the
276 // concrete events for). These concrete events are generated when users
277 // edit a ghost, and replace the ghost events.
279 // First, generate a map of all concrete <parentPHID, sequence> events we
280 // already loaded. We don't need to load these again.
281 $have_pairs = array();
282 foreach ($events as $event) {
283 if ($event->getIsGhostEvent()) {
287 $parent_phid = $event->getInstanceOfEventPHID();
288 $sequence = $event->getSequenceIndex();
290 $have_pairs[$parent_phid][$sequence] = true;
293 // Now, generate a map of all <parentPHID, sequence> events we generated
294 // ghosts for. We need to try to load these if we don't already have them.
296 $parent_pairs = array();
297 foreach ($events as $key => $event) {
298 if (!$event->getIsGhostEvent()) {
302 $parent_phid = $event->getInstanceOfEventPHID();
303 $sequence = $event->getSequenceIndex();
305 // We already loaded the concrete version of this event, so we can just
306 // throw out the ghost and move on.
307 if (isset($have_pairs[$parent_phid][$sequence])) {
308 unset($events[$key]);
312 // We didn't load the concrete version of this event, so we need to
313 // try to load it if it exists.
314 $parent_pairs[] = array($parent_phid, $sequence);
315 $map[$parent_phid][$sequence] = $key;
319 $instances = id(new self())
321 ->setParentQuery($this)
322 ->withInstanceSequencePairs($parent_pairs)
325 foreach ($instances as $instance) {
326 $parent_phid = $instance->getInstanceOfEventPHID();
327 $sequence = $instance->getSequenceIndex();
329 $indexes = idx($map, $parent_phid);
330 $key = idx($indexes, $sequence);
332 // Replace the ghost with the corresponding concrete event.
333 $events[$key] = $instance;
337 $events = msort($events, 'getStartDateTimeEpoch');
342 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn_r) {
343 $parts = parent
::buildJoinClauseParts($conn_r);
345 if ($this->inviteePHIDs
!== null) {
348 'JOIN %T invitee ON invitee.eventPHID = event.phid
349 AND invitee.status != %s',
350 id(new PhabricatorCalendarEventInvitee())->getTableName(),
351 PhabricatorCalendarEventInvitee
::STATUS_UNINVITED
);
357 protected function buildWhereClauseParts(AphrontDatabaseConnection
$conn) {
358 $where = parent
::buildWhereClauseParts($conn);
360 if ($this->ids
!== null) {
367 if ($this->phids
!== null) {
370 'event.phid IN (%Ls)',
374 // NOTE: The date ranges we query for are larger than the requested ranges
375 // because we need to catch all-day events. We'll refine this range later
376 // after adjusting the visible range of events we load.
378 if ($this->rangeBegin
) {
381 '(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
382 $this->rangeBegin
- phutil_units('16 hours in seconds'));
385 if ($this->rangeEnd
) {
388 'event.utcInitialEpoch <= %d',
389 $this->rangeEnd +
phutil_units('16 hours in seconds'));
392 if ($this->utcInitialEpochMin
!== null) {
395 'event.utcInitialEpoch >= %d',
396 $this->utcInitialEpochMin
);
399 if ($this->utcInitialEpochMax
!== null) {
402 'event.utcInitialEpoch <= %d',
403 $this->utcInitialEpochMax
);
406 if ($this->inviteePHIDs
!== null) {
409 'invitee.inviteePHID IN (%Ls)',
410 $this->inviteePHIDs
);
413 if ($this->hostPHIDs
!== null) {
416 'event.hostPHID IN (%Ls)',
420 if ($this->isCancelled
!== null) {
423 'event.isCancelled = %d',
424 (int)$this->isCancelled
);
427 if ($this->eventsWithNoParent
== true) {
430 'event.instanceOfEventPHID IS NULL');
433 if ($this->instanceSequencePairs
!== null) {
436 foreach ($this->instanceSequencePairs
as $pair) {
439 '(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',
450 if ($this->isStub
!== null) {
457 if ($this->parentEventPHIDs
!== null) {
460 'event.instanceOfEventPHID IN (%Ls)',
461 $this->parentEventPHIDs
);
464 if ($this->importSourcePHIDs
!== null) {
467 'event.importSourcePHID IN (%Ls)',
468 $this->importSourcePHIDs
);
471 if ($this->importAuthorPHIDs
!== null) {
474 'event.importAuthorPHID IN (%Ls)',
475 $this->importAuthorPHIDs
);
478 if ($this->importUIDs
!== null) {
481 'event.importUID IN (%Ls)',
485 if ($this->isImported
!== null) {
486 if ($this->isImported
) {
489 'event.importSourcePHID IS NOT NULL');
493 'event.importSourcePHID IS NULL');
500 protected function getPrimaryTableAlias() {
504 protected function shouldGroupQueryResultRows() {
505 if ($this->inviteePHIDs
!== null) {
508 return parent
::shouldGroupQueryResultRows();
511 public function getQueryApplicationClass() {
512 return 'PhabricatorCalendarApplication';
515 protected function willFilterPage(array $events) {
516 $instance_of_event_phids = array();
517 $recurring_events = array();
518 $viewer = $this->getViewer();
520 $events = $this->getEventsInRange($events);
522 $import_phids = array();
523 foreach ($events as $event) {
524 $import_phid = $event->getImportSourcePHID();
525 if ($import_phid !== null) {
526 $import_phids[$import_phid] = $import_phid;
531 $imports = id(new PhabricatorCalendarImportQuery())
532 ->setParentQuery($this)
534 ->withPHIDs($import_phids)
536 $imports = mpull($imports, null, 'getPHID');
541 foreach ($events as $key => $event) {
542 $import_phid = $event->getImportSourcePHID();
543 if ($import_phid === null) {
544 $event->attachImportSource(null);
548 $import = idx($imports, $import_phid);
550 unset($events[$key]);
551 $this->didRejectResult($event);
555 $event->attachImportSource($import);
560 foreach ($events as $event) {
561 $phids[] = $event->getPHID();
562 $instance_of = $event->getInstanceOfEventPHID();
565 $instance_of_event_phids[] = $instance_of;
569 if (count($instance_of_event_phids) > 0) {
570 $recurring_events = id(new PhabricatorCalendarEventQuery())
572 ->withPHIDs($instance_of_event_phids)
573 ->withEventsWithNoParent(true)
576 $recurring_events = mpull($recurring_events, null, 'getPHID');
580 $invitees = id(new PhabricatorCalendarEventInviteeQuery())
582 ->withEventPHIDs($phids)
584 $invitees = mgroup($invitees, 'getEventPHID');
589 foreach ($events as $key => $event) {
590 $event_invitees = idx($invitees, $event->getPHID(), array());
591 $event->attachInvitees($event_invitees);
593 $instance_of = $event->getInstanceOfEventPHID();
597 $parent = idx($recurring_events, $instance_of);
599 // should never get here
601 unset($events[$key]);
604 $event->attachParentEvent($parent);
606 if ($this->isCancelled
!== null) {
607 if ($event->getIsCancelled() != $this->isCancelled
) {
608 unset($events[$key]);
614 $events = msort($events, 'getStartDateTimeEpoch');
616 if ($this->needRSVPs
) {
617 $rsvp_phids = $this->needRSVPs
;
618 $project_type = PhabricatorProjectProjectPHIDType
::TYPECONST
;
620 $project_phids = array();
621 foreach ($events as $event) {
622 foreach ($event->getInvitees() as $invitee) {
623 $invitee_phid = $invitee->getInviteePHID();
624 if (phid_get_type($invitee_phid) == $project_type) {
625 $project_phids[] = $invitee_phid;
630 if ($project_phids) {
631 $member_type = PhabricatorProjectMaterializedMemberEdgeType
::EDGECONST
;
633 $query = id(new PhabricatorEdgeQuery())
634 ->withSourcePHIDs($project_phids)
635 ->withEdgeTypes(array($member_type))
636 ->withDestinationPHIDs($rsvp_phids);
638 $edges = $query->execute();
640 $project_map = array();
641 foreach ($edges as $src => $types) {
642 foreach ($types as $type => $dsts) {
643 foreach ($dsts as $dst => $edge) {
644 $project_map[$dst][] = $src;
649 $project_map = array();
652 $membership_map = array();
653 foreach ($rsvp_phids as $rsvp_phid) {
654 $membership_map[$rsvp_phid] = array();
655 $membership_map[$rsvp_phid][] = $rsvp_phid;
657 $project_phids = idx($project_map, $rsvp_phid);
658 if ($project_phids) {
659 foreach ($project_phids as $project_phid) {
660 $membership_map[$rsvp_phid][] = $project_phid;
665 foreach ($events as $event) {
666 $invitees = $event->getInvitees();
667 $invitees = mpull($invitees, null, 'getInviteePHID');
670 foreach ($rsvp_phids as $rsvp_phid) {
671 $membership_phids = $membership_map[$rsvp_phid];
672 $rsvps = array_select_keys($invitees, $membership_phids);
673 $rsvp_map[$rsvp_phid] = $rsvps;
676 $event->attachRSVPs($rsvp_map);
683 private function getEventsInRange(array $events) {
684 $range_start = $this->rangeBegin
;
685 $range_end = $this->rangeEnd
;
687 foreach ($events as $key => $event) {
688 $event_start = $event->getStartDateTimeEpoch();
689 $event_end = $event->getEndDateTimeEpoch();
691 if ($range_start && $event_end < $range_start) {
692 unset($events[$key]);
695 if ($range_end && $event_start > $range_end) {
696 unset($events[$key]);
703 private function getRecurrenceWindowStart(
704 PhabricatorCalendarEvent
$event,
707 if (!$generate_from) {
711 return PhutilCalendarAbsoluteDateTime
::newFromEpoch($generate_from);
714 private function getRecurrenceWindowEnd(
715 PhabricatorCalendarEvent
$event,
718 $end_epochs = array();
719 if ($generate_until) {
720 $end_epochs[] = $generate_until;
723 $until_epoch = $event->getUntilDateTimeEpoch();
725 $end_epochs[] = $until_epoch;
732 return PhutilCalendarAbsoluteDateTime
::newFromEpoch(min($end_epochs));
735 private function getRecurrenceLimit(
736 PhabricatorCalendarEvent
$event,
739 $count = $event->getRecurrenceCount();
740 if ($count && ($count <= $raw_limit)) {