]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Event/EventPlugin.php
14dadbc247faf08fbfb07a7e4934fad45461f7a0
[quix0rs-gnu-social.git] / plugins / Event / EventPlugin.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2011, StatusNet, Inc.
5  *
6  * Microapp plugin for event invitations and RSVPs
7  *
8  * PHP version 5
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU Affero General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU Affero General Public License for more details.
19  *
20  * You should have received a copy of the GNU Affero General Public License
21  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22  *
23  * @category  Event
24  * @package   StatusNet
25  * @author    Evan Prodromou <evan@status.net>
26  * @copyright 2011 StatusNet, Inc.
27  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28  * @link      http://status.net/
29  */
30
31 if (!defined('GNUSOCIAL')) { exit(1); }
32
33 /**
34  * Event plugin
35  *
36  * @category  Event
37  * @package   StatusNet
38  * @author    Evan Prodromou <evan@status.net>
39  * @copyright 2011 StatusNet, Inc.
40  * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
41  * @link      http://status.net/
42  */
43 class EventPlugin extends MicroAppPlugin
44 {
45
46     var $oldSaveNew = true;
47
48     /**
49      * Set up our tables (event and rsvp)
50      *
51      * @see Schema
52      * @see ColumnDef
53      *
54      * @return boolean hook value; true means continue processing, false means stop.
55      */
56     function onCheckSchema()
57     {
58         $schema = Schema::get();
59
60         $schema->ensureTable('happening', Happening::schemaDef());
61         $schema->ensureTable('rsvp', RSVP::schemaDef());
62
63         return true;
64     }
65
66     public function onBeforePluginCheckSchema()
67     {
68         RSVP::beforeSchemaUpdate();
69         return true;
70     }
71
72     /**
73      * Map URLs to actions
74      *
75      * @param URLMapper $m path-to-action mapper
76      *
77      * @return boolean hook value; true means continue processing, false means stop.
78      */
79     public function onRouterInitialized(URLMapper $m)
80     {
81         $m->connect('main/event/new',
82                     array('action' => 'newevent'));
83         $m->connect('main/event/rsvp',
84                     array('action' => 'newrsvp'));
85         $m->connect('main/event/rsvp/cancel',
86                     array('action' => 'cancelrsvp'));
87         $m->connect('event/:id',
88                     array('action' => 'showevent'),
89                     array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
90         $m->connect('rsvp/:id',
91                     array('action' => 'showrsvp'),
92                     array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
93         $m->connect('main/event/updatetimes',
94                     array('action' => 'timelist'));
95
96         $m->connect(':nickname/events',
97                     array('action' => 'events'),
98                     array('nickname' => Nickname::DISPLAY_FMT));
99         return true;
100     }
101
102     function onPluginVersion(array &$versions)
103     {
104         $versions[] = array('name' => 'Event',
105                             'version' => GNUSOCIAL_VERSION,
106                             'author' => 'Evan Prodromou',
107                             'homepage' => 'http://status.net/wiki/Plugin:Event',
108                             'description' =>
109                             // TRANS: Plugin description.
110                             _m('Event invitations and RSVPs.'));
111         return true;
112     }
113
114     function appTitle() {
115         // TRANS: Title for event application.
116         return _m('TITLE','Event');
117     }
118
119     function tag() {
120         return 'event';
121     }
122
123     function types() {
124         return array(Happening::OBJECT_TYPE);
125     }
126
127     function verbs() {
128         return array(ActivityVerb::POST,
129                      RSVP::POSITIVE,
130                      RSVP::NEGATIVE,
131                      RSVP::POSSIBLE);
132     }
133
134     protected function saveObjectFromActivity(Activity $activity, Notice $stored, array $options=array())
135     {
136         if (count($act->objects) !== 1) {
137             // TRANS: Exception thrown when there are too many activity objects.
138             throw new Exception(_m('Too many activity objects.'));
139         }
140         $actobj = $activity->objects[0];
141
142         switch ($activity->verb) {
143         case ActivityVerb::POST:
144             $actobj = $activity->objects[0];
145             if (!ActivityUtils::compareTypes($actobj->type, array(Happening::OBJECT_TYPE))) {
146                 // TRANS: Exception thrown when event plugin comes across a non-event type object.
147                 throw new Exception(_m('Wrong type for object.'));
148             }
149             return Happening::saveActivityObject($actobj, $stored->getProfile());
150             break;
151         case RSVP::POSITIVE:
152         case RSVP::NEGATIVE:
153         case RSVP::POSSIBLE:
154             $happening = Happening::getKV('uri', $actobj->id);
155             if (empty($happening)) {
156                 // FIXME: save the event
157                 // TRANS: Exception thrown when trying to RSVP for an unknown event.
158                 throw new Exception(_m('RSVP for unknown event.'));
159             }
160             $object = RSVP::saveNewFromNotice($stored, $happening, $activity->verb);
161             // Our data model expects this
162             $stored->object_type = $activity->verb;
163             return $object;
164             break;
165         default:
166             common_log(LOG_ERR, 'Unknown verb for events.');
167             return NULL;
168         }
169     }
170
171     /**
172      * Turn a Notice into an activity object
173      *
174      * @param Notice $notice
175      *
176      * @return ActivityObject
177      */
178     function activityObjectFromNotice(Notice $notice)
179     {
180         $happening = null;
181
182         switch ($notice->object_type) {
183         case Happening::OBJECT_TYPE:
184             $happening = Happening::fromNotice($notice);
185             break;
186         case RSVP::POSITIVE:
187         case RSVP::NEGATIVE:
188         case RSVP::POSSIBLE:
189             $rsvp  = RSVP::fromNotice($notice);
190             $happening = $rsvp->getEvent();
191             break;
192         }
193
194         if (empty($happening)) {
195             // TRANS: Exception thrown when event plugin comes across a unknown object type.
196             throw new Exception(_m('Unknown object type.'));
197         }
198
199         $notice = $happening->getNotice();
200
201         if (empty($notice)) {
202             // TRANS: Exception thrown when referring to a notice that is not an event an in event context.
203             throw new Exception(_m('Unknown event notice.'));
204         }
205
206         $obj = new ActivityObject();
207
208         $obj->id      = $happening->uri;
209         $obj->type    = Happening::OBJECT_TYPE;
210         $obj->title   = $happening->title;
211         $obj->summary = $happening->description;
212         $obj->link    = $notice->getUrl();
213
214         // XXX: how to get this stuff into JSON?!
215
216         $obj->extra[] = array('dtstart',
217                               array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
218                               common_date_iso8601($happening->start_time));
219
220         $obj->extra[] = array('dtend',
221                               array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
222                               common_date_iso8601($happening->end_time));
223
224         $obj->extra[] = array('location', false, $happening->location);
225         $obj->extra[] = array('url', false, $happening->url);
226
227         // XXX: probably need other stuff here
228
229         return $obj;
230     }
231
232     /**
233      * Change the verb on RSVP notices
234      *
235      * @param Notice $notice
236      *
237      * @return ActivityObject
238      */
239     protected function extendActivity(Notice $stored, Activity $act, Profile $scoped=null) {
240         switch ($stored->object_type) {
241         case RSVP::POSITIVE:
242         case RSVP::NEGATIVE:
243         case RSVP::POSSIBLE:
244             $act->verb = $stored->object_type;
245             break;
246         }
247         return true;
248     }
249
250     /**
251      * Form for our app
252      *
253      * @param HTMLOutputter $out
254      * @return Widget
255      */
256     function entryForm($out)
257     {
258         return new EventForm($out);
259     }
260
261     /**
262      * When a notice is deleted, clean up related tables.
263      *
264      * @param Notice $notice
265      */
266     function deleteRelated(Notice $notice)
267     {
268         switch ($notice->object_type) {
269         case Happening::OBJECT_TYPE:
270             common_log(LOG_DEBUG, "Deleting event from notice...");
271             $happening = Happening::fromNotice($notice);
272             $happening->delete();
273             break;
274         case RSVP::POSITIVE:
275         case RSVP::NEGATIVE:
276         case RSVP::POSSIBLE:
277             common_log(LOG_DEBUG, "Deleting rsvp from notice...");
278             $rsvp = RSVP::fromNotice($notice);
279             common_log(LOG_DEBUG, "to delete: $rsvp->id");
280             $rsvp->delete();
281             break;
282         default:
283             common_log(LOG_DEBUG, "Not deleting related, wtf...");
284         }
285     }
286
287     function onEndShowScripts($action)
288     {
289         $action->script($this->path('js/event.js'));
290     }
291
292     function onEndShowStyles($action)
293     {
294         $action->cssLink($this->path('css/event.css'));
295         return true;
296     }
297
298     function onStartAddNoticeReply($nli, $parent, $child)
299     {
300         // Filter out any poll responses
301         if (($parent->object_type == Happening::OBJECT_TYPE) &&
302             in_array($child->object_type, array(RSVP::POSITIVE, RSVP::NEGATIVE, RSVP::POSSIBLE))) {
303             return false;
304         }
305         return true;
306     }
307
308     protected function showNoticeItemNotice(NoticeListItem $nli)
309     {
310         $nli->showAuthor();
311         $nli->showContent();
312     }
313
314     protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
315     {
316         switch ($stored->object_type) {
317         case Happening::OBJECT_TYPE:
318             $this->showEvent($stored, $out, $scoped);
319             break;
320         case RSVP::POSITIVE:
321         case RSVP::NEGATIVE:
322         case RSVP::POSSIBLE:
323             $this->showRSVP($stored, $out, $scoped);
324             break;
325         }
326     }
327
328     protected function showEvent(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
329     {
330         $profile = $stored->getProfile();
331         $event   = Happening::fromNotice($stored);
332
333         if (!$event instanceof Happening) {
334             // TRANS: Content for a deleted RSVP list item (RSVP stands for "please respond").
335             $out->element('p', null, _m('Deleted.'));
336             return;
337         }
338
339         $out->elementStart('div', 'h-event');
340
341         $out->elementStart('h3', 'p-summary p-name');
342
343         try {
344             $out->element('a', array('href' => $event->getUrl()), $event->title);
345         } catch (InvalidUrlException $e) {
346             $out->text($event->title);
347         }
348
349         $out->elementEnd('h3');
350
351         $now       = new DateTime();
352         $startDate = new DateTime($event->start_time);
353         $endDate   = new DateTime($event->end_time);
354         $userTz    = new DateTimeZone(common_timezone());
355
356         // Localize the time for the observer
357         $now->setTimeZone($userTz);
358         $startDate->setTimezone($userTz);
359         $endDate->setTimezone($userTz);
360
361         $thisYear  = $now->format('Y');
362         $startYear = $startDate->format('Y');
363         $endYear   = $endDate->format('Y');
364
365         $dateFmt = 'D, F j, '; // e.g.: Mon, Aug 31
366
367         if ($startYear != $thisYear || $endYear != $thisYear) {
368             $dateFmt .= 'Y,'; // append year if we need to think about years
369         }
370
371         $startDateStr = $startDate->format($dateFmt);
372         $endDateStr = $endDate->format($dateFmt);
373
374         $timeFmt = 'g:ia';
375
376         $startTimeStr = $startDate->format($timeFmt);
377         $endTimeStr = $endDate->format("{$timeFmt} (T)");
378
379         $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN
380
381         // TRANS: Field label for event description.
382         $out->element('strong', null, _m('Time:'));
383
384         $out->element('time', array('class' => 'dt-start',
385                                     'datetime' => common_date_iso8601($event->start_time)),
386                       $startDateStr . ' ' . $startTimeStr);
387         $out->text(' – ');
388         $out->element('time', array('class' => 'dt-end',
389                                     'datetime' => common_date_iso8601($event->end_time)),
390                       $startDateStr != $endDateStr
391                                     ? "$endDateStr $endTimeStr"
392                                     :  $endTimeStr);
393
394         $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT
395
396         if (!empty($event->location)) {
397             $out->elementStart('div', 'event-location');
398             // TRANS: Field label for event description.
399             $out->element('strong', null, _m('Location:'));
400             $out->element('span', 'p-location', $event->location);
401             $out->elementEnd('div');
402         }
403
404         if (!empty($event->description)) {
405             $out->elementStart('div', 'event-description');
406             // TRANS: Field label for event description.
407             $out->element('strong', null, _m('Description:'));
408             $out->element('div', 'p-description', $event->description);
409             $out->elementEnd('div');
410         }
411
412         $rsvps = $event->getRSVPs();
413
414         $out->elementStart('div', 'event-rsvps');
415
416         // TRANS: Field label for event description.
417         $out->element('strong', null, _m('Attending:'));
418         $out->elementStart('ul', 'attending-list');
419
420         foreach ($rsvps as $verb => $responses) {
421             $out->elementStart('li', 'rsvp-list');
422             switch ($verb) {
423             case RSVP::POSITIVE:
424                 $out->text(_('Yes:'));
425                 break;
426             case RSVP::NEGATIVE:
427                 $out->text(_('No:'));
428                 break;
429             case RSVP::POSSIBLE:
430                 $out->text(_('Maybe:'));
431                 break;
432             }
433             $ids = array();
434             foreach ($responses as $response) {
435                 $ids[] = $response->profile_id;
436             }
437             $ids = array_slice($ids, 0, ProfileMiniList::MAX_PROFILES + 1);
438             $minilist = new ProfileMiniList(Profile::multiGet('id', $ids), $out);
439             $minilist->show();
440
441             $out->elementEnd('li');
442         }
443
444         $out->elementEnd('ul');
445         $out->elementEnd('div');
446
447         if ($scoped instanceof Profile) {
448             $rsvp = $event->getRSVP($scoped);
449
450             if (empty($rsvp)) {
451                 $form = new RSVPForm($event, $out);
452             } else {
453                 $form = new CancelRSVPForm($rsvp, $out);
454             }
455
456             $form->show();
457         }
458         $out->elementEnd('div');
459     }
460
461     protected function showRSVP(Notice $stored, HTMLOutputter $out, Profile $scoped=null)
462     {
463         $rsvp = RSVP::fromNotice($stored);
464
465         if (empty($rsvp)) {
466             // TRANS: Content for a deleted RSVP list item (RSVP stands for "please respond").
467             $out->element('p', null, _m('Deleted.'));
468             return;
469         }
470
471         $out->elementStart('div', 'rsvp');
472         $out->raw($rsvp->asHTML());
473         $out->elementEnd('div');
474     }
475
476     function onEndPersonalGroupNav(Menu $menu, Profile $target, Profile $scoped=null)
477     {
478         $menu->menuItem(common_local_url('events', array('nickname' => $target->getNickname())),
479                           // TRANS: Menu item in sample plugin.
480                           _m('Happenings'),
481                           // TRANS: Menu item title in sample plugin.
482                           _m('A list of your events'), false, 'nav_timeline_events');
483         return true;
484     }
485 }