We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

EventPlugin.php 14.4 KB
Newer Older
Evan Prodromou's avatar
Evan Prodromou committed
1 2 3
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2011, StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
5
 *
6
 * Microapp plugin for event invitations and RSVPs
Evan Prodromou's avatar
Evan Prodromou committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
23
 * @category  Event
Evan Prodromou's avatar
Evan Prodromou committed
24 25
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
26
 * @copyright 2011 StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
27 28 29 30 31 32 33 34 35 36 37
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    // This check helps protect against security problems;
    // your code file can't be executed directly from the web.
    exit(1);
}

/**
38
 * Event plugin
Evan Prodromou's avatar
Evan Prodromou committed
39 40 41 42
 *
 * @category  Sample
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
43
 * @copyright 2011 StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
44 45 46
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
47
class EventPlugin extends MicroappPlugin
Evan Prodromou's avatar
Evan Prodromou committed
48 49
{
    /**
50
     * Set up our tables (event and rsvp)
Evan Prodromou's avatar
Evan Prodromou committed
51 52 53 54 55 56 57 58 59 60
     *
     * @see Schema
     * @see ColumnDef
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onCheckSchema()
    {
        $schema = Schema::get();

Evan Prodromou's avatar
Evan Prodromou committed
61
        $schema->ensureTable('happening', Happening::schemaDef());
62
        $schema->ensureTable('rsvp', RSVP::schemaDef());
Evan Prodromou's avatar
Evan Prodromou committed
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

        return true;
    }

    /**
     * Load related modules when needed
     *
     * @param string $cls Name of the class to be loaded
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function onAutoload($cls)
    {
        $dir = dirname(__FILE__);

        switch ($cls)
        {
80 81
        case 'NeweventAction':
        case 'NewrsvpAction':
Evan Prodromou's avatar
Evan Prodromou committed
82
        case 'CancelrsvpAction':
83 84
        case 'ShoweventAction':
        case 'ShowrsvpAction':
Evan Prodromou's avatar
Evan Prodromou committed
85 86
            include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
            return false;
87
        case 'EventForm':
Evan Prodromou's avatar
Evan Prodromou committed
88 89
        case 'RSVPForm':
        case 'CancelRSVPForm':
90 91 92
            include_once $dir . '/'.strtolower($cls).'.php';
            break;
        case 'Happening':
93
        case 'RSVP':
Evan Prodromou's avatar
Evan Prodromou committed
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
            include_once $dir . '/'.$cls.'.php';
            return false;
        default:
            return true;
        }
    }

    /**
     * Map URLs to actions
     *
     * @param Net_URL_Mapper $m path-to-action mapper
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */

109
    function onRouterInitialized($m)
Evan Prodromou's avatar
Evan Prodromou committed
110
    {
111 112 113 114
        $m->connect('main/event/new',
                    array('action' => 'newevent'));
        $m->connect('main/event/rsvp',
                    array('action' => 'newrsvp'));
Evan Prodromou's avatar
Evan Prodromou committed
115 116
        $m->connect('main/event/rsvp/cancel',
                    array('action' => 'cancelrsvp'));
117 118 119 120 121 122
        $m->connect('event/:id',
                    array('action' => 'showevent'),
                    array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
        $m->connect('rsvp/:id',
                    array('action' => 'showrsvp'),
                    array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
Evan Prodromou's avatar
Evan Prodromou committed
123 124 125 126 127
        return true;
    }

    function onPluginVersion(&$versions)
    {
128
        $versions[] = array('name' => 'Event',
Evan Prodromou's avatar
Evan Prodromou committed
129
                            'version' => STATUSNET_VERSION,
130 131 132 133
                            'author' => 'Evan Prodromou',
                            'homepage' => 'http://status.net/wiki/Plugin:Event',
                            'description' =>
                            _m('Event invitations and RSVPs.'));
Evan Prodromou's avatar
Evan Prodromou committed
134 135
        return true;
    }
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248

    function appTitle() {
        return _m('Event');
    }

    function tag() {
        return 'event';
    }

    function types() {
        return array(Happening::OBJECT_TYPE,
                     RSVP::POSITIVE,
                     RSVP::NEGATIVE,
                     RSVP::POSSIBLE);
    }

    /**
     * Given a parsed ActivityStreams activity, save it into a notice
     * and other data structures.
     *
     * @param Activity $activity
     * @param Profile $actor
     * @param array $options=array()
     *
     * @return Notice the resulting notice
     */
    function saveNoticeFromActivity($activity, $actor, $options=array())
    {
        if (count($activity->objects) != 1) {
            throw new Exception('Too many activity objects.');
        }

        $happeningObj = $activity->objects[0];

        if ($happeningObj->type != Happening::OBJECT_TYPE) {
            throw new Exception('Wrong type for object.');
        }

        $notice = null;

        switch ($activity->verb) {
        case ActivityVerb::POST:
            $notice = Happening::saveNew($actor, 
                                     $start_time, 
                                     $end_time,
                                     $happeningObj->title,
                                     null,
                                     $happeningObj->summary,
                                     $options);
            break;
        case RSVP::POSITIVE:
        case RSVP::NEGATIVE:
        case RSVP::POSSIBLE:
            $happening = Happening::staticGet('uri', $happeningObj->id);
            if (empty($happening)) {
                // FIXME: save the event
                throw new Exception("RSVP for unknown event.");
            }
            $notice = RSVP::saveNew($actor, $happening, $activity->verb, $options);
            break;
        default:
            throw new Exception("Unknown verb for events");
        }

        return $notice;
    }

    /**
     * Turn a Notice into an activity object
     *
     * @param Notice $notice
     *
     * @return ActivityObject
     */

    function activityObjectFromNotice($notice)
    {
        $happening = null;

        switch ($notice->object_type) {
        case Happening::OBJECT_TYPE:
            $happening = Happening::fromNotice($notice);
            break;
        case RSVP::POSITIVE:
        case RSVP::NEGATIVE:
        case RSVP::POSSIBLE:
            $rsvp  = RSVP::fromNotice($notice);
            $happening = $rsvp->getEvent();
            break;
        }

        if (empty($happening)) {
            throw new Exception("Unknown object type.");
        }

        $notice = $happening->getNotice();

        if (empty($notice)) {
            throw new Exception("Unknown event notice.");
        }

        $obj = new ActivityObject();

        $obj->id      = $happening->uri;
        $obj->type    = Happening::OBJECT_TYPE;
        $obj->title   = $happening->title;
        $obj->summary = $happening->description;
        $obj->link    = $notice->bestUrl();

        // XXX: how to get this stuff into JSON?!

        $obj->extra[] = array('dtstart',
                              array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
249
                              common_date_iso8601($happening->start_time));
250 251 252

        $obj->extra[] = array('dtend',
                              array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
253
                              common_date_iso8601($happening->end_time));
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

        // XXX: probably need other stuff here

        return $obj;
    }

    /**
     * Change the verb on RSVP notices
     *
     * @param Notice $notice
     *
     * @return ActivityObject
     */

    function onEndNoticeAsActivity($notice, &$act) {
        switch ($notice->object_type) {
        case RSVP::POSITIVE:
        case RSVP::NEGATIVE:
        case RSVP::POSSIBLE:
            $act->verb = $notice->object_type;
            break;
        }
        return true;
    }

    /**
     * Custom HTML output for our notices
     *
     * @param Notice $notice
     * @param HTMLOutputter $out
     */

    function showNotice($notice, $out)
    {
        switch ($notice->object_type) {
        case Happening::OBJECT_TYPE:
            $this->showEventNotice($notice, $out);
            break;
        case RSVP::POSITIVE:
        case RSVP::NEGATIVE:
        case RSVP::POSSIBLE:
            $this->showRSVPNotice($notice, $out);
            break;
        }
Brion Vibber's avatar
Brion Vibber committed
298 299

        // @fixme we have to start the name/avatar and open this div
300
        $out->elementStart('div', array('class' => 'event-info entry-content')); // EVENT-INFO.ENTRY-CONTENT IN
Brion Vibber's avatar
Brion Vibber committed
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322

        $profile = $notice->getProfile();
        $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);

        $out->element('img',
                      array('src' => ($avatar) ?
                            $avatar->displayUrl() :
                            Avatar::defaultImage(AVATAR_MINI_SIZE),
                            'class' => 'avatar photo bookmark-avatar',
                            'width' => AVATAR_MINI_SIZE,
                            'height' => AVATAR_MINI_SIZE,
                            'alt' => $profile->getBestName()));

        $out->raw('&#160;'); // avoid &nbsp; for AJAX XML compatibility

        $out->elementStart('span', 'vcard author'); // hack for belongsOnTimeline; JS needs to be able to find the author
        $out->element('a',
                      array('class' => 'url',
                            'href' => $profile->profileurl,
                            'title' => $profile->getBestName()),
                      $profile->nickname);
        $out->elementEnd('span');
323 324 325 326
    }

    function showRSVPNotice($notice, $out)
    {
Evan Prodromou's avatar
Evan Prodromou committed
327 328 329 330 331
        $rsvp = RSVP::fromNotice($notice);

        $out->elementStart('div', 'rsvp');
        $out->raw($rsvp->asHTML());
        $out->elementEnd('div');
332 333 334 335 336
        return;
    }

    function showEventNotice($notice, $out)
    {
Evan Prodromou's avatar
Evan Prodromou committed
337 338 339 340 341 342
        $profile = $notice->getProfile();
        $event   = Happening::fromNotice($notice);

        assert(!empty($event));
        assert(!empty($profile));

343
        $out->elementStart('div', 'vevent event'); // VEVENT IN
Evan Prodromou's avatar
Evan Prodromou committed
344

Brion Vibber's avatar
Brion Vibber committed
345
        $out->elementStart('h3');  // VEVENT/H3 IN
Evan Prodromou's avatar
Evan Prodromou committed
346 347 348

        if (!empty($event->url)) {
            $out->element('a',
Evan Prodromou's avatar
Evan Prodromou committed
349
                          array('href' => $event->url,
Evan Prodromou's avatar
Evan Prodromou committed
350 351 352 353 354 355
                                'class' => 'event-title entry-title summary'),
                          $event->title);
        } else {
            $out->text($event->title);
        }

Brion Vibber's avatar
Brion Vibber committed
356
        $out->elementEnd('h3'); // VEVENT/H3 OUT
Evan Prodromou's avatar
Evan Prodromou committed
357

358 359
        $startDate = strftime("%x", strtotime($event->start_time));
        $startTime = strftime("%R", strtotime($event->start_time));
360

361 362
        $endDate = strftime("%x", strtotime($event->end_time));
        $endTime = strftime("%R", strtotime($event->end_time));
363

Evan Prodromou's avatar
Evan Prodromou committed
364 365
        // FIXME: better dates

Brion Vibber's avatar
Brion Vibber committed
366
        $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN
367 368 369

        $out->element('strong', null, _('Time:'));

Evan Prodromou's avatar
Evan Prodromou committed
370
        $out->element('abbr', array('class' => 'dtstart',
371
                                    'title' => common_date_iso8601($event->start_time)),
372
                      $startDate . ' ' . $startTime);
Evan Prodromou's avatar
Evan Prodromou committed
373
        $out->text(' - ');
374 375 376 377 378 379 380 381 382 383
        if ($startDate == $endDate) {
            $out->element('span', array('class' => 'dtend',
                                        'title' => common_date_iso8601($event->end_time)),
                          $endTime);
        } else {
            $out->element('span', array('class' => 'dtend',
                                        'title' => common_date_iso8601($event->end_time)),
                          $endDate . ' ' . $endTime);
        }

Brion Vibber's avatar
Brion Vibber committed
384
        $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT
Evan Prodromou's avatar
Evan Prodromou committed
385

386 387 388 389 390
        if (!empty($event->location)) {
            $out->elementStart('div', 'event-location');
            $out->element('strong', null, _('Location: '));
            $out->element('span', 'location', $event->location);
            $out->elementEnd('div');
Evan Prodromou's avatar
Evan Prodromou committed
391 392
        }

393 394 395 396 397
        if (!empty($event->description)) {
            $out->elementStart('div', 'event-description');
            $out->element('strong', null, _('Description: '));
            $out->element('span', 'description', $event->description);
            $out->elementEnd('div');
Evan Prodromou's avatar
Evan Prodromou committed
398 399
        }

Evan Prodromou's avatar
Evan Prodromou committed
400 401
        $rsvps = $event->getRSVPs();

402 403 404
        $out->elementStart('div', 'event-rsvps');
        $out->element('strong', null, _('Attending: '));
        $out->element('span', 'event-rsvps',
Evan Prodromou's avatar
Evan Prodromou committed
405 406 407 408
                      sprintf(_('Yes: %d No: %d Maybe: %d'),
                              count($rsvps[RSVP::POSITIVE]),
                              count($rsvps[RSVP::NEGATIVE]),
                              count($rsvps[RSVP::POSSIBLE])));
409
        $out->elementEnd('div');
Evan Prodromou's avatar
Evan Prodromou committed
410

Evan Prodromou's avatar
Evan Prodromou committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424
        $user = common_current_user();

        if (!empty($user)) {
            $rsvp = $event->getRSVP($user->getProfile());

            if (empty($rsvp)) {
                $form = new RSVPForm($event, $out);
            } else {
                $form = new CancelRSVPForm($rsvp, $out);
            }

            $form->show();
        }

Brion Vibber's avatar
Brion Vibber committed
425
        $out->elementEnd('div'); // vevent out
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
    }

    /**
     * Form for our app
     *
     * @param HTMLOutputter $out
     * @return Widget
     */

    function entryForm($out)
    {
        return new EventForm($out);
    }

    /**
     * When a notice is deleted, clean up related tables.
     *
     * @param Notice $notice
     */

    function deleteRelated($notice)
    {
        switch ($notice->object_type) {
        case Happening::OBJECT_TYPE:
Brion Vibber's avatar
Brion Vibber committed
450
            common_log(LOG_DEBUG, "Deleting event from notice...");
451 452 453 454 455 456
            $happening = Happening::fromNotice($notice);
            $happening->delete();
            break;
        case RSVP::POSITIVE:
        case RSVP::NEGATIVE:
        case RSVP::POSSIBLE:
Brion Vibber's avatar
Brion Vibber committed
457
            common_log(LOG_DEBUG, "Deleting rsvp from notice...");
458
            $rsvp = RSVP::fromNotice($notice);
Brion Vibber's avatar
Brion Vibber committed
459
            common_log(LOG_DEBUG, "to delete: $rsvp->id");
460 461
            $rsvp->delete();
            break;
Brion Vibber's avatar
Brion Vibber committed
462 463
        default:
            common_log(LOG_DEBUG, "Not deleting related, wtf...");
464 465
        }
    }
Evan Prodromou's avatar
Evan Prodromou committed
466 467 468 469 470

    function onEndShowScripts($action)
    {
        $action->inlineScript('$(document).ready(function() { $("#startdate").datepicker(); $("#enddate").datepicker(); });');
    }
471 472 473 474 475 476

    function onEndShowStyles($action)
    {
        $action->cssLink($this->path('event.css'));
        return true;
    }
Evan Prodromou's avatar
Evan Prodromou committed
477
}