diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php new file mode 100644 index 0000000000..7ca2fa9c0e --- /dev/null +++ b/plugins/Event/EventPlugin.php @@ -0,0 +1,430 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * Event plugin + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class EventPlugin extends MicroappPlugin +{ + /** + * Set up our tables (event and rsvp) + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + + $schema->ensureTable('happening', Happening::schemaDef()); + $schema->ensureTable('rsvp', RSVP::schemaDef()); + + 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) + { + case 'NeweventAction': + case 'NewrsvpAction': + case 'CancelrsvpAction': + case 'ShoweventAction': + case 'ShowrsvpAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'EventForm': + case 'RSVPForm': + case 'CancelRSVPForm': + include_once $dir . '/'.strtolower($cls).'.php'; + break; + case 'Happening': + case 'RSVP': + 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. + */ + + function onRouterInitialized($m) + { + $m->connect('main/event/new', + array('action' => 'newevent')); + $m->connect('main/event/rsvp', + array('action' => 'newrsvp')); + $m->connect('main/event/rsvp/cancel', + array('action' => 'cancelrsvp')); + $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}')); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Event', + 'version' => STATUSNET_VERSION, + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Event', + 'description' => + _m('Event invitations and RSVPs.')); + return true; + } + + 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'), + common_date_iso8601($happening->start_date)); + + $obj->extra[] = array('dtend', + array('xmlns' => 'urn:ietf:params:xml:ns:xcal'), + common_date_iso8601($happening->end_date)); + + // 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; + } + } + + function showRSVPNotice($notice, $out) + { + $out->raw($notice->rendered); + return; + } + + function showEventNotice($notice, $out) + { + $profile = $notice->getProfile(); + $event = Happening::fromNotice($notice); + + assert(!empty($event)); + assert(!empty($profile)); + + $out->elementStart('div', 'vevent'); + + $out->elementStart('h3'); + + if (!empty($event->url)) { + $out->element('a', + array('href' => $event->url, + 'class' => 'event-title entry-title summary'), + $event->title); + } else { + $out->text($event->title); + } + + $out->elementEnd('h3'); + + // FIXME: better dates + + $out->elementStart('div', 'event-times'); + $out->element('abbr', array('class' => 'dtstart', + 'title' => common_date_iso8601($event->start_time)), + common_exact_date($event->start_time)); + $out->text(' - '); + $out->element('span', array('class' => 'dtend', + 'title' => common_date_iso8601($event->end_time)), + common_exact_date($event->end_time)); + $out->elementEnd('div'); + + if (!empty($event->description)) { + $out->element('div', 'description', $event->description); + } + + if (!empty($event->location)) { + $out->element('div', 'location', $event->location); + } + + $rsvps = $event->getRSVPs(); + + $out->element('div', 'event-rsvps', + sprintf(_('Yes: %d No: %d Maybe: %d'), + count($rsvps[RSVP::POSITIVE]), + count($rsvps[RSVP::NEGATIVE]), + count($rsvps[RSVP::POSSIBLE]))); + + $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(); + } + + $out->elementStart('div', array('class' => 'event-info entry-content')); + + $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(' '); // avoid   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'); + + $out->elementEnd('div'); + } + + /** + * 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: + $happening = Happening::fromNotice($notice); + $happening->delete(); + break; + case RSVP::POSITIVE: + case RSVP::NEGATIVE: + case RSVP::POSSIBLE: + $rsvp = RSVP::fromNotice($notice); + $rsvp->delete(); + break; + } + } +} diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php new file mode 100644 index 0000000000..1a6a028dca --- /dev/null +++ b/plugins/Event/Happening.php @@ -0,0 +1,219 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Data class for happenings + * + * There's already an Event class in lib/event.php, so we couldn't + * call this an Event without causing a hole in space-time. + * + * "Happening" seemed good enough. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see Managed_DataObject + */ + +class Happening extends Managed_DataObject +{ + const OBJECT_TYPE = 'http://activitystrea.ms/schema/1.0/event'; + + public $__table = 'happening'; // table name + public $id; // varchar(36) UUID + public $uri; // varchar(255) + public $profile_id; // int + public $start_time; // datetime + public $end_time; // datetime + public $title; // varchar(255) + public $location; // varchar(255) + public $url; // varchar(255) + public $description; // text + public $created; // datetime + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'id' for this class) + * @param mixed $v Value to lookup + * + * @return Happening object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Happening', $k, $v); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'A real-world happening', + 'fields' => array( + 'id' => array('type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID'), + 'uri' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'profile_id' => array('type' => 'int', 'not null' => true), + 'start_time' => array('type' => 'datetime', 'not null' => true), + 'end_time' => array('type' => 'datetime', 'not null' => true), + 'title' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'location' => array('type' => 'varchar', + 'length' => 255), + 'url' => array('type' => 'varchar', + 'length' => 255), + 'description' => array('type' => 'text'), + 'created' => array('type' => 'datetime', + 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'happening_uri_key' => array('uri'), + ), + 'foreign keys' => array('happening_profile_id__key' => array('profile', array('profile_id' => 'id'))), + 'indexes' => array('happening_created_idx' => array('created'), + 'happening_start_end_idx' => array('start_time', 'end_time')), + ); + } + + function saveNew($profile, $start_time, $end_time, $title, $location, $description, $url, $options=array()) + { + if (array_key_exists('uri', $options)) { + $other = Happening::staticGet('uri', $options['uri']); + if (!empty($other)) { + throw new ClientException(_('Event already exists.')); + } + } + + $ev = new Happening(); + + $ev->id = UUID::gen(); + $ev->profile_id = $profile->id; + $ev->start_time = common_sql_date($start_time); + $ev->end_time = common_sql_date($end_time); + $ev->title = $title; + $ev->location = $location; + $ev->description = $description; + $ev->url = $url; + + if (array_key_exists('created', $options)) { + $ev->created = $options['created']; + } else { + $ev->created = common_sql_now(); + } + + if (array_key_exists('uri', $options)) { + $ev->uri = $options['uri']; + } else { + $ev->uri = common_local_url('showevent', + array('id' => $ev->id)); + } + + $ev->insert(); + + // XXX: does this get truncated? + + $content = sprintf(_('"%s" %s - %s (%s): %s'), + $title, + common_exact_date($start_time), + common_exact_date($end_time), + $location, + $description); + + $rendered = sprintf(_(''. + '%s '. + '%s - '. + '%s '. + '(%s): '. + '%s '. + ''), + htmlspecialchars($title), + htmlspecialchars(common_date_iso8601($start_time)), + htmlspecialchars(common_exact_date($start_time)), + htmlspecialchars(common_date_iso8601($end_time)), + htmlspecialchars(common_exact_date($end_time)), + htmlspecialchars($location), + htmlspecialchars($description)); + + $options = array_merge(array('object_type' => Happening::OBJECT_TYPE), + $options); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $ev->uri; + } + + if (!empty($url)) { + $options['urls'] = array($url); + } + + $saved = Notice::saveNew($profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options); + + return $saved; + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + static function fromNotice($notice) + { + return Happening::staticGet('uri', $notice->uri); + } + + function getRSVPs() + { + return RSVP::forEvent($this); + } + + function getRSVP($profile) + { + return RSVP::pkeyGet(array('profile_id' => $profile->id, + 'event_id' => $this->id)); + } +} diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php new file mode 100644 index 0000000000..22bd239a68 --- /dev/null +++ b/plugins/Event/RSVP.php @@ -0,0 +1,239 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Data class for event RSVPs + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see Managed_DataObject + */ + +class RSVP extends Managed_DataObject +{ + const POSITIVE = 'http://activitystrea.ms/schema/1.0/rsvp-yes'; + const POSSIBLE = 'http://activitystrea.ms/schema/1.0/rsvp-maybe'; + const NEGATIVE = 'http://activitystrea.ms/schema/1.0/rsvp-no'; + + public $__table = 'rsvp'; // table name + public $id; // varchar(36) UUID + public $uri; // varchar(255) + public $profile_id; // int + public $event_id; // varchar(36) UUID + public $result; // tinyint + public $created; // datetime + + /** + * Get an instance by key + * + * @param string $k Key to use to lookup (usually 'id' for this class) + * @param mixed $v Value to lookup + * + * @return RSVP object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('RSVP', $k, $v); + } + + /** + * Get an instance by compound key + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits + * + */ + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('RSVP', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'Plan to attend event', + 'fields' => array( + 'id' => array('type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID'), + 'uri' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'profile_id' => array('type' => 'int'), + 'event_id' => array('type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID'), + 'result' => array('type' => 'tinyint', + 'description' => '1, 0, or null for three-state yes, no, maybe'), + 'created' => array('type' => 'datetime', + 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'rsvp_uri_key' => array('uri'), + 'rsvp_profile_event_key' => array('profile_id', 'event_id'), + ), + 'foreign keys' => array('rsvp_event_id_key' => array('event', array('event_id' => 'id')), + 'rsvp_profile_id__key' => array('profile', array('profile_id' => 'id'))), + 'indexes' => array('rsvp_created_idx' => array('created')), + ); + } + + function saveNew($profile, $event, $result, $options=array()) + { + if (array_key_exists('uri', $options)) { + $other = RSVP::staticGet('uri', $options['uri']); + if (!empty($other)) { + throw new ClientException(_('RSVP already exists.')); + } + } + + $other = RSVP::pkeyGet(array('profile_id' => $profile->id, + 'event_id' => $event->id)); + + if (!empty($other)) { + throw new ClientException(_('RSVP already exists.')); + } + + $rsvp = new RSVP(); + + $rsvp->id = UUID::gen(); + $rsvp->profile_id = $profile->id; + $rsvp->event_id = $event->id; + $rsvp->result = self::codeFor($result); + + if (array_key_exists('created', $options)) { + $rsvp->created = $options['created']; + } else { + $rsvp->created = common_sql_now(); + } + + if (array_key_exists('uri', $options)) { + $rsvp->uri = $options['uri']; + } else { + $rsvp->uri = common_local_url('showrsvp', + array('id' => $rsvp->id)); + } + + $rsvp->insert(); + + // XXX: come up with something sexier + + $content = sprintf(_('RSVPed %s for an event.'), + ($result == RSVP::POSITIVE) ? _('positively') : + ($result == RSVP::NEGATIVE) ? _('negatively') : _('possibly')); + + $rendered = $content; + + $options = array_merge(array('object_type' => $result), + $options); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $rsvp->uri; + } + + $eventNotice = $event->getNotice(); + + if (!empty($eventNotice)) { + $options['reply_to'] = $eventNotice->id; + } + + $saved = Notice::saveNew($profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options); + + return $saved; + } + + function codeFor($verb) + { + return ($verb == RSVP::POSITIVE) ? 1 : + ($verb == RSVP::NEGATIVE) ? 0 : null; + } + + static function verbFor($code) + { + return ($code == 1) ? RSVP::POSITIVE : + ($code == 0) ? RSVP::NEGATIVE : null; + } + + function getNotice() + { + $notice = Notice::staticGet('uri', $this->uri); + if (empty($notice)) { + throw new ServerException("RSVP {$this->id} does not correspond to a notice in the DB."); + } + return $notice; + } + + static function fromNotice($notice) + { + return RSVP::staticGet('uri', $notice->uri); + } + + static function forEvent($event) + { + $rsvps = array(RSVP::POSITIVE => array(), RSVP::NEGATIVE => array(), RSVP::POSSIBLE => array()); + + $rsvp = new RSVP(); + + $rsvp->event_id = $event->id; + + if ($rsvp->find()) { + while ($rsvp->fetch()) { + $verb = self::verbFor($rsvp->result); + $rsvps[$verb][] = clone($rsvp); + } + } + + return $rsvps; + } + + function delete() + { + } +} diff --git a/plugins/Event/cancelrsvp.php b/plugins/Event/cancelrsvp.php new file mode 100644 index 0000000000..21ed41a451 --- /dev/null +++ b/plugins/Event/cancelrsvp.php @@ -0,0 +1,202 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * RSVP for an event + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class CancelrsvpAction extends Action +{ + protected $user = null; + protected $rsvp = null; + protected $event = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + + function title() + { + return _('Cancel RSVP'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $rsvpId = $this->trimmed('rsvp'); + + if (empty($rsvpId)) { + throw new ClientException(_('No such rsvp.')); + } + + $this->rsvp = RSVP::staticGet('id', $rsvpId); + + if (empty($this->rsvp)) { + throw new ClientException(_('No such rsvp.')); + } + + $this->event = Happening::staticGet('id', $this->rsvp->event_id); + + if (empty($this->event)) { + throw new ClientException(_('No such event.')); + } + + $this->user = common_current_user(); + + if (empty($this->user)) { + throw new ClientException(_('You must be logged in to RSVP for an event.')); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->cancelRSVP(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new event + * + * @return void + */ + + function cancelRSVP() + { + try { + $notice = $this->rsvp->getNotice(); + // NB: this will delete the rsvp, too + if (!empty($notice)) { + $notice->delete(); + } else { + $this->rsvp->delete(); + } + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending a notice. + $this->element('title', null, _('Event saved')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->elementStart('body'); + $form = new RSVPForm($this->event, $this); + $form->show(); + $this->elementEnd('body'); + $this->elementEnd('body'); + $this->elementEnd('html'); + } + } + + /** + * Show the event form + * + * @return void + */ + + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new CancelRSVPForm($this->rsvp, $this); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } +} diff --git a/plugins/Event/cancelrsvpform.php b/plugins/Event/cancelrsvpform.php new file mode 100644 index 0000000000..8cccbdb661 --- /dev/null +++ b/plugins/Event/cancelrsvpform.php @@ -0,0 +1,128 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * A form to RSVP for an event + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class CancelRSVPForm extends Form +{ + protected $rsvp = null; + + function __construct($rsvp, $out=null) + { + parent::__construct($out); + $this->rsvp = $rsvp; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_event_rsvp'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('cancelrsvp'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'new_rsvp_data')); + + $this->out->hidden('rsvp', $this->rsvp->id); + + switch (RSVP::verbFor($this->rsvp->result)) { + case RSVP::POSITIVE: + $this->out->text(_('You will attend this event.')); + break; + case RSVP::NEGATIVE: + $this->out->text(_('You will not attend this event.')); + break; + case RSVP::POSSIBLE: + $this->out->text(_('You might attend this event.')); + break; + } + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('cancel', _m('BUTTON', 'Cancel')); + } +} diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php new file mode 100644 index 0000000000..e6bc1e7016 --- /dev/null +++ b/plugins/Event/eventform.php @@ -0,0 +1,164 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * Form for adding an event + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class EventForm extends Form +{ + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_new_event'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings ajax-notice'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('newevent'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'new_bookmark_data')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->out->input('title', + _('Title'), + null, + _('Title of the event')); + $this->unli(); + + $this->li(); + $this->out->input('startdate', + _('Start date'), + null, + _('Date the event starts')); + $this->unli(); + + $this->li(); + $this->out->input('starttime', + _('Start time'), + null, + _('Time the event starts')); + $this->unli(); + + $this->li(); + $this->out->input('enddate', + _('End date'), + null, + _('Date the event ends')); + $this->unli(); + + $this->li(); + $this->out->input('endtime', + _('End time'), + null, + _('Time the event ends')); + $this->unli(); + + $this->li(); + $this->out->input('location', + _('Location'), + null, + _('Event location')); + $this->unli(); + + $this->li(); + $this->out->input('url', + _('URL'), + null, + _('URL for more information')); + $this->unli(); + + $this->li(); + $this->out->input('description', + _('Description'), + null, + _('Description of the event')); + $this->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _m('BUTTON', 'Save')); + } +} diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php new file mode 100644 index 0000000000..0f5635487b --- /dev/null +++ b/plugins/Event/newevent.php @@ -0,0 +1,241 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * Add a new event + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class NeweventAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + protected $title = null; + protected $location = null; + protected $description = null; + protected $start_time = null; + protected $end_time = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + + function title() + { + return _('New event'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $this->user = common_current_user(); + + if (empty($this->user)) { + throw new ClientException(_("Must be logged in to post a event."), + 403); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + $this->title = $this->trimmed('title'); + $this->location = $this->trimmed('location'); + $this->url = $this->trimmed('url'); + $this->description = $this->trimmed('description'); + + $start_date = $this->trimmed('start_date'); + $start_time = $this->trimmed('start_time'); + $end_date = $this->trimmed('end_date'); + $end_time = $this->trimmed('end_time'); + + $this->start_time = strtotime($start_date . ' ' . $start_time); + $this->end_time = strtotime($end_date . ' ' . $end_time); + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->newEvent(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new event + * + * @return void + */ + + function newEvent() + { + try { + if (empty($this->title)) { + throw new ClientException(_('Event must have a title.')); + } + + if (empty($this->start_time)) { + throw new ClientException(_('Event must have a start time.')); + } + + if (empty($this->end_time)) { + throw new ClientException(_('Event must have an end time.')); + } + + $profile = $this->user->getProfile(); + + $saved = Happening::saveNew($profile, + $this->start_time, + $this->end_time, + $this->title, + $this->location, + $this->description, + $this->url); + + $event = Happening::fromNotice($saved); + + RSVP::saveNew($profile, $event, RSVP::POSITIVE); + + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending a notice. + $this->element('title', null, _('Event saved')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showNotice($saved); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($saved->bestUrl(), 303); + } + } + + /** + * Show the event form + * + * @return void + */ + + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new EventForm($this); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } + + + /** + * Output a notice + * + * Used to generate the notice code for Ajax results. + * + * @param Notice $notice Notice that was saved + * + * @return void + */ + function showNotice($notice) + { + $nli = new NoticeListItem($notice, $this); + $nli->show(); + } +} diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php new file mode 100644 index 0000000000..da613ec6c7 --- /dev/null +++ b/plugins/Event/newrsvp.php @@ -0,0 +1,202 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * RSVP for an event + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class NewrsvpAction extends Action +{ + protected $user = null; + protected $event = null; + protected $type = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + + function title() + { + return _('New RSVP'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $eventId = $this->trimmed('event'); + + if (empty($eventId)) { + throw new ClientException(_('No such event.')); + } + + $this->event = Happening::staticGet('id', $eventId); + + if (empty($this->event)) { + throw new ClientException(_('No such event.')); + } + + $this->user = common_current_user(); + + if (empty($this->user)) { + throw new ClientException(_('You must be logged in to RSVP for an event.')); + } + + if ($this->arg('yes')) { + $this->type = RSVP::POSITIVE; + } else if ($this->arg('no')) { + $this->type = RSVP::NEGATIVE; + } else { + $this->type = RSVP::POSSIBLE; + } + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->newRSVP(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new event + * + * @return void + */ + + function newRSVP() + { + try { + $saved = RSVP::saveNew($this->user->getProfile(), + $this->event, + $this->type); + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + $rsvp = RSVP::fromNotice($saved); + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending a notice. + $this->element('title', null, _('Event saved')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->elementStart('body'); + $cancel = new CancelRSVPForm($rsvp, $this); + $cancel->show(); + $this->elementEnd('body'); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($saved->bestUrl(), 303); + } + } + + /** + * Show the event form + * + * @return void + */ + + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new RSVPForm($this->event, $this); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } +} diff --git a/plugins/Event/rsvpform.php b/plugins/Event/rsvpform.php new file mode 100644 index 0000000000..ad30f6a36e --- /dev/null +++ b/plugins/Event/rsvpform.php @@ -0,0 +1,120 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * A form to RSVP for an event + * + * @category General + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class RSVPForm extends Form +{ + protected $event = null; + + function __construct($event, $out=null) + { + parent::__construct($out); + $this->event = $event; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'form_event_rsvp'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('newrsvp'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'new_rsvp_data')); + + $this->out->text(_('RSVP: ')); + + $this->out->hidden('event', $this->event->id); + + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('yes', _m('BUTTON', 'Yes')); + $this->out->submit('no', _m('BUTTON', 'No')); + $this->out->submit('maybe', _m('BUTTON', 'Maybe')); + } +} diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php new file mode 100644 index 0000000000..7fb702f9db --- /dev/null +++ b/plugins/Event/showevent.php @@ -0,0 +1,109 @@ +. + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @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); +} + +/** + * Show a single event, with associated information + * + * @category Event + * @package StatusNet + * @author Evan Prodromou + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ShoweventAction extends ShownoticeAction +{ + protected $id = null; + protected $event = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + OwnerDesignAction::prepare($argarray); + + $this->id = $this->trimmed('id'); + + $this->event = Happening::staticGet('id', $this->id); + + if (empty($this->event)) { + throw new ClientException(_('No such event.'), 404); + } + + $this->notice = $this->event->getNotice(); + + if (empty($this->notice)) { + // Did we used to have it, and it got deleted? + throw new ClientException(_('No such event.'), 404); + } + + $this->user = User::staticGet('id', $this->event->profile_id); + + if (empty($this->user)) { + throw new ClientException(_('No such user.'), 404); + } + + $this->profile = $this->user->getProfile(); + + if (empty($this->profile)) { + throw new ServerException(_('User without a profile.')); + } + + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + return true; + } + + /** + * Title of the page + * + * Used by Action class for layout. + * + * @return string page tile + */ + + function title() + { + return $this->event->title; + } +} diff --git a/plugins/Event/showrsvp.php b/plugins/Event/showrsvp.php new file mode 100644 index 0000000000..fde1d48f0e --- /dev/null +++ b/plugins/Event/showrsvp.php @@ -0,0 +1,117 @@ +. + * + * @category RSVP + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @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); +} + +/** + * Show a single RSVP, with associated information + * + * @category RSVP + * @package StatusNet + * @author Evan Prodromou + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ShowrsvpAction extends ShownoticeAction +{ + protected $rsvp = null; + protected $event = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + OwnerDesignAction::prepare($argarray); + + $this->id = $this->trimmed('id'); + + $this->rsvp = RSVP::staticGet('id', $this->id); + + if (empty($this->rsvp)) { + throw new ClientException(_('No such RSVP.'), 404); + } + + $this->event = $this->rsvp->getEvent(); + + if (empty($this->event)) { + throw new ClientException(_('No such Event.'), 404); + } + + $this->notice = $this->rsvp->getNotice(); + + if (empty($this->notice)) { + // Did we used to have it, and it got deleted? + throw new ClientException(_('No such RSVP.'), 404); + } + + $this->user = User::staticGet('id', $this->rsvp->profile_id); + + if (empty($this->user)) { + throw new ClientException(_('No such user.'), 404); + } + + $this->profile = $this->user->getProfile(); + + if (empty($this->profile)) { + throw new ServerException(_('User without a profile.')); + } + + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + return true; + } + + /** + * Title of the page + * + * Used by Action class for layout. + * + * @return string page tile + */ + + function title() + { + return sprintf(_('%s\'s RSVP for "%s"'), + $this->user->nickname, + $this->event->title); + } +}