Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php
12256 views
1
<?php
2
3
final class PhabricatorCalendarEventViewController
4
extends PhabricatorCalendarController {
5
6
public function shouldAllowPublic() {
7
return true;
8
}
9
10
public function handleRequest(AphrontRequest $request) {
11
$viewer = $request->getViewer();
12
13
$event = $this->loadEvent();
14
if (!$event) {
15
return new Aphront404Response();
16
}
17
18
// If we looked up or generated a stub event, redirect to that event's
19
// canonical URI.
20
$id = $request->getURIData('id');
21
if ($event->getID() != $id) {
22
$uri = $event->getURI();
23
return id(new AphrontRedirectResponse())->setURI($uri);
24
}
25
26
$monogram = $event->getMonogram();
27
$page_title = $monogram.' '.$event->getName();
28
$crumbs = $this->buildApplicationCrumbs();
29
30
$start = $event->newStartDateTime()
31
->newPHPDateTime();
32
33
$crumbs->addTextCrumb(
34
$start->format('F Y'),
35
'/calendar/query/month/'.$start->format('Y/m/'));
36
37
$crumbs->addTextCrumb(
38
$start->format('D jS'),
39
'/calendar/query/month/'.$start->format('Y/m/d/'));
40
41
$crumbs->addTextCrumb($monogram);
42
$crumbs->setBorder(true);
43
44
$timeline = $this->buildTransactionTimeline(
45
$event,
46
new PhabricatorCalendarEventTransactionQuery());
47
48
$header = $this->buildHeaderView($event);
49
$subheader = $this->buildSubheaderView($event);
50
$curtain = $this->buildCurtain($event);
51
$details = $this->buildPropertySection($event);
52
$recurring = $this->buildRecurringSection($event);
53
$description = $this->buildDescriptionView($event);
54
55
$comment_view = id(new PhabricatorCalendarEventEditEngine())
56
->setViewer($viewer)
57
->buildEditEngineCommentView($event);
58
59
$timeline->setQuoteRef($monogram);
60
$comment_view->setTransactionTimeline($timeline);
61
62
$details_header = id(new PHUIHeaderView())
63
->setHeader(pht('Details'));
64
$recurring_header = $this->buildRecurringHeader($event);
65
66
// NOTE: This is a bit hacky: for imported events, we're just hiding the
67
// comment form without actually preventing comments. Users could still
68
// submit a request to add comments to these events. This isn't really a
69
// major problem since they can't do anything truly bad and there isn't an
70
// easy way to selectively disable this or some other similar behaviors
71
// today, but it would probably be nice to fully disable these
72
// "pseudo-edits" (like commenting and probably subscribing and awarding
73
// tokens) at some point.
74
if ($event->isImportedEvent()) {
75
$comment_view = null;
76
$timeline->setShouldTerminate(true);
77
}
78
79
$view = id(new PHUITwoColumnView())
80
->setHeader($header)
81
->setSubheader($subheader)
82
->setMainColumn(
83
array(
84
$timeline,
85
$comment_view,
86
))
87
->setCurtain($curtain)
88
->addPropertySection(pht('Description'), $description)
89
->addPropertySection($recurring_header, $recurring)
90
->addPropertySection($details_header, $details);
91
92
return $this->newPage()
93
->setTitle($page_title)
94
->setCrumbs($crumbs)
95
->setPageObjectPHIDs(array($event->getPHID()))
96
->appendChild($view);
97
}
98
99
private function buildHeaderView(
100
PhabricatorCalendarEvent $event) {
101
$viewer = $this->getViewer();
102
$id = $event->getID();
103
104
if ($event->getIsCancelled()) {
105
$icon = 'fa-ban';
106
$color = 'red';
107
$status = pht('Cancelled');
108
} else {
109
$icon = 'fa-check';
110
$color = 'bluegrey';
111
$status = pht('Active');
112
}
113
114
$header = id(new PHUIHeaderView())
115
->setUser($viewer)
116
->setHeader($event->getName())
117
->setStatus($icon, $color, $status)
118
->setPolicyObject($event)
119
->setHeaderIcon($event->getIcon());
120
121
if ($event->isImportedEvent()) {
122
$header->addTag(
123
id(new PHUITagView())
124
->setType(PHUITagView::TYPE_SHADE)
125
->setName(pht('Imported'))
126
->setIcon('fa-download')
127
->setHref($event->getImportSource()->getURI())
128
->setColor(PHUITagView::COLOR_ORANGE));
129
}
130
131
foreach ($this->buildRSVPActions($event) as $action) {
132
$header->addActionLink($action);
133
}
134
135
$options = PhabricatorCalendarEventInvitee::getAvailabilityMap();
136
137
$is_attending = $event->getIsUserAttending($viewer->getPHID());
138
if ($is_attending) {
139
$invitee = $event->getInviteeForPHID($viewer->getPHID());
140
141
$selected = $invitee->getDisplayAvailability($event);
142
if (!$selected) {
143
$selected = PhabricatorCalendarEventInvitee::AVAILABILITY_AVAILABLE;
144
}
145
146
$selected_option = idx($options, $selected);
147
148
$availability_select = id(new PHUIButtonView())
149
->setTag('a')
150
->setIcon('fa-circle '.$selected_option['color'])
151
->setText(pht('Availability: %s', $selected_option['name']));
152
153
$dropdown = id(new PhabricatorActionListView())
154
->setUser($viewer);
155
156
foreach ($options as $key => $option) {
157
$uri = "event/availability/{$id}/{$key}/";
158
$uri = $this->getApplicationURI($uri);
159
160
$dropdown->addAction(
161
id(new PhabricatorActionView())
162
->setName($option['name'])
163
->setIcon('fa-circle '.$option['color'])
164
->setHref($uri)
165
->setWorkflow(true));
166
}
167
168
$availability_select->setDropdownMenu($dropdown);
169
$header->addActionLink($availability_select);
170
}
171
172
return $header;
173
}
174
175
private function buildCurtain(PhabricatorCalendarEvent $event) {
176
$viewer = $this->getRequest()->getUser();
177
$id = $event->getID();
178
$is_attending = $event->getIsUserAttending($viewer->getPHID());
179
180
$can_edit = PhabricatorPolicyFilter::hasCapability(
181
$viewer,
182
$event,
183
PhabricatorPolicyCapability::CAN_EDIT);
184
185
$edit_uri = "event/edit/{$id}/";
186
$edit_uri = $this->getApplicationURI($edit_uri);
187
$is_recurring = $event->getIsRecurring();
188
$edit_label = pht('Edit Event');
189
190
$curtain = $this->newCurtainView($event);
191
192
if ($edit_label && $edit_uri) {
193
$curtain->addAction(
194
id(new PhabricatorActionView())
195
->setName($edit_label)
196
->setIcon('fa-pencil')
197
->setHref($edit_uri)
198
->setDisabled(!$can_edit)
199
->setWorkflow(!$can_edit || $is_recurring));
200
}
201
202
$recurring_uri = "{$edit_uri}page/recurring/";
203
$can_recurring = $can_edit && !$event->isChildEvent();
204
205
if ($event->getIsRecurring()) {
206
$recurring_label = pht('Edit Recurrence');
207
} else {
208
$recurring_label = pht('Make Recurring');
209
}
210
211
$curtain->addAction(
212
id(new PhabricatorActionView())
213
->setName($recurring_label)
214
->setIcon('fa-repeat')
215
->setHref($recurring_uri)
216
->setDisabled(!$can_recurring)
217
->setWorkflow(true));
218
219
$can_attend = !$event->isImportedEvent();
220
221
if ($is_attending) {
222
$curtain->addAction(
223
id(new PhabricatorActionView())
224
->setName(pht('Decline Event'))
225
->setIcon('fa-user-times')
226
->setHref($this->getApplicationURI("event/join/{$id}/"))
227
->setDisabled(!$can_attend)
228
->setWorkflow(true));
229
} else {
230
$curtain->addAction(
231
id(new PhabricatorActionView())
232
->setName(pht('Join Event'))
233
->setIcon('fa-user-plus')
234
->setHref($this->getApplicationURI("event/join/{$id}/"))
235
->setDisabled(!$can_attend)
236
->setWorkflow(true));
237
}
238
239
$cancel_uri = $this->getApplicationURI("event/cancel/{$id}/");
240
$cancel_disabled = !$can_edit;
241
242
$cancel_label = pht('Cancel Event');
243
$reinstate_label = pht('Reinstate Event');
244
245
if ($event->getIsCancelled()) {
246
$curtain->addAction(
247
id(new PhabricatorActionView())
248
->setName($reinstate_label)
249
->setIcon('fa-plus')
250
->setHref($cancel_uri)
251
->setDisabled($cancel_disabled)
252
->setWorkflow(true));
253
} else {
254
$curtain->addAction(
255
id(new PhabricatorActionView())
256
->setName($cancel_label)
257
->setIcon('fa-times')
258
->setHref($cancel_uri)
259
->setDisabled($cancel_disabled)
260
->setWorkflow(true));
261
}
262
263
$ics_name = $event->getICSFilename();
264
$export_uri = $this->getApplicationURI("event/export/{$id}/{$ics_name}");
265
266
$curtain->addAction(
267
id(new PhabricatorActionView())
268
->setName(pht('Export as .ics'))
269
->setIcon('fa-download')
270
->setHref($export_uri));
271
272
return $curtain;
273
}
274
275
private function buildPropertySection(
276
PhabricatorCalendarEvent $event) {
277
$viewer = $this->getViewer();
278
279
$properties = id(new PHUIPropertyListView())
280
->setUser($viewer);
281
282
$invitees = $event->getInvitees();
283
foreach ($invitees as $key => $invitee) {
284
if ($invitee->isUninvited()) {
285
unset($invitees[$key]);
286
}
287
}
288
289
if ($invitees) {
290
$invitee_list = new PHUIStatusListView();
291
292
$icon_invited = PHUIStatusItemView::ICON_OPEN;
293
$icon_attending = PHUIStatusItemView::ICON_ACCEPT;
294
$icon_declined = PHUIStatusItemView::ICON_REJECT;
295
296
$status_invited = PhabricatorCalendarEventInvitee::STATUS_INVITED;
297
$status_attending = PhabricatorCalendarEventInvitee::STATUS_ATTENDING;
298
$status_declined = PhabricatorCalendarEventInvitee::STATUS_DECLINED;
299
300
$icon_map = array(
301
$status_invited => $icon_invited,
302
$status_attending => $icon_attending,
303
$status_declined => $icon_declined,
304
);
305
306
$icon_color_map = array(
307
$status_invited => null,
308
$status_attending => 'green',
309
$status_declined => 'red',
310
);
311
312
$viewer_phid = $viewer->getPHID();
313
$is_rsvp_invited = $event->isRSVPInvited($viewer_phid);
314
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
315
316
$head = array();
317
$tail = array();
318
foreach ($invitees as $invitee) {
319
$item = new PHUIStatusItemView();
320
$invitee_phid = $invitee->getInviteePHID();
321
$status = $invitee->getStatus();
322
$target = $viewer->renderHandle($invitee_phid);
323
$is_user = (phid_get_type($invitee_phid) == $type_user);
324
325
if (!$is_user) {
326
$icon = 'fa-users';
327
$icon_color = 'blue';
328
} else {
329
$icon = $icon_map[$status];
330
$icon_color = $icon_color_map[$status];
331
}
332
333
// Highlight invited groups which you're a member of if you have
334
// not RSVP'd to an event yet.
335
if ($is_rsvp_invited) {
336
if ($invitee_phid != $viewer_phid) {
337
if ($event->hasRSVPAuthority($viewer_phid, $invitee_phid)) {
338
$item->setHighlighted(true);
339
}
340
}
341
}
342
343
$item->setIcon($icon, $icon_color)
344
->setTarget($target);
345
346
if ($is_user) {
347
$tail[] = $item;
348
} else {
349
$head[] = $item;
350
}
351
}
352
353
foreach (array_merge($head, $tail) as $item) {
354
$invitee_list->addItem($item);
355
}
356
} else {
357
$invitee_list = phutil_tag(
358
'em',
359
array(),
360
pht('None'));
361
}
362
363
if ($event->isImportedEvent()) {
364
$properties->addProperty(
365
pht('Imported By'),
366
pht(
367
'%s from %s',
368
$viewer->renderHandle($event->getImportAuthorPHID()),
369
$viewer->renderHandle($event->getImportSourcePHID())));
370
}
371
372
$properties->addProperty(
373
pht('Invitees'),
374
$invitee_list);
375
376
$properties->invokeWillRenderEvent();
377
378
return $properties;
379
}
380
381
private function buildRecurringHeader(PhabricatorCalendarEvent $event) {
382
$viewer = $this->getViewer();
383
384
if (!$event->getIsRecurring()) {
385
return null;
386
}
387
388
$header = id(new PHUIHeaderView())
389
->setHeader(pht('Recurring Event'));
390
391
$sequence = $event->getSequenceIndex();
392
if ($event->isParentEvent()) {
393
$parent = $event;
394
} else {
395
$parent = $event->getParentEvent();
396
}
397
398
if ($parent->isValidSequenceIndex($viewer, $sequence + 1)) {
399
$next_uri = $parent->getURI().'/'.($sequence + 1);
400
$has_next = true;
401
} else {
402
$next_uri = null;
403
$has_next = false;
404
}
405
406
if ($sequence) {
407
if ($sequence > 1) {
408
$previous_uri = $parent->getURI().'/'.($sequence - 1);
409
} else {
410
$previous_uri = $parent->getURI();
411
}
412
$has_previous = true;
413
} else {
414
$has_previous = false;
415
$previous_uri = null;
416
}
417
418
$prev_button = id(new PHUIButtonView())
419
->setTag('a')
420
->setIcon('fa-chevron-left')
421
->setHref($previous_uri)
422
->setDisabled(!$has_previous)
423
->setText(pht('Previous'));
424
425
$next_button = id(new PHUIButtonView())
426
->setTag('a')
427
->setIcon('fa-chevron-right')
428
->setHref($next_uri)
429
->setDisabled(!$has_next)
430
->setText(pht('Next'));
431
432
$header
433
->addActionLink($next_button)
434
->addActionLink($prev_button);
435
436
return $header;
437
}
438
439
private function buildRecurringSection(PhabricatorCalendarEvent $event) {
440
$viewer = $this->getViewer();
441
442
if (!$event->getIsRecurring()) {
443
return null;
444
}
445
446
$properties = id(new PHUIPropertyListView())
447
->setUser($viewer);
448
449
$is_parent = $event->isParentEvent();
450
if ($is_parent) {
451
$parent_link = null;
452
} else {
453
$parent = $event->getParentEvent();
454
$parent_link = $viewer
455
->renderHandle($parent->getPHID())
456
->render();
457
}
458
459
$rrule = $event->newRecurrenceRule();
460
461
if ($rrule) {
462
$frequency = $rrule->getFrequency();
463
} else {
464
$frequency = null;
465
}
466
467
switch ($frequency) {
468
case PhutilCalendarRecurrenceRule::FREQUENCY_DAILY:
469
if ($is_parent) {
470
$message = pht('This event repeats every day.');
471
} else {
472
$message = pht(
473
'This event is an instance of %s, and repeats every day.',
474
$parent_link);
475
}
476
break;
477
case PhutilCalendarRecurrenceRule::FREQUENCY_WEEKLY:
478
if ($is_parent) {
479
$message = pht('This event repeats every week.');
480
} else {
481
$message = pht(
482
'This event is an instance of %s, and repeats every week.',
483
$parent_link);
484
}
485
break;
486
case PhutilCalendarRecurrenceRule::FREQUENCY_MONTHLY:
487
if ($is_parent) {
488
$message = pht('This event repeats every month.');
489
} else {
490
$message = pht(
491
'This event is an instance of %s, and repeats every month.',
492
$parent_link);
493
}
494
break;
495
case PhutilCalendarRecurrenceRule::FREQUENCY_YEARLY:
496
if ($is_parent) {
497
$message = pht('This event repeats every year.');
498
} else {
499
$message = pht(
500
'This event is an instance of %s, and repeats every year.',
501
$parent_link);
502
}
503
break;
504
}
505
506
$properties->addProperty(pht('Event Series'), $message);
507
508
return $properties;
509
}
510
511
private function buildDescriptionView(
512
PhabricatorCalendarEvent $event) {
513
$viewer = $this->getViewer();
514
515
$properties = id(new PHUIPropertyListView())
516
->setUser($viewer);
517
518
if (strlen($event->getDescription())) {
519
$description = new PHUIRemarkupView($viewer, $event->getDescription());
520
$properties->addTextContent($description);
521
return $properties;
522
}
523
524
return null;
525
}
526
527
private function loadEvent() {
528
$request = $this->getRequest();
529
$viewer = $this->getViewer();
530
531
$id = $request->getURIData('id');
532
$sequence = $request->getURIData('sequence');
533
534
// We're going to figure out which event you're trying to look at. Most of
535
// the time this is simple, but you may be looking at an instance of a
536
// recurring event which we haven't generated an object for.
537
538
// If you are, we're going to generate a "stub" event so we have a real
539
// ID and PHID to work with, since the rest of the infrastructure relies
540
// on these identifiers existing.
541
542
// Load the event identified by ID first.
543
$event = id(new PhabricatorCalendarEventQuery())
544
->setViewer($viewer)
545
->withIDs(array($id))
546
->needRSVPs(array($viewer->getPHID()))
547
->executeOne();
548
if (!$event) {
549
return null;
550
}
551
552
// If we aren't looking at an instance of this event, this is a completely
553
// normal request and we can just return this event.
554
if (!$sequence) {
555
return $event;
556
}
557
558
// When you view "E123/999", E123 is normally the parent event. However,
559
// you might visit a different instance first instead and then fiddle
560
// with the URI. If the event we're looking at is a child, we are going
561
// to act on the parent instead.
562
if ($event->isChildEvent()) {
563
$event = $event->getParentEvent();
564
}
565
566
// Try to load the instance. If it already exists, we're all done and
567
// can just return it.
568
$instance = id(new PhabricatorCalendarEventQuery())
569
->setViewer($viewer)
570
->withInstanceSequencePairs(
571
array(
572
array($event->getPHID(), $sequence),
573
))
574
->executeOne();
575
if ($instance) {
576
return $instance;
577
}
578
579
if (!$viewer->isLoggedIn()) {
580
throw new Exception(
581
pht(
582
'This event instance has not been created yet. Log in to create '.
583
'it.'));
584
}
585
586
if (!$event->isValidSequenceIndex($viewer, $sequence)) {
587
return null;
588
}
589
590
return $event->newStub($viewer, $sequence);
591
}
592
593
private function buildSubheaderView(PhabricatorCalendarEvent $event) {
594
$viewer = $this->getViewer();
595
596
$host_phid = $event->getHostPHID();
597
598
$handles = $viewer->loadHandles(array($host_phid));
599
$handle = $handles[$host_phid];
600
601
$host = $viewer->renderHandle($host_phid);
602
$host = phutil_tag('strong', array(), $host);
603
604
$image_uri = $handles[$host_phid]->getImageURI();
605
$image_href = $handles[$host_phid]->getURI();
606
607
$date = $event->renderEventDate($viewer, true);
608
609
$content = pht('Hosted by %s on %s.', $host, $date);
610
611
return id(new PHUIHeadThingView())
612
->setImage($image_uri)
613
->setImageHref($image_href)
614
->setContent($content);
615
}
616
617
618
private function buildRSVPActions(PhabricatorCalendarEvent $event) {
619
$viewer = $this->getViewer();
620
$id = $event->getID();
621
622
$is_pending = $event->isRSVPInvited($viewer->getPHID());
623
if (!$is_pending) {
624
return array();
625
}
626
627
$decline_button = id(new PHUIButtonView())
628
->setTag('a')
629
->setIcon('fa-times grey')
630
->setHref($this->getApplicationURI("/event/decline/{$id}/"))
631
->setWorkflow(true)
632
->setText(pht('Decline'));
633
634
$accept_button = id(new PHUIButtonView())
635
->setTag('a')
636
->setIcon('fa-check green')
637
->setHref($this->getApplicationURI("/event/accept/{$id}/"))
638
->setWorkflow(true)
639
->setText(pht('Accept'));
640
641
return array($decline_button, $accept_button);
642
}
643
644
}
645
646