From 24fd39d6ab2c0bf72ba0dc970883fdd9fa751465 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 8 Mar 2011 11:15:17 -0500 Subject: [PATCH 01/10] new Event plugin --- plugins/Event/Event.php | 184 ++++++++++++++++++++++ plugins/Event/EventPlugin.php | 284 ++++++++++++++++++++++++++++++++++ plugins/Event/RSVP.php | 184 ++++++++++++++++++++++ plugins/Event/newevent.php | 164 ++++++++++++++++++++ plugins/Event/newrsvp.php | 164 ++++++++++++++++++++ 5 files changed, 980 insertions(+) create mode 100644 plugins/Event/Event.php create mode 100644 plugins/Event/EventPlugin.php create mode 100644 plugins/Event/RSVP.php create mode 100644 plugins/Event/newevent.php create mode 100644 plugins/Event/newrsvp.php diff --git a/plugins/Event/Event.php b/plugins/Event/Event.php new file mode 100644 index 0000000000..38d68c91ed --- /dev/null +++ b/plugins/Event/Event.php @@ -0,0 +1,184 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + function keyTypes() + { + return array('user_id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not save new greeting count for %d."), + $user_id)); + } + } else { + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not increment greeting count for %d."), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php new file mode 100644 index 0000000000..a0d1140f37 --- /dev/null +++ b/plugins/Event/EventPlugin.php @@ -0,0 +1,284 @@ +. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2009 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); +} + +/** + * Sample plugin main class + * + * Each plugin requires a main class to interact with the StatusNet system. + * + * The main class usually extends the Plugin class that comes with StatusNet. + * + * The class has standard-named methods that will be called when certain events + * happen in the code base. These methods have names like 'onX' where X is an + * event name (see EVENTS.txt for the list of available events). Event handlers + * have pre-defined arguments, based on which event they're handling. A typical + * event handler: + * + * function onSomeEvent($paramA, &$paramB) + * { + * if ($paramA == 'jed') { + * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); + * } + * $paramB = 'spock'; + * return true; + * } + * + * Event handlers must return a boolean value. If they return false, all other + * event handlers for this event (in other plugins) will be skipped, and in some + * cases the default processing for that event would be skipped. This is great for + * replacing the default action of an event. + * + * If the handler returns true, processing of other event handlers and the default + * processing will continue. This is great for extending existing functionality. + * + * If the handler throws an exception, processing will stop, and the exception's + * error will be shown to the user. + * + * To install a plugin (like this one), site admins add the following code to + * their config.php file: + * + * addPlugin('Sample'); + * + * Plugins must be installed in one of the following directories: + * + * local/plugins/{$pluginclass}.php + * local/plugins/{$name}/{$pluginclass}.php + * local/{$pluginclass}.php + * local/{$name}/{$pluginclass}.php + * plugins/{$pluginclass}.php + * plugins/{$name}/{$pluginclass}.php + * + * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is + * the name of the main class, like 'SamplePlugin'. Plugins that are part of the + * main StatusNet distribution go in 'plugins' and third-party or local ones go + * in 'local'. + * + * Simple plugins can be implemented as a single module. Others are more complex + * and require additional modules; these should use their own directory, like + * 'local/plugins/{$name}/'. All files related to the plugin, including images, + * JavaScript, CSS, external libraries or PHP modules should go in the plugin + * directory. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class SamplePlugin extends Plugin +{ + /** + * Plugins are configured using public instance attributes. To set + * their values, site administrators use this syntax: + * + * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); + * + * The same plugin class can be initialized multiple times with different + * arguments: + * + * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); + * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); + * + */ + + public $attr1 = null; + public $attr2 = null; + + /** + * Initializer for this plugin + * + * Plugins overload this method to do any initialization they need, + * like connecting to remote servers or creating paths or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function initialize() + { + return true; + } + + /** + * Cleanup for this plugin + * + * Plugins overload this method to do any cleanup they need, + * like disconnecting from remote servers or deleting temp files or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function cleanup() + { + return true; + } + + /** + * Database schema setup + * + * Plugins can add their own tables to the StatusNet database. Plugins + * should use StatusNet's schema interface to add or delete tables. The + * ensureTable() method provides an easy way to ensure a table's structure + * and availability. + * + * By default, the schema is checked every time StatusNet is run (say, when + * a Web page is hit). Admins can configure their systems to only check the + * schema when the checkschema.php script is run, greatly improving performance. + * However, they need to remember to run that script after installing or + * upgrading a plugin! + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('user_greeting_count', + array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true), + 'greeting_count' => array('type' => 'int'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + // Not all databases will support foreign keys, but even + // when not enforced it's helpful to include these definitions + // as documentation. + 'user_greeting_count_user_id_fkey' => array('user', array('user_id' => 'id')), + ), + ) + ); + + return true; + } + + /** + * Load related modules when needed + * + * Most non-trivial plugins will require extra modules to do their work. Typically + * these include data classes, action classes, widget classes, or external libraries. + * + * This method receives a class name and loads the PHP file related to that class. By + * tradition, action classes typically have files named for the action, all lower-case. + * Data classes are in files with the data class name, initial letter capitalized. + * + * Note that this method will be called for *all* overloaded classes, not just ones + * in this plugin! So, make sure to return true by default to let other plugins, and + * the core code, get a chance. + * + * @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 'HelloAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'User_greeting_count': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * This event handler lets the plugin map URLs on the site to actions (and + * thus an action handler class). Note that the action handler class for an + * action will be named 'FoobarAction', where action = 'foobar'. The class + * must be loaded in the onAutoload() method. + * + * @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/hello', + array('action' => 'hello')); + return true; + } + + /** + * Modify the default menu to link to our custom action + * + * Using event handlers, it's possible to modify the default UI for pages + * almost without limit. In this method, we add a menu item to the default + * primary menu for the interface to link to our action. + * + * The Action class provides a rich set of events to hook, as well as output + * methods. + * + * @param Action $action The current action handler. Use this to + * do any output. + * + * @return boolean hook value; true means continue processing, false means stop. + * + * @see Action + */ + function onEndPrimaryNav($action) + { + // common_local_url() gets the correct URL for the action name + // we provide + + $action->menuItem(common_local_url('hello'), + _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Sample', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Sample', + 'rawdescription' => + _m('A sample plugin to show basics of development for new hackers.')); + return true; + } +} diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php new file mode 100644 index 0000000000..38d68c91ed --- /dev/null +++ b/plugins/Event/RSVP.php @@ -0,0 +1,184 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + function keyTypes() + { + return array('user_id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not save new greeting count for %d."), + $user_id)); + } + } else { + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not increment greeting count for %d."), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php new file mode 100644 index 0000000000..a793ac6de2 --- /dev/null +++ b/plugins/Event/newevent.php @@ -0,0 +1,164 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s!'), $this->user->nickname); + } + } + + /** + * Show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * This method also demonstrates use of a plural localized string. + * + * @return void + */ + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time.', + 'I have greeted you %d times.', + $this->gc->greeting_count), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } +} diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php new file mode 100644 index 0000000000..a793ac6de2 --- /dev/null +++ b/plugins/Event/newrsvp.php @@ -0,0 +1,164 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s!'), $this->user->nickname); + } + } + + /** + * Show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * This method also demonstrates use of a plural localized string. + * + * @return void + */ + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time.', + 'I have greeted you %d times.', + $this->gc->greeting_count), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } +} From 0b30fb3971ab1ac3a2bf9e4efa969a449490fb5c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 8 Mar 2011 11:28:53 -0500 Subject: [PATCH 02/10] Make event-specific stuff in EventPlugin --- plugins/Event/EventPlugin.php | 212 ++++------------------------------ 1 file changed, 24 insertions(+), 188 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index a0d1140f37..af0875c805 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -1,9 +1,9 @@ . * - * @category Sample + * @category Event * @package StatusNet - * @author Brion Vibber * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ @@ -36,129 +35,19 @@ if (!defined('STATUSNET')) { } /** - * Sample plugin main class - * - * Each plugin requires a main class to interact with the StatusNet system. - * - * The main class usually extends the Plugin class that comes with StatusNet. - * - * The class has standard-named methods that will be called when certain events - * happen in the code base. These methods have names like 'onX' where X is an - * event name (see EVENTS.txt for the list of available events). Event handlers - * have pre-defined arguments, based on which event they're handling. A typical - * event handler: - * - * function onSomeEvent($paramA, &$paramB) - * { - * if ($paramA == 'jed') { - * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); - * } - * $paramB = 'spock'; - * return true; - * } - * - * Event handlers must return a boolean value. If they return false, all other - * event handlers for this event (in other plugins) will be skipped, and in some - * cases the default processing for that event would be skipped. This is great for - * replacing the default action of an event. - * - * If the handler returns true, processing of other event handlers and the default - * processing will continue. This is great for extending existing functionality. - * - * If the handler throws an exception, processing will stop, and the exception's - * error will be shown to the user. - * - * To install a plugin (like this one), site admins add the following code to - * their config.php file: - * - * addPlugin('Sample'); - * - * Plugins must be installed in one of the following directories: - * - * local/plugins/{$pluginclass}.php - * local/plugins/{$name}/{$pluginclass}.php - * local/{$pluginclass}.php - * local/{$name}/{$pluginclass}.php - * plugins/{$pluginclass}.php - * plugins/{$name}/{$pluginclass}.php - * - * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is - * the name of the main class, like 'SamplePlugin'. Plugins that are part of the - * main StatusNet distribution go in 'plugins' and third-party or local ones go - * in 'local'. - * - * Simple plugins can be implemented as a single module. Others are more complex - * and require additional modules; these should use their own directory, like - * 'local/plugins/{$name}/'. All files related to the plugin, including images, - * JavaScript, CSS, external libraries or PHP modules should go in the plugin - * directory. + * Event plugin * * @category Sample * @package StatusNet - * @author Brion Vibber * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class SamplePlugin extends Plugin +class EventPlugin extends MicroappPlugin { /** - * Plugins are configured using public instance attributes. To set - * their values, site administrators use this syntax: - * - * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); - * - * The same plugin class can be initialized multiple times with different - * arguments: - * - * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); - * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); - * - */ - - public $attr1 = null; - public $attr2 = null; - - /** - * Initializer for this plugin - * - * Plugins overload this method to do any initialization they need, - * like connecting to remote servers or creating paths or so on. - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function initialize() - { - return true; - } - - /** - * Cleanup for this plugin - * - * Plugins overload this method to do any cleanup they need, - * like disconnecting from remote servers or deleting temp files or so on. - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function cleanup() - { - return true; - } - - /** - * Database schema setup - * - * Plugins can add their own tables to the StatusNet database. Plugins - * should use StatusNet's schema interface to add or delete tables. The - * ensureTable() method provides an easy way to ensure a table's structure - * and availability. - * - * By default, the schema is checked every time StatusNet is run (say, when - * a Web page is hit). Admins can configure their systems to only check the - * schema when the checkschema.php script is run, greatly improving performance. - * However, they need to remember to run that script after installing or - * upgrading a plugin! + * Set up our tables (event and rsvp) * * @see Schema * @see ColumnDef @@ -169,23 +58,8 @@ class SamplePlugin extends Plugin { $schema = Schema::get(); - // For storing user-submitted flags on profiles - - $schema->ensureTable('user_greeting_count', - array( - 'fields' => array( - 'user_id' => array('type' => 'int', 'not null' => true), - 'greeting_count' => array('type' => 'int'), - ), - 'primary key' => array('user_id'), - 'foreign keys' => array( - // Not all databases will support foreign keys, but even - // when not enforced it's helpful to include these definitions - // as documentation. - 'user_greeting_count_user_id_fkey' => array('user', array('user_id' => 'id')), - ), - ) - ); + $schema->ensureTable('event', Event::schemaDef()); + $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; } @@ -193,17 +67,6 @@ class SamplePlugin extends Plugin /** * Load related modules when needed * - * Most non-trivial plugins will require extra modules to do their work. Typically - * these include data classes, action classes, widget classes, or external libraries. - * - * This method receives a class name and loads the PHP file related to that class. By - * tradition, action classes typically have files named for the action, all lower-case. - * Data classes are in files with the data class name, initial letter capitalized. - * - * Note that this method will be called for *all* overloaded classes, not just ones - * in this plugin! So, make sure to return true by default to let other plugins, and - * the core code, get a chance. - * * @param string $cls Name of the class to be loaded * * @return boolean hook value; true means continue processing, false means stop. @@ -214,10 +77,12 @@ class SamplePlugin extends Plugin switch ($cls) { - case 'HelloAction': + case 'NeweventAction': + case 'NewrsvpAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'User_greeting_count': + case 'Event': + case 'RSVP': include_once $dir . '/'.$cls.'.php'; return false; default: @@ -228,57 +93,28 @@ class SamplePlugin extends Plugin /** * Map URLs to actions * - * This event handler lets the plugin map URLs on the site to actions (and - * thus an action handler class). Note that the action handler class for an - * action will be named 'FoobarAction', where action = 'foobar'. The class - * must be loaded in the onAutoload() method. - * * @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/hello', - array('action' => 'hello')); - return true; - } - - /** - * Modify the default menu to link to our custom action - * - * Using event handlers, it's possible to modify the default UI for pages - * almost without limit. In this method, we add a menu item to the default - * primary menu for the interface to link to our action. - * - * The Action class provides a rich set of events to hook, as well as output - * methods. - * - * @param Action $action The current action handler. Use this to - * do any output. - * - * @return boolean hook value; true means continue processing, false means stop. - * - * @see Action - */ - function onEndPrimaryNav($action) - { - // common_local_url() gets the correct URL for the action name - // we provide - - $action->menuItem(common_local_url('hello'), - _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + $m->connect('main/event/new', + array('action' => 'newevent')); + $m->connect('main/event/rsvp', + array('action' => 'newrsvp')); return true; } function onPluginVersion(&$versions) { - $versions[] = array('name' => 'Sample', + $versions[] = array('name' => 'Event', 'version' => STATUSNET_VERSION, - 'author' => 'Brion Vibber, Evan Prodromou', - 'homepage' => 'http://status.net/wiki/Plugin:Sample', - 'rawdescription' => - _m('A sample plugin to show basics of development for new hackers.')); + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Event', + 'description' => + _m('Event invitations and RSVPs.')); return true; } } From f715821cf0a4f95eb0d265eda62abed294474b2b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:33:26 -0500 Subject: [PATCH 03/10] Kinda complete and kinda working-ish events --- plugins/Event/Event.php | 184 ------------------------ plugins/Event/EventPlugin.php | 224 ++++++++++++++++++++++++++++- plugins/Event/Happening.php | 198 ++++++++++++++++++++++++++ plugins/Event/RSVP.php | 255 ++++++++++++++++++---------------- plugins/Event/eventform.php | 157 +++++++++++++++++++++ plugins/Event/newevent.php | 217 ++++++++++++++++------------- plugins/Event/showevent.php | 109 +++++++++++++++ plugins/Event/showrsvp.php | 117 ++++++++++++++++ 8 files changed, 1060 insertions(+), 401 deletions(-) delete mode 100644 plugins/Event/Event.php create mode 100644 plugins/Event/Happening.php create mode 100644 plugins/Event/eventform.php create mode 100644 plugins/Event/showevent.php create mode 100644 plugins/Event/showrsvp.php diff --git a/plugins/Event/Event.php b/plugins/Event/Event.php deleted file mode 100644 index 38d68c91ed..0000000000 --- a/plugins/Event/Event.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, 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); -} - -require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; - -/** - * Data class for counting greetings - * - * We use the DB_DataObject framework for data classes in StatusNet. Each - * table maps to a particular data class, making it easier to manipulate - * data. - * - * Data classes should extend Memcached_DataObject, the (slightly misnamed) - * extension of DB_DataObject that provides caching, internationalization, - * and other bits of good functionality to StatusNet-specific data classes. - * - * @category Action - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * @see DB_DataObject - */ - -class User_greeting_count extends Memcached_DataObject -{ - public $__table = 'user_greeting_count'; // table name - public $user_id; // int(4) primary_key not_null - public $greeting_count; // int(4) - - /** - * Get an instance by key - * - * This is a utility method to get a single instance with a given key value. - * - * @param string $k Key to use to lookup (usually 'user_id' for this class) - * @param mixed $v Value to lookup - * - * @return User_greeting_count object found, or null for no hits - * - */ - function staticGet($k, $v=null) - { - return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); - } - - /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions - */ - function table() - { - return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'greeting_count' => DB_DATAOBJECT_INT); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has, since it - * won't appear in StatusNet's own keys list. In most cases, this will - * simply reference your keyTypes() function. - * - * @return array list of key field names - */ - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. This key information is used to store and clear - * cached data, so be sure to list any key that will be used for static - * lookups. - * - * @return array associative array of key definitions, field name to type: - * 'K' for primary key: for compound keys, add an entry for each component; - * 'U' for unique keys: compound keys are not well supported here. - */ - function keyTypes() - { - return array('user_id' => 'K'); - } - - /** - * Magic formula for non-autoincrementing integer primary keys - * - * If a table has a single integer column as its primary key, DB_DataObject - * assumes that the column is auto-incrementing and makes a sequence table - * to do this incrementation. Since we don't need this for our class, we - * overload this method and return the magic formula that DB_DataObject needs. - * - * @return array magic three-false array that stops auto-incrementing. - */ - function sequenceKey() - { - return array(false, false, false); - } - - /** - * Increment a user's greeting count and return instance - * - * This method handles the ins and outs of creating a new greeting_count for a - * user or fetching the existing greeting count and incrementing its value. - * - * @param integer $user_id ID of the user to get a count for - * - * @return User_greeting_count instance for this user, with count already incremented. - */ - static function inc($user_id) - { - $gc = User_greeting_count::staticGet('user_id', $user_id); - - if (empty($gc)) { - - $gc = new User_greeting_count(); - - $gc->user_id = $user_id; - $gc->greeting_count = 1; - - $result = $gc->insert(); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not save new greeting count for %d."), - $user_id)); - } - } else { - $orig = clone($gc); - - $gc->greeting_count++; - - $result = $gc->update($orig); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not increment greeting count for %d."), - $user_id)); - } - } - - return $gc; - } -} diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index af0875c805..d6d7e00fc1 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -58,7 +58,7 @@ class EventPlugin extends MicroappPlugin { $schema = Schema::get(); - $schema->ensureTable('event', Event::schemaDef()); + $schema->ensureTable('event', Happening::schemaDef()); $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; @@ -79,9 +79,14 @@ class EventPlugin extends MicroappPlugin { case 'NeweventAction': case 'NewrsvpAction': + case 'ShoweventAction': + case 'ShowrsvpAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'Event': + case 'EventForm': + include_once $dir . '/'.strtolower($cls).'.php'; + break; + case 'Happening': case 'RSVP': include_once $dir . '/'.$cls.'.php'; return false; @@ -104,6 +109,12 @@ class EventPlugin extends MicroappPlugin array('action' => 'newevent')); $m->connect('main/event/rsvp', array('action' => 'newrsvp')); + $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; } @@ -117,4 +128,213 @@ class EventPlugin extends MicroappPlugin _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->element('span', null, 'RSVP'); + return; + } + + function showEventNotice($notice, $out) + { + $out->raw($notice->rendered); + return; + } + + /** + * 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..054e57c732 --- /dev/null +++ b/plugins/Event/Happening.php @@ -0,0 +1,198 @@ + + * @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 $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'), + 'start_time' => array('type' => 'datetime'), + 'end_time' => array('type' => 'datetime'), + 'title' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'location' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'description' => array('type' => 'text'), + 'created' => array('type' => 'datetime', + 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'happening_uri_key' => array('uri'), + ), + ); + } + + function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options) + { + 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; + + 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; + } + + $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() + { + return Happening::staticGet('uri', $notice->uri); + } +} diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 38d68c91ed..69cae4b7dc 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -1,6 +1,6 @@ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ * - * @see DB_DataObject + * @see Managed_DataObject */ -class User_greeting_count extends Memcached_DataObject +class RSVP extends Managed_DataObject { - public $__table = 'user_greeting_count'; // table name - public $user_id; // int(4) primary_key not_null - public $greeting_count; // int(4) + 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 * - * This is a utility method to get a single instance with a given key value. - * - * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param string $k Key to use to lookup (usually 'id' for this class) * @param mixed $v Value to lookup * - * @return User_greeting_count object found, or null for no hits + * @return RSVP object found, or null for no hits * */ function staticGet($k, $v=null) { - return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + return Memcached_DataObject::staticGet('RSVP', $k, $v); } /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions + * The One True Thingy that must be defined and declared. */ - function table() + public static function schemaDef() { - return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'greeting_count' => DB_DATAOBJECT_INT); + return array( + 'description' => 'A real-world 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')), + ); } - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has, since it - * won't appear in StatusNet's own keys list. In most cases, this will - * simply reference your keyTypes() function. - * - * @return array list of key field names - */ - function keys() + function saveNew($profile, $event, $result, $options) { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. This key information is used to store and clear - * cached data, so be sure to list any key that will be used for static - * lookups. - * - * @return array associative array of key definitions, field name to type: - * 'K' for primary key: for compound keys, add an entry for each component; - * 'U' for unique keys: compound keys are not well supported here. - */ - function keyTypes() - { - return array('user_id' => 'K'); - } - - /** - * Magic formula for non-autoincrementing integer primary keys - * - * If a table has a single integer column as its primary key, DB_DataObject - * assumes that the column is auto-incrementing and makes a sequence table - * to do this incrementation. Since we don't need this for our class, we - * overload this method and return the magic formula that DB_DataObject needs. - * - * @return array magic three-false array that stops auto-incrementing. - */ - function sequenceKey() - { - return array(false, false, false); - } - - /** - * Increment a user's greeting count and return instance - * - * This method handles the ins and outs of creating a new greeting_count for a - * user or fetching the existing greeting count and incrementing its value. - * - * @param integer $user_id ID of the user to get a count for - * - * @return User_greeting_count instance for this user, with count already incremented. - */ - static function inc($user_id) - { - $gc = User_greeting_count::staticGet('user_id', $user_id); - - if (empty($gc)) { - - $gc = new User_greeting_count(); - - $gc->user_id = $user_id; - $gc->greeting_count = 1; - - $result = $gc->insert(); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not save new greeting count for %d."), - $user_id)); - } - } else { - $orig = clone($gc); - - $gc->greeting_count++; - - $result = $gc->update($orig); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not increment greeting count for %d."), - $user_id)); + if (array_key_exists('uri', $options)) { + $other = RSVP::staticGet('uri', $options['uri']); + if (!empty($other)) { + throw new ClientException(_('RSVP already exists.')); } } - return $gc; + $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; + } + + function verbFor($code) + { + return ($code == 1) ? RSVP::POSITIVE : + ($code == 0) ? RSVP::NEGATIVE : null; + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + static function fromNotice() + { + return RSVP::staticGet('uri', $notice->uri); } } diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php new file mode 100644 index 0000000000..8347639b5b --- /dev/null +++ b/plugins/Event/eventform.php @@ -0,0 +1,157 @@ +. + * + * @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'; + } + + /** + * 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('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 index a793ac6de2..66b15ea41b 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -1,17 +1,11 @@ - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * Copyright (C) 2011, StatusNet, Inc. + * + * Add a new event + * + * 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 @@ -25,140 +19,173 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + * + * @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); } /** - * Give a warm greeting to our friendly user + * Add a new event * - * This sample action shows some basic ways of doing output in an action - * class. - * - * Action classes have several output methods that they override from - * the parent class. - * - * @category Sample - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ + * @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 HelloAction extends Action + +class NeweventAction extends Action { - var $user = null; - var $gc = null; + 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; /** - * Take arguments for running + * Returns the title of the action * - * This method is called first, and it lets the action class get - * all its arguments and validate them. It's also the time - * to fetch any relevant data from the database. - * - * Action classes should run parent::prepare($args) as the first - * line of this method to make sure the default argument-processing - * happens. - * - * @param array $args $_REQUEST args - * - * @return boolean success flag + * @return string Action title */ - function prepare($args) + + function title() { - parent::prepare($args); + 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)) { - $this->gc = User_greeting_count::inc($this->user->id); + 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->description = $this->trimmed('description'); + return true; } /** - * Handle request + * Handler method * - * This is the main method for handling a request. Note that - * most preparation should be done in the prepare() method; - * by the time handle() is called the action should be - * more or less ready to go. - * - * @param array $args $_REQUEST args; handled in prepare() + * @param array $argarray is ignored since it's now passed in in prepare() * * @return void */ - function handle($args) - { - parent::handle($args); - $this->showPage(); - } - - /** - * Title of this page - * - * Override this method to show a custom title. - * - * @return string Title of the page - */ - function title() + function handle($argarray=null) { - if (empty($this->user)) { - return _m('Hello'); + parent::handle($argarray); + + if ($this->isPost()) { + $this->newEvent(); } else { - return sprintf(_m('Hello, %s!'), $this->user->nickname); + $this->showPage(); } + + return; } /** - * Show content in the content area - * - * The default StatusNet page has a lot of decorations: menus, - * logos, tabs, all that jazz. This method is used to show - * content in the content area of the page; it's the main - * thing you want to overload. - * - * This method also demonstrates use of a plural localized string. + * Add a new event * * @return void */ + + function newEvent() + { + try { + if (empty($this->title)) { + throw new ClientException(_('Event must have a title.')); + } + + if (empty($this->url)) { + throw new ClientException(_('Event must have an URL.')); + } + + + $saved = Event::saveNew($this->user->getProfile(), + $this->title, + $this->url, + $this->tags, + $this->description); + + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + common_redirect($saved->bestUrl(), 303); + } + + /** + * Show the event form + * + * @return void + */ + function showContent() { - if (empty($this->user)) { - $this->element('p', array('class' => 'greeting'), - _m('Hello, stranger!')); - } else { - $this->element('p', array('class' => 'greeting'), - sprintf(_m('Hello, %s'), $this->user->nickname)); - $this->element('p', array('class' => 'greeting_count'), - sprintf(_m('I have greeted you %d time.', - 'I have greeted you %d times.', - $this->gc->greeting_count), - $this->gc->greeting_count)); + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); } + + $form = new EventForm($this); + + $form->show(); + + return; } /** * Return true if read only. * - * Some actions only read from the database; others read and write. - * The simple database load-balancer built into StatusNet will - * direct read-only actions to database mirrors (if they are configured), - * and read-write actions to the master database. + * MAY override * - * This defaults to false to avoid data integrity issues, but you - * should make sure to overload it for performance gains. - * - * @param array $args other arguments, if RO/RW status depends on them. + * @param array $args other arguments * * @return boolean is read only action? */ + function isReadOnly($args) { - return false; + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } } } diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php new file mode 100644 index 0000000000..f8b032c111 --- /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 = Event::staticGet('id', $this->id); + + if (empty($this->event)) { + throw new ClientException(_('No such event.'), 404); + } + + $this->notice = $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); + } +} From ca36dfecf9217670f31d9c1c65c337064cd2413a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:37:51 -0500 Subject: [PATCH 04/10] more fixes for events --- plugins/Event/Happening.php | 2 +- plugins/Event/newevent.php | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 054e57c732..5ede26706b 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -112,7 +112,7 @@ class Happening extends Managed_DataObject ); } - function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options) + function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options=array()) { if (array_key_exists('uri', $options)) { $other = Happening::staticGet('uri', $options['uri']); diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index 66b15ea41b..fe23e8825d 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -93,6 +93,14 @@ class NeweventAction extends Action $this->location = $this->trimmed('location'); $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; } @@ -130,16 +138,20 @@ class NeweventAction extends Action throw new ClientException(_('Event must have a title.')); } - if (empty($this->url)) { - throw new ClientException(_('Event must have an URL.')); + 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.')); + } $saved = Event::saveNew($this->user->getProfile(), - $this->title, - $this->url, - $this->tags, - $this->description); + $this->start_time, + $this->end_time, + $this->title, + $this->location, + $this->description); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -147,7 +159,7 @@ class NeweventAction extends Action return; } - common_redirect($saved->bestUrl(), 303); + } /** From 53e67b5ed5bb01e19fef02b98932e5d9d6e76160 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:48:14 -0500 Subject: [PATCH 05/10] make new event work, sort of --- plugins/Event/EventPlugin.php | 2 +- plugins/Event/eventform.php | 2 +- plugins/Event/newevent.php | 44 +++++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index d6d7e00fc1..f33de09d78 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -58,7 +58,7 @@ class EventPlugin extends MicroappPlugin { $schema = Schema::get(); - $schema->ensureTable('event', Happening::schemaDef()); + $schema->ensureTable('happening', Happening::schemaDef()); $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 8347639b5b..8d108020a6 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -66,7 +66,7 @@ class EventForm extends Form function formClass() { - return 'form_settings ajax'; + return 'form_settings ajax-notice'; } /** diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index fe23e8825d..365e9c1434 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -146,12 +146,12 @@ class NeweventAction extends Action throw new ClientException(_('Event must have an end time.')); } - $saved = Event::saveNew($this->user->getProfile(), - $this->start_time, - $this->end_time, - $this->title, - $this->location, - $this->description); + $saved = Happening::saveNew($this->user->getProfile(), + $this->start_time, + $this->end_time, + $this->title, + $this->location, + $this->description); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -159,7 +159,21 @@ class NeweventAction extends Action 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); + } } /** @@ -200,4 +214,20 @@ class NeweventAction extends Action 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(); + } } From f00f5f20b8b373fc8631071e8be6522a937a7690 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:07:30 -0500 Subject: [PATCH 06/10] add url to events --- plugins/Event/EventPlugin.php | 65 +++++++++++++++++++++++++++++++++-- plugins/Event/Happening.php | 24 +++++++++---- plugins/Event/RSVP.php | 2 +- plugins/Event/eventform.php | 7 ++++ plugins/Event/newevent.php | 8 +++-- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index f33de09d78..896ecaf5c8 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -300,8 +300,69 @@ class EventPlugin extends MicroappPlugin function showEventNotice($notice, $out) { - $out->raw($notice->rendered); - return; + $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' => $att->url, + 'class' => 'event-title entry-title summary'), + $event->title); + } else { + $out->text($event->title); + } + + $out->elementEnd('h3'); + + $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); + } + + $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'); } /** diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 5ede26706b..503cd8af14 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -60,6 +60,7 @@ class Happening extends Managed_DataObject public $end_time; // datetime public $title; // varchar(255) public $location; // varchar(255) + public $url; // varchar(255) public $description; // text public $created; // datetime @@ -92,15 +93,16 @@ class Happening extends Managed_DataObject 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), - 'profile_id' => array('type' => 'int'), - 'start_time' => array('type' => 'datetime'), - 'end_time' => array('type' => 'datetime'), + '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, - 'not null' => true), + 'length' => 255), + 'url' => array('type' => 'varchar', + 'length' => 255), 'description' => array('type' => 'text'), 'created' => array('type' => 'datetime', 'not null' => true), @@ -109,10 +111,13 @@ class Happening extends Managed_DataObject '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, $options=array()) + 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']); @@ -130,6 +135,7 @@ class Happening extends Managed_DataObject $ev->title = $title; $ev->location = $location; $ev->description = $description; + $ev->url = $url; if (array_key_exists('created', $options)) { $ev->created = $options['created']; @@ -177,6 +183,10 @@ class Happening extends Managed_DataObject $options['uri'] = $ev->uri; } + if (!empty($url)) { + $options['urls'] = array($url); + } + $saved = Notice::saveNew($profile->id, $content, array_key_exists('source', $options) ? @@ -191,7 +201,7 @@ class Happening extends Managed_DataObject return Notice::staticGet('uri', $this->uri); } - static function fromNotice() + static function fromNotice($notice) { return Happening::staticGet('uri', $notice->uri); } diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 69cae4b7dc..851978e819 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -192,7 +192,7 @@ class RSVP extends Managed_DataObject return Notice::staticGet('uri', $this->uri); } - static function fromNotice() + static function fromNotice($notice) { return RSVP::staticGet('uri', $notice->uri); } diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 8d108020a6..e6bc1e7016 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -133,6 +133,13 @@ class EventForm extends Form _('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'), diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index 365e9c1434..7c0fd0177f 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -91,6 +91,7 @@ class NeweventAction extends Action $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'); @@ -146,12 +147,15 @@ class NeweventAction extends Action throw new ClientException(_('Event must have an end time.')); } - $saved = Happening::saveNew($this->user->getProfile(), + $profile = $this->user->getProfile(); + + $saved = Happening::saveNew($profile, $this->start_time, $this->end_time, $this->title, $this->location, - $this->description); + $this->description, + $this->url); } catch (ClientException $ce) { $this->error = $ce->getMessage(); From 04a3157681ed5375220c1030b7d5043413ecd65a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:25:50 -0500 Subject: [PATCH 07/10] stream for direct responses --- classes/Notice.php | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index d520f4728f..d49a8f483b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -463,6 +463,10 @@ class Notice extends Memcached_DataObject // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); + + if (!empty($this->reply_to)) { + self::blow('notice:responses:%d', $this->reply_to); + } if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -492,6 +496,7 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); + self::blow('notice:responses:%d', $this->id); if ($this->isPublic()) { self::blow('public;last'); @@ -2133,4 +2138,46 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + function responseStream($offset=0, $limit=20, $since_id=0, $max_id=0) + { + $ids = Notice::stream(array($this, '_responseStreamDirect'), + array(), + 'notice:responses:'.$id, + $offset, $limit, $since_id, $max_id); + + return Notice::getStreamByIds($ids); + } + + function _responseStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->reply_to = $this->reply_to; + + $notice->orderBy('created DESC, id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; + } } From 1e4e9a8456907821a2c84ae17ec4f28b0fb44604 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:40:19 -0500 Subject: [PATCH 08/10] Revert "stream for direct responses" I don't actually need this so I'm reverting it. This reverts commit 04a3157681ed5375220c1030b7d5043413ecd65a. --- classes/Notice.php | 47 ---------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index d49a8f483b..d520f4728f 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -463,10 +463,6 @@ class Notice extends Memcached_DataObject // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); - - if (!empty($this->reply_to)) { - self::blow('notice:responses:%d', $this->reply_to); - } if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -496,7 +492,6 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); - self::blow('notice:responses:%d', $this->id); if ($this->isPublic()) { self::blow('public;last'); @@ -2138,46 +2133,4 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } - - function responseStream($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $ids = Notice::stream(array($this, '_responseStreamDirect'), - array(), - 'notice:responses:'.$id, - $offset, $limit, $since_id, $max_id); - - return Notice::getStreamByIds($ids); - } - - function _responseStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->reply_to = $this->reply_to; - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; - } } From 728869e3110d3246dc1943a5d6ca14e88a2cc821 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:40:49 -0500 Subject: [PATCH 09/10] show RSVPs for an event --- plugins/Event/EventPlugin.php | 10 ++++++++++ plugins/Event/Happening.php | 5 +++++ plugins/Event/RSVP.php | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index 896ecaf5c8..d8d9b572ed 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -321,6 +321,8 @@ class EventPlugin extends MicroappPlugin $out->elementEnd('h3'); + // FIXME: better dates + $out->elementStart('div', 'event-times'); $out->element('abbr', array('class' => 'dtstart', 'title' => common_date_iso8601($event->start_time)), @@ -339,6 +341,14 @@ class EventPlugin extends MicroappPlugin $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]))); + $out->elementStart('div', array('class' => 'event-info entry-content')); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 503cd8af14..b2adb4d9b5 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -205,4 +205,9 @@ class Happening extends Managed_DataObject { return Happening::staticGet('uri', $notice->uri); } + + function getRSVPs() + { + return RSVP::forEvent($this); + } } diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 851978e819..36c6b32ec7 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -196,4 +196,22 @@ class RSVP extends Managed_DataObject { 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 = $this->verbFor($rsvp->code); + $rsvps[$verb][] = clone($rsvp); + } + } + + return $rsvps; + } } From 35429c28e5bdde71fe9dff9e69ef795a31a96e8d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 12:28:25 -0500 Subject: [PATCH 10/10] updates to make RSVPs work --- plugins/Event/EventPlugin.php | 23 +++- plugins/Event/Happening.php | 6 + plugins/Event/RSVP.php | 32 ++++- plugins/Event/cancelrsvp.php | 202 ++++++++++++++++++++++++++++ plugins/Event/cancelrsvpform.php | 128 ++++++++++++++++++ plugins/Event/newevent.php | 4 + plugins/Event/newrsvp.php | 218 ++++++++++++++++++------------- plugins/Event/rsvpform.php | 120 +++++++++++++++++ plugins/Event/showevent.php | 4 +- 9 files changed, 638 insertions(+), 99 deletions(-) create mode 100644 plugins/Event/cancelrsvp.php create mode 100644 plugins/Event/cancelrsvpform.php create mode 100644 plugins/Event/rsvpform.php diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index d8d9b572ed..7ca2fa9c0e 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -79,11 +79,14 @@ class EventPlugin extends MicroappPlugin { 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': @@ -109,6 +112,8 @@ class EventPlugin extends MicroappPlugin 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}')); @@ -294,7 +299,7 @@ class EventPlugin extends MicroappPlugin function showRSVPNotice($notice, $out) { - $out->element('span', null, 'RSVP'); + $out->raw($notice->rendered); return; } @@ -312,7 +317,7 @@ class EventPlugin extends MicroappPlugin if (!empty($event->url)) { $out->element('a', - array('href' => $att->url, + array('href' => $event->url, 'class' => 'event-title entry-title summary'), $event->title); } else { @@ -349,6 +354,20 @@ class EventPlugin extends MicroappPlugin 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); diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index b2adb4d9b5..1a6a028dca 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -210,4 +210,10 @@ class Happening extends Managed_DataObject { 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 index 36c6b32ec7..22bd239a68 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -71,13 +71,27 @@ class RSVP extends Managed_DataObject 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' => 'A real-world event', + 'description' => 'Plan to attend event', 'fields' => array( 'id' => array('type' => 'char', 'length' => 36, @@ -107,7 +121,7 @@ class RSVP extends Managed_DataObject ); } - function saveNew($profile, $event, $result, $options) + function saveNew($profile, $event, $result, $options=array()) { if (array_key_exists('uri', $options)) { $other = RSVP::staticGet('uri', $options['uri']); @@ -181,7 +195,7 @@ class RSVP extends Managed_DataObject ($verb == RSVP::NEGATIVE) ? 0 : null; } - function verbFor($code) + static function verbFor($code) { return ($code == 1) ? RSVP::POSITIVE : ($code == 0) ? RSVP::NEGATIVE : null; @@ -189,7 +203,11 @@ class RSVP extends Managed_DataObject function getNotice() { - return Notice::staticGet('uri', $this->uri); + $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) @@ -207,11 +225,15 @@ class RSVP extends Managed_DataObject if ($rsvp->find()) { while ($rsvp->fetch()) { - $verb = $this->verbFor($rsvp->code); + $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/newevent.php b/plugins/Event/newevent.php index 7c0fd0177f..0f5635487b 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -157,6 +157,10 @@ class NeweventAction extends Action $this->description, $this->url); + $event = Happening::fromNotice($saved); + + RSVP::saveNew($profile, $event, RSVP::POSITIVE); + } catch (ClientException $ce) { $this->error = $ce->getMessage(); $this->showPage(); diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php index a793ac6de2..da613ec6c7 100644 --- a/plugins/Event/newrsvp.php +++ b/plugins/Event/newrsvp.php @@ -1,17 +1,11 @@ - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * Copyright (C) 2011, StatusNet, Inc. + * + * RSVP for an event + * + * 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 @@ -25,140 +19,184 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + * + * @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); } /** - * Give a warm greeting to our friendly user + * RSVP for an event * - * This sample action shows some basic ways of doing output in an action - * class. - * - * Action classes have several output methods that they override from - * the parent class. - * - * @category Sample - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ + * @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 HelloAction extends Action + +class NewrsvpAction extends Action { - var $user = null; - var $gc = null; + protected $user = null; + protected $event = null; + protected $type = null; /** - * Take arguments for running + * Returns the title of the action * - * This method is called first, and it lets the action class get - * all its arguments and validate them. It's also the time - * to fetch any relevant data from the database. - * - * Action classes should run parent::prepare($args) as the first - * line of this method to make sure the default argument-processing - * happens. - * - * @param array $args $_REQUEST args - * - * @return boolean success flag + * @return string Action title */ - function prepare($args) + + function title() { - parent::prepare($args); + 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)) { - $this->gc = User_greeting_count::inc($this->user->id); + 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; } /** - * Handle request + * Handler method * - * This is the main method for handling a request. Note that - * most preparation should be done in the prepare() method; - * by the time handle() is called the action should be - * more or less ready to go. - * - * @param array $args $_REQUEST args; handled in prepare() + * @param array $argarray is ignored since it's now passed in in prepare() * * @return void */ - function handle($args) - { - parent::handle($args); - $this->showPage(); + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->newRSVP(); + } else { + $this->showPage(); + } + + return; } /** - * Title of this page + * Add a new event * - * Override this method to show a custom title. - * - * @return string Title of the page + * @return void */ - function title() + + function newRSVP() { - if (empty($this->user)) { - return _m('Hello'); + 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 { - return sprintf(_m('Hello, %s!'), $this->user->nickname); + common_redirect($saved->bestUrl(), 303); } } /** - * Show content in the content area - * - * The default StatusNet page has a lot of decorations: menus, - * logos, tabs, all that jazz. This method is used to show - * content in the content area of the page; it's the main - * thing you want to overload. - * - * This method also demonstrates use of a plural localized string. + * Show the event form * * @return void */ + function showContent() { - if (empty($this->user)) { - $this->element('p', array('class' => 'greeting'), - _m('Hello, stranger!')); - } else { - $this->element('p', array('class' => 'greeting'), - sprintf(_m('Hello, %s'), $this->user->nickname)); - $this->element('p', array('class' => 'greeting_count'), - sprintf(_m('I have greeted you %d time.', - 'I have greeted you %d times.', - $this->gc->greeting_count), - $this->gc->greeting_count)); + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); } + + $form = new RSVPForm($this->event, $this); + + $form->show(); + + return; } /** * Return true if read only. * - * Some actions only read from the database; others read and write. - * The simple database load-balancer built into StatusNet will - * direct read-only actions to database mirrors (if they are configured), - * and read-write actions to the master database. + * MAY override * - * This defaults to false to avoid data integrity issues, but you - * should make sure to overload it for performance gains. - * - * @param array $args other arguments, if RO/RW status depends on them. + * @param array $args other arguments * * @return boolean is read only action? */ + function isReadOnly($args) { - return false; + 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 index f8b032c111..7fb702f9db 100644 --- a/plugins/Event/showevent.php +++ b/plugins/Event/showevent.php @@ -64,13 +64,13 @@ class ShoweventAction extends ShownoticeAction $this->id = $this->trimmed('id'); - $this->event = Event::staticGet('id', $this->id); + $this->event = Happening::staticGet('id', $this->id); if (empty($this->event)) { throw new ClientException(_('No such event.'), 404); } - $this->notice = $event->getNotice(); + $this->notice = $this->event->getNotice(); if (empty($this->notice)) { // Did we used to have it, and it got deleted?