3 final class PhabricatorCalendarEventSearchEngine
4 extends PhabricatorApplicationSearchEngine
{
7 private $calendarMonth;
10 public function getResultTypeDescription() {
11 return pht('Calendar Events');
14 public function getApplicationClassName() {
15 return 'PhabricatorCalendarApplication';
18 public function newQuery() {
19 $viewer = $this->requireViewer();
21 return id(new PhabricatorCalendarEventQuery())
22 ->needRSVPs(array($viewer->getPHID()));
25 protected function shouldShowOrderField() {
29 protected function buildCustomSearchFields() {
31 id(new PhabricatorSearchDatasourceField())
32 ->setLabel(pht('Hosts'))
34 ->setAliases(array('host', 'hostPHID', 'hosts'))
35 ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()),
36 id(new PhabricatorSearchDatasourceField())
37 ->setLabel(pht('Invited'))
38 ->setKey('invitedPHIDs')
39 ->setDatasource(new PhabricatorCalendarInviteeDatasource()),
40 id(new PhabricatorSearchDateControlField())
41 ->setLabel(pht('Occurs After'))
42 ->setKey('rangeStart'),
43 id(new PhabricatorSearchDateControlField())
44 ->setLabel(pht('Occurs Before'))
46 ->setAliases(array('rangeEnd')),
47 id(new PhabricatorSearchCheckboxesField())
50 'upcoming' => pht('Show only upcoming events.'),
52 id(new PhabricatorSearchSelectField())
53 ->setLabel(pht('Cancelled Events'))
54 ->setKey('isCancelled')
55 ->setOptions($this->getCancelledOptions())
56 ->setDefault('active'),
57 id(new PhabricatorPHIDsSearchField())
58 ->setLabel(pht('Import Sources'))
59 ->setKey('importSourcePHIDs')
60 ->setAliases(array('importSourcePHID')),
61 id(new PhabricatorSearchSelectField())
62 ->setLabel(pht('Display Options'))
64 ->setOptions($this->getViewOptions())
65 ->setDefault('month'),
69 private function getCancelledOptions() {
71 'active' => pht('Active Events Only'),
72 'cancelled' => pht('Cancelled Events Only'),
73 'both' => pht('Both Cancelled and Active Events'),
77 private function getViewOptions() {
79 'month' => pht('Month View'),
80 'day' => pht('Day View'),
81 'list' => pht('List View'),
85 public function buildQueryFromSavedQuery(PhabricatorSavedQuery
$saved) {
86 $query = parent
::buildQueryFromSavedQuery($saved);
88 // If this is an export query for generating an ".ics" file, don't
89 // build ghost events.
90 if ($saved->getParameter('export')) {
91 $query->setGenerateGhosts(false);
97 protected function buildQueryFromParameters(array $map) {
98 $query = $this->newQuery();
99 $viewer = $this->requireViewer();
101 if ($map['hostPHIDs']) {
102 $query->withHostPHIDs($map['hostPHIDs']);
105 if ($map['invitedPHIDs']) {
106 $query->withInvitedPHIDs($map['invitedPHIDs']);
109 $range_start = $map['rangeStart'];
110 $range_end = $map['rangeEnd'];
111 $display = $map['display'];
113 if ($map['upcoming'] && $map['upcoming'][0] == 'upcoming') {
119 list($range_start, $range_end) = $this->getQueryDateRange(
125 $query->withDateRange($range_start, $range_end);
127 switch ($map['isCancelled']) {
129 $query->withIsCancelled(false);
132 $query->withIsCancelled(true);
136 if ($map['importSourcePHIDs']) {
137 $query->withImportSourcePHIDs($map['importSourcePHIDs']);
140 if (!$map['ids'] && !$map['phids']) {
143 ->setGenerateGhosts(true);
149 private function getQueryDateRange(
155 $start_date_value = $this->getSafeDate($start_date_wild);
156 $end_date_value = $this->getSafeDate($end_date_wild);
158 $viewer = $this->requireViewer();
159 $timezone = new DateTimeZone($viewer->getTimezoneIdentifier());
163 $min_range = $start_date_value->getEpoch();
164 $max_range = $end_date_value->getEpoch();
166 if ($display == 'month' ||
$display == 'day') {
167 list($start_year, $start_month, $start_day) =
168 $this->getDisplayYearAndMonthAndDay($min_range, $max_range, $display);
170 $start_day = new DateTime(
171 "{$start_year}-{$start_month}-{$start_day}",
173 $next = clone $start_day;
175 if ($display == 'month') {
176 $next->modify('+1 month');
177 } else if ($display == 'day') {
178 $next->modify('+7 day');
181 $display_start = $start_day->format('U');
182 $display_end = $next->format('U');
184 $start_of_week = $viewer->getUserSetting(
185 PhabricatorWeekStartDaySetting
::SETTINGKEY
);
187 $end_of_week = ($start_of_week +
6) %
7;
189 $first_of_month = $start_day->format('w');
190 $last_of_month = id(clone $next)->modify('-1 day')->format('w');
192 if (!$min_range ||
($min_range < $display_start)) {
193 $min_range = $display_start;
195 if ($display == 'month' &&
196 $first_of_month !== $start_of_week) {
197 $interim_day_num = ($first_of_month +
7 - $start_of_week) %
7;
198 $min_range = id(clone $start_day)
199 ->modify('-'.$interim_day_num.' days')
203 if (!$max_range ||
($max_range > $display_end)) {
204 $max_range = $display_end;
206 if ($display == 'month' &&
207 $last_of_month !== $end_of_week) {
208 $interim_day_num = ($end_of_week +
7 - $last_of_month) %
7;
209 $max_range = id(clone $next)
210 ->modify('+'.$interim_day_num.' days')
217 $now = PhabricatorTime
::getNow();
219 $min_range = max($now, $min_range);
225 return array($min_range, $max_range);
228 protected function getURI($path) {
229 return '/calendar/'.$path;
232 protected function getBuiltinQueryNames() {
234 'month' => pht('Month View'),
235 'day' => pht('Day View'),
236 'upcoming' => pht('Upcoming Events'),
237 'all' => pht('All Events'),
243 public function setCalendarYearAndMonthAndDay($year, $month, $day = null) {
244 $this->calendarYear
= $year;
245 $this->calendarMonth
= $month;
246 $this->calendarDay
= $day;
251 public function buildSavedQueryFromBuiltin($query_key) {
252 $query = $this->newSavedQuery();
253 $query->setQueryKey($query_key);
255 switch ($query_key) {
257 return $query->setParameter('display', 'month');
259 return $query->setParameter('display', 'day');
262 ->setParameter('display', 'list')
263 ->setParameter('upcoming', array(
270 return parent
::buildSavedQueryFromBuiltin($query_key);
273 protected function renderResultList(
275 PhabricatorSavedQuery
$query,
278 if ($this->isMonthView($query)) {
279 $result = $this->buildCalendarMonthView($events, $query);
280 } else if ($this->isDayView($query)) {
281 $result = $this->buildCalendarDayView($events, $query);
283 $result = $this->buildCalendarListView($events, $query);
289 private function buildCalendarListView(
291 PhabricatorSavedQuery
$query) {
293 assert_instances_of($events, 'PhabricatorCalendarEvent');
294 $viewer = $this->requireViewer();
295 $list = new PHUIObjectItemListView();
297 foreach ($events as $event) {
298 if ($event->getIsGhostEvent()) {
299 $monogram = $event->getParentEvent()->getMonogram();
300 $index = $event->getSequenceIndex();
301 $monogram = "{$monogram}/{$index}";
303 $monogram = $event->getMonogram();
306 $item = id(new PHUIObjectItemView())
309 ->setObjectName($monogram)
310 ->setHeader($event->getName())
311 ->setHref($event->getURI());
313 $item->addAttribute($event->renderEventDate($viewer, false));
315 if ($event->getIsCancelled()) {
316 $item->setDisabled(true);
319 $status_icon = $event->getDisplayIcon($viewer);
320 $status_color = $event->getDisplayIconColor($viewer);
321 $status_label = $event->getDisplayIconLabel($viewer);
323 $item->setStatusIcon("{$status_icon} {$status_color}", $status_label);
327 $viewer->renderHandle($event->getHostPHID()));
328 $item->addByline($host);
330 $list->addItem($item);
333 return $this->newResultView()
334 ->setObjectList($list)
335 ->setNoDataString(pht('No events found.'));
338 private function buildCalendarMonthView(
340 PhabricatorSavedQuery
$query) {
341 assert_instances_of($events, 'PhabricatorCalendarEvent');
343 $viewer = $this->requireViewer();
344 $now = PhabricatorTime
::getNow();
346 list($start_year, $start_month) =
347 $this->getDisplayYearAndMonthAndDay(
348 $this->getQueryDateFrom($query)->getEpoch(),
349 $this->getQueryDateTo($query)->getEpoch(),
350 $query->getParameter('display'));
352 $now_year = phabricator_format_local_time($now, $viewer, 'Y');
353 $now_month = phabricator_format_local_time($now, $viewer, 'm');
354 $now_day = phabricator_format_local_time($now, $viewer, 'j');
356 if ($start_month == $now_month && $start_year == $now_year) {
357 $month_view = new PHUICalendarMonthView(
358 $this->getQueryDateFrom($query),
359 $this->getQueryDateTo($query),
364 $month_view = new PHUICalendarMonthView(
365 $this->getQueryDateFrom($query),
366 $this->getQueryDateTo($query),
371 $month_view->setUser($viewer);
373 $viewer_phid = $viewer->getPHID();
374 foreach ($events as $event) {
375 $epoch_min = $event->getStartDateTimeEpoch();
376 $epoch_max = $event->getEndDateTimeEpoch();
378 $is_invited = $event->isRSVPInvited($viewer_phid);
379 $is_attending = $event->getIsUserAttending($viewer_phid);
381 $event_view = id(new AphrontCalendarEventView())
382 ->setHostPHID($event->getHostPHID())
383 ->setEpochRange($epoch_min, $epoch_max)
384 ->setIsCancelled($event->getIsCancelled())
385 ->setName($event->getName())
386 ->setURI($event->getURI())
387 ->setIsAllDay($event->getIsAllDay())
388 ->setIcon($event->getDisplayIcon($viewer))
389 ->setViewerIsInvited($is_invited ||
$is_attending)
390 ->setDatetimeSummary($event->renderEventDate($viewer, true))
391 ->setIconColor($event->getDisplayIconColor($viewer));
393 $month_view->addEvent($event_view);
396 $month_view->setBrowseURI(
397 $this->getURI('query/'.$query->getQueryKey().'/'));
399 $from = $this->getQueryDateFrom($query)->getDateTime();
402 $crumbs[] = id(new PHUICrumbView())
403 ->setName($from->format('F Y'));
405 $header = id(new PHUIHeaderView())
406 ->setProfileHeader(true)
407 ->setHeader($from->format('F Y'));
409 return $this->newResultView($month_view)
411 ->setHeader($header);
414 private function buildCalendarDayView(
416 PhabricatorSavedQuery
$query) {
418 $viewer = $this->requireViewer();
420 list($start_year, $start_month, $start_day) =
421 $this->getDisplayYearAndMonthAndDay(
422 $this->getQueryDateFrom($query)->getEpoch(),
423 $this->getQueryDateTo($query)->getEpoch(),
424 $query->getParameter('display'));
426 $day_view = id(new PHUICalendarDayView(
427 $this->getQueryDateFrom($query),
428 $this->getQueryDateTo($query),
432 ->setQuery($query->getQueryKey());
434 $day_view->setUser($viewer);
436 $phids = mpull($events, 'getHostPHID');
438 foreach ($events as $event) {
439 $can_edit = PhabricatorPolicyFilter
::hasCapability(
442 PhabricatorPolicyCapability
::CAN_EDIT
);
444 $epoch_min = $event->getStartDateTimeEpoch();
445 $epoch_max = $event->getEndDateTimeEpoch();
447 $status_icon = $event->getDisplayIcon($viewer);
448 $status_color = $event->getDisplayIconColor($viewer);
450 $event_view = id(new AphrontCalendarEventView())
451 ->setCanEdit($can_edit)
452 ->setEventID($event->getID())
453 ->setEpochRange($epoch_min, $epoch_max)
454 ->setIsAllDay($event->getIsAllDay())
455 ->setIcon($status_icon)
456 ->setIconColor($status_color)
457 ->setName($event->getName())
458 ->setURI($event->getURI())
459 ->setDatetimeSummary($event->renderEventDate($viewer, true))
460 ->setIsCancelled($event->getIsCancelled());
462 $day_view->addEvent($event_view);
465 $browse_uri = $this->getURI('query/'.$query->getQueryKey().'/');
466 $day_view->setBrowseURI($browse_uri);
468 $from = $this->getQueryDateFrom($query)->getDateTime();
469 $month_uri = $browse_uri.$from->format('Y/m/');
472 id(new PHUICrumbView())
473 ->setName($from->format('F Y'))
474 ->setHref($month_uri),
475 id(new PHUICrumbView())
476 ->setName($from->format('D jS')),
479 $header = id(new PHUIHeaderView())
480 ->setProfileHeader(true)
481 ->setHeader($from->format('D, F jS'));
483 return $this->newResultView($day_view)
485 ->setHeader($header);
488 private function getDisplayYearAndMonthAndDay(
493 $viewer = $this->requireViewer();
496 if ($this->calendarYear
&& $this->calendarMonth
) {
497 $start_year = $this->calendarYear
;
498 $start_month = $this->calendarMonth
;
499 $start_day = $this->calendarDay ?
$this->calendarDay
: 1;
502 $epoch = $range_start;
503 } else if ($range_end) {
508 if ($display == 'month') {
511 $day = phabricator_format_local_time($epoch, $viewer, 'd');
513 $start_year = phabricator_format_local_time($epoch, $viewer, 'Y');
514 $start_month = phabricator_format_local_time($epoch, $viewer, 'm');
517 return array($start_year, $start_month, $start_day);
520 public function getPageSize(PhabricatorSavedQuery
$saved) {
521 if ($this->isMonthView($saved) ||
$this->isDayView($saved)) {
522 return $saved->getParameter('limit', 1000);
524 return $saved->getParameter('limit', 100);
528 private function getQueryDateFrom(PhabricatorSavedQuery
$saved) {
529 if ($this->calendarYear
&& $this->calendarMonth
) {
530 $viewer = $this->requireViewer();
532 $start_year = $this->calendarYear
;
533 $start_month = $this->calendarMonth
;
534 $start_day = $this->calendarDay ?
$this->calendarDay
: 1;
536 return AphrontFormDateControlValue
::newFromDictionary(
539 'd' => "{$start_year}-{$start_month}-{$start_day}",
543 return $this->getQueryDate($saved, 'rangeStart');
546 private function getQueryDateTo(PhabricatorSavedQuery
$saved) {
547 return $this->getQueryDate($saved, 'rangeEnd');
550 private function getQueryDate(PhabricatorSavedQuery
$saved, $key) {
551 $viewer = $this->requireViewer();
553 $wild = $saved->getParameter($key);
554 return $this->getSafeDate($wild);
557 private function getSafeDate($value) {
558 $viewer = $this->requireViewer();
560 // ideally this would be consistent and always pass in the same type
561 if ($value instanceof AphrontFormDateControlValue
) {
564 $value = AphrontFormDateControlValue
::newFromWild($viewer, $value);
567 $value = AphrontFormDateControlValue
::newFromEpoch(
569 PhabricatorTime
::getTodayMidnightDateTime($viewer)->format('U'));
570 $value->setEnabled(false);
573 $value->setOptional(true);
578 private function isMonthView(PhabricatorSavedQuery
$query) {
579 if ($this->isDayView($query)) {
582 if ($query->getParameter('display') == 'month') {
587 private function isDayView(PhabricatorSavedQuery
$query) {
588 if ($query->getParameter('display') == 'day') {
591 if ($this->calendarDay
) {
598 public function newUseResultsActions(PhabricatorSavedQuery
$saved) {
599 $viewer = $this->requireViewer();
600 $can_export = $viewer->isLoggedIn();
603 id(new PhabricatorActionView())
604 ->setIcon('fa-download')
605 ->setName(pht('Export Query as .ics'))
606 ->setDisabled(!$can_export)
607 ->setHref('/calendar/export/edit/?queryKey='.$saved->getQueryKey()),
612 private function newResultView($content = null) {
613 // If we aren't rendering a dashboard panel, activate global drag-and-drop
614 // so you can import ".ics" files by dropping them directly onto the
616 if (!$this->isPanelContext()) {
617 $drop_upload = id(new PhabricatorGlobalUploadTargetView())
618 ->setViewer($this->requireViewer())
619 ->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import'))
620 ->setSubmitURI('/calendar/import/drop/')
621 ->setViewPolicy(PhabricatorPolicies
::POLICY_NOONE
);
629 return id(new PhabricatorApplicationSearchResultView())
630 ->setContent($content);