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

Drive calendar event queries through the RRULE engine

Summary: Ref T10747. This drives event queries through RRULE, too.

Test Plan: Created recurring events, saw them appear correctly on the calendar.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10747

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

+101 -60
+75 -51
src/applications/calendar/query/PhabricatorCalendarEventQuery.php
··· 165 165 // discard anything outside of the time window. 166 166 $events = $this->getEventsInRange($events); 167 167 168 - $enforced_end = null; 168 + $generate_from = $this->rangeBegin; 169 + $generate_until = $this->rangeEnd; 169 170 foreach ($parents as $key => $event) { 170 - $sequence_start = 0; 171 - $sequence_end = null; 172 - $start = null; 173 - 174 171 $duration = $event->getDuration(); 175 172 176 - $frequency = $event->getFrequencyUnit(); 177 - $modify_key = '+1 '.$frequency; 173 + $start_date = $this->getRecurrenceWindowStart( 174 + $event, 175 + $generate_from - $duration); 178 176 179 - if (($this->rangeBegin !== null) && 180 - ($this->rangeBegin > $event->getStartDateTimeEpoch())) { 181 - $max_date = $this->rangeBegin - $duration; 182 - $date = $event->getStartDateTimeEpoch(); 183 - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 177 + $end_date = $this->getRecurrenceWindowEnd( 178 + $event, 179 + $generate_until); 184 180 185 - while ($date < $max_date) { 186 - // TODO: optimize this to not loop through all off-screen events 187 - $sequence_start++; 188 - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 189 - $date = $datetime->modify($modify_key)->format('U'); 190 - } 181 + $limit = $this->getRecurrenceLimit($event, $raw_limit); 191 182 192 - $start = $this->rangeBegin; 193 - } else { 194 - $start = $event->getStartDateTimeEpoch() - $duration; 195 - } 183 + $set = $event->newRecurrenceSet(); 196 184 197 - $date = $start; 198 - $datetime = PhabricatorTime::getDateTimeFromEpoch($date, $viewer); 199 - 200 - // Select the minimum end time we need to generate events until. 201 - $end_times = array(); 202 - if ($this->rangeEnd) { 203 - $end_times[] = $this->rangeEnd; 204 - } 185 + $recurrences = $set->getEventsBetween( 186 + null, 187 + $end_date, 188 + $limit + 1); 205 189 206 - if ($event->getUntilDateTimeEpoch()) { 207 - $end_times[] = $event->getUntilDateTimeEpoch(); 190 + // We're generating events from the beginning and then filtering them 191 + // here (instead of only generating events starting at the start date) 192 + // because we need to know the proper sequence indexes to generate ghost 193 + // events. This may change after RDATE support. 194 + if ($start_date) { 195 + $start_epoch = $start_date->getEpoch(); 196 + } else { 197 + $start_epoch = null; 208 198 } 209 199 210 - if ($enforced_end) { 211 - $end_times[] = $enforced_end; 212 - } 200 + foreach ($recurrences as $sequence_index => $sequence_datetime) { 201 + if (!$sequence_index) { 202 + // This is the parent event, which we already have. 203 + continue; 204 + } 213 205 214 - if ($end_times) { 215 - $end = min($end_times); 216 - $sequence_end = $sequence_start; 217 - while ($date < $end) { 218 - $sequence_end++; 219 - $datetime->modify($modify_key); 220 - $date = $datetime->format('U'); 221 - if ($sequence_end > $raw_limit + $sequence_start) { 222 - break; 206 + if ($start_epoch) { 207 + if ($sequence_datetime->getEpoch() < $start_epoch) { 208 + continue; 223 209 } 224 210 } 225 - } else { 226 - $sequence_end = $raw_limit + $sequence_start; 227 - } 228 211 229 - $sequence_start = max(1, $sequence_start); 230 - for ($index = $sequence_start; $index < $sequence_end; $index++) { 231 - $events[] = $event->newGhost($viewer, $index); 212 + $events[] = $event->newGhost( 213 + $viewer, 214 + $sequence_index, 215 + $sequence_datetime); 232 216 } 233 217 234 218 // NOTE: We're slicing results every time because this makes it cheaper ··· 240 224 if (count($events) > $raw_limit) { 241 225 $events = msort($events, 'getStartDateTimeEpoch'); 242 226 $events = array_slice($events, 0, $raw_limit, true); 243 - $enforced_end = last($events)->getStartDateTimeEpoch(); 227 + $generate_until = last($events)->getEndDateTimeEpoch(); 244 228 } 245 229 } 246 230 } ··· 524 508 525 509 return $events; 526 510 } 511 + 512 + private function getRecurrenceWindowStart( 513 + PhabricatorCalendarEvent $event, 514 + $generate_from) { 515 + 516 + if (!$generate_from) { 517 + return null; 518 + } 519 + 520 + return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from); 521 + } 522 + 523 + private function getRecurrenceWindowEnd( 524 + PhabricatorCalendarEvent $event, 525 + $generate_until) { 526 + 527 + $end_epochs = array(); 528 + if ($generate_until) { 529 + $end_epochs[] = $generate_until; 530 + } 531 + 532 + $until_epoch = $event->getUntilDateTimeEpoch(); 533 + if ($until_epoch) { 534 + $end_epochs[] = $until_epoch; 535 + } 536 + 537 + if (!$end_epochs) { 538 + return null; 539 + } 540 + 541 + return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs)); 542 + } 543 + 544 + private function getRecurrenceLimit( 545 + PhabricatorCalendarEvent $event, 546 + $raw_limit) { 547 + 548 + return $raw_limit; 549 + } 550 + 527 551 528 552 }
+26 -9
src/applications/calendar/storage/PhabricatorCalendarEvent.php
··· 103 103 ->applyViewerTimezone($actor); 104 104 } 105 105 106 - private function newChild(PhabricatorUser $actor, $sequence) { 106 + private function newChild( 107 + PhabricatorUser $actor, 108 + $sequence, 109 + PhutilCalendarDateTime $start = null) { 107 110 if (!$this->isParentEvent()) { 108 111 throw new Exception( 109 112 pht( ··· 124 127 ->setDateFrom(0) 125 128 ->setDateTo(0); 126 129 127 - return $child->copyFromParent($actor); 130 + return $child->copyFromParent($actor, $start); 128 131 } 129 132 130 133 protected function readField($field) { ··· 156 159 } 157 160 158 161 159 - public function copyFromParent(PhabricatorUser $actor) { 162 + public function copyFromParent( 163 + PhabricatorUser $actor, 164 + PhutilCalendarDateTime $start = null) { 165 + 160 166 if (!$this->isChildEvent()) { 161 167 throw new Exception( 162 168 pht( ··· 176 182 ->setDescription($parent->getDescription()); 177 183 178 184 $sequence = $this->getSequenceIndex(); 179 - $start_datetime = $parent->newSequenceIndexDateTime($sequence); 185 + 186 + if ($start) { 187 + $start_datetime = $start; 188 + } else { 189 + $start_datetime = $parent->newSequenceIndexDateTime($sequence); 180 190 181 - if (!$start_datetime) { 182 - throw new Exception( 183 - "Sequence {$sequence} does not exist for event!"); 191 + if (!$start_datetime) { 192 + throw new Exception( 193 + pht( 194 + 'Sequence "%s" is not valid for event!', 195 + $sequence)); 196 + } 184 197 } 185 198 186 199 $duration = $parent->newDuration(); ··· 225 238 return $stub; 226 239 } 227 240 228 - public function newGhost(PhabricatorUser $actor, $sequence) { 229 - $ghost = $this->newChild($actor, $sequence); 241 + public function newGhost( 242 + PhabricatorUser $actor, 243 + $sequence, 244 + PhutilCalendarDateTime $start = null) { 245 + 246 + $ghost = $this->newChild($actor, $sequence, $start); 230 247 231 248 $ghost 232 249 ->setIsGhostEvent(true)