From 3438a78c023d84c344200b5f2794c37ead83e539 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 7 Mar 2011 21:28:36 -0800 Subject: [PATCH] Initial checkin of Poll plugin: micro-app to post mini polls/surveys from the notice form. This version is fairly basic; votes do not (yet) show a reply, they just got in the table. No pretty graphs for the results yet, just text. The ActivityStream output is temporary and probably should be replaced; the current structures for adding custom data aren't really ready yet (especially since we need to cover JSON and Atom formats, probably pretty differently) Uses similar system as Bookmark for attaching to notices -- saves a custom URI for an alternate action, which we can then pass in and hook back up to our poll object. This can probably do with a little more simplification in the parent MicroAppPlugin class. Currently adds two tables: - poll holds the main poll info: id and URI to associate with the notice, then the question and a text blob with the options. - poll_response records the selections picked by our nice fellows. Hopefully no off-by-one bugs left in the selection, but I give no guarantees. ;) Some todo notes in the README and in doc comments. --- plugins/Poll/Poll.php | 250 +++++++++++++++++++++++++ plugins/Poll/PollPlugin.php | 293 ++++++++++++++++++++++++++++++ plugins/Poll/Poll_response.php | 110 +++++++++++ plugins/Poll/README | 21 +++ plugins/Poll/newpoll.php | 193 ++++++++++++++++++++ plugins/Poll/newpollform.php | 150 +++++++++++++++ plugins/Poll/pollresponseform.php | 135 ++++++++++++++ plugins/Poll/pollresultform.php | 131 +++++++++++++ plugins/Poll/respondpoll.php | 189 +++++++++++++++++++ plugins/Poll/showpoll.php | 111 +++++++++++ 10 files changed, 1583 insertions(+) create mode 100644 plugins/Poll/Poll.php create mode 100644 plugins/Poll/PollPlugin.php create mode 100644 plugins/Poll/Poll_response.php create mode 100644 plugins/Poll/README create mode 100644 plugins/Poll/newpoll.php create mode 100644 plugins/Poll/newpollform.php create mode 100644 plugins/Poll/pollresponseform.php create mode 100644 plugins/Poll/pollresultform.php create mode 100644 plugins/Poll/respondpoll.php create mode 100644 plugins/Poll/showpoll.php diff --git a/plugins/Poll/Poll.php b/plugins/Poll/Poll.php new file mode 100644 index 0000000000..60ec4399fd --- /dev/null +++ b/plugins/Poll/Poll.php @@ -0,0 +1,250 @@ + + * @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); +} + +/** + * For storing the poll options and such + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Poll extends Managed_DataObject +{ + public $__table = 'poll'; // table name + public $id; // char(36) primary key not null -> UUID + public $profile_id; // int -> profile.id + public $question; // text + public $options; // text; newline(?)-delimited + 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 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('Poll', $k, $v); + } + + /** + * Get an instance by compound key + * + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits + * + */ + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Poll', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'Per-notice poll data for Poll plugin', + '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'), + 'question' => array('type' => 'text'), + 'options' => array('type' => 'text'), + 'created' => array('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'poll_uri_key' => array('uri'), + ), + ); + } + + /** + * Get a bookmark based on a notice + * + * @param Notice $notice Notice to check for + * + * @return Poll found poll or null + */ + + function getByNotice($notice) + { + return self::staticGet('uri', $notice->uri); + } + + function getOptions() + { + return explode("\n", $this->options); + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + function bestUrl() + { + return $this->getNotice()->bestUrl(); + } + + /** + * Get the response of a particular user to this poll, if any. + * + * @param Profile $profile + * @return Poll_response object or null + */ + function getResponse(Profile $profile) + { + $pr = new Poll_response(); + $pr->poll_id = $this->id; + $pr->profile_id = $profile->id; + $pr->find(); + if ($pr->fetch()) { + return $pr; + } else { + return null; + } + } + + function countResponses() + { + $pr = new Poll_response(); + $pr->poll_id = $this->id; + $pr->groupBy('selection'); + $pr->selectAdd('count(profile_id) as votes'); + $pr->find(); + + $raw = array(); + while ($pr->fetch()) { + $raw[$pr->selection] = $pr->votes; + } + + $counts = array(); + foreach (array_keys($this->getOptions()) as $key) { + if (isset($raw[$key])) { + $counts[$key] = $raw[$key]; + } else { + $counts[$key] = 0; + } + } + return $counts; + } + + /** + * Save a new poll notice + * + * @param Profile $profile + * @param string $question + * @param array $opts (poll responses) + * + * @return Notice saved notice + */ + + static function saveNew($profile, $question, $opts, $options=null) + { + if (empty($options)) { + $options = array(); + } + + $p = new Poll(); + + $p->id = UUID::gen(); + $p->profile_id = $profile->id; + $p->question = $question; + $p->options = implode("\n", $opts); + + if (array_key_exists('created', $options)) { + $p->created = $options['created']; + } else { + $p->created = common_sql_now(); + } + + if (array_key_exists('uri', $options)) { + $p->uri = $options['uri']; + } else { + $p->uri = common_local_url('showpoll', + array('id' => $p->id)); + } + + $p->insert(); + + $content = sprintf(_m('Poll: %s %s'), + $question, + $p->uri); + $rendered = sprintf(_m('Poll: %s'), + htmlspecialchars($p->uri), + htmlspecialchars($question)); + + $tags = array('poll'); + $replies = array(); + + $options = array_merge(array('urls' => array(), + 'rendered' => $rendered, + 'tags' => $tags, + 'replies' => $replies, + 'object_type' => PollPlugin::POLL_OBJECT), + $options); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $p->uri; + } + + $saved = Notice::saveNew($profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options); + + return $saved; + } +} diff --git a/plugins/Poll/PollPlugin.php b/plugins/Poll/PollPlugin.php new file mode 100644 index 0000000000..6fa95aa0e7 --- /dev/null +++ b/plugins/Poll/PollPlugin.php @@ -0,0 +1,293 @@ +. + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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')) { + exit(1); +} + +/** + * Poll plugin main class + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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 PollPlugin extends MicroAppPlugin +{ + const VERSION = '0.1'; + const POLL_OBJECT = 'http://apinamespace.org/activitystreams/object/poll'; + + /** + * Database schema setup + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('poll', Poll::schemaDef()); + $schema->ensureTable('poll_response', Poll_response::schemaDef()); + return true; + } + + /** + * Show the CSS necessary for this plugin + * + * @param Action $action the action being run + * + * @return boolean hook value + */ + + function onEndShowStyles($action) + { + $action->cssLink($this->path('poll.css')); + return true; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'ShowpollAction': + case 'NewpollAction': + case 'RespondpollAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'Poll': + case 'Poll_response': + include_once $dir.'/'.$cls.'.php'; + return false; + case 'NewPollForm': + case 'PollResponseForm': + case 'PollResultForm': + include_once $dir.'/'.strtolower($cls).'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * @param Net_URL_Mapper $m path-to-action mapper + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onRouterInitialized($m) + { + $m->connect('main/poll/new', + array('action' => 'newpoll'), + array('id' => '[0-9]+')); + + $m->connect('main/poll/:id/respond', + array('action' => 'respondpoll'), + array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + + return true; + } + + /** + * Plugin version data + * + * @param array &$versions array of version data + * + * @return value + */ + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Poll', + 'version' => self::VERSION, + 'author' => 'Brion Vibber', + 'homepage' => 'http://status.net/wiki/Plugin:Poll', + 'rawdescription' => + _m('Simple extension for supporting basic polls.')); + return true; + } + + function types() + { + return array(self::POLL_OBJECT); + } + + /** + * When a notice is deleted, delete the related Poll + * + * @param Notice $notice Notice being deleted + * + * @return boolean hook value + */ + + function deleteRelated($notice) + { + $p = Poll::getByNotice($notice); + + if (!empty($p)) { + $p->delete(); + } + + return true; + } + + /** + * Save a poll from an activity + * + * @param Profile $profile Profile to use as author + * @param Activity $activity Activity to save + * @param array $options Options to pass to bookmark-saving code + * + * @return Notice resulting notice + */ + + function saveNoticeFromActivity($activity, $profile, $options=array()) + { + // @fixme + } + + function activityObjectFromNotice($notice) + { + assert($this->isMyNotice($notice)); + + $object = new ActivityObject(); + $object->id = $notice->uri; + $object->type = self::POLL_OBJECT; + $object->title = 'Poll title'; + $object->summary = 'Poll summary'; + $object->link = $notice->bestUrl(); + + $poll = Poll::getByNotice($notice); + /** + * Adding the poll-specific data. There's no standard in AS for polls, + * so we're making stuff up. + * + * For the moment, using a kind of icky-looking schema that happens to + * work with out code for generating both Atom and JSON forms, though + * I don't like it: + * + * + * + * "poll:data": { + * "xmlns:poll": http://apinamespace.org/activitystreams/object/poll + * "question": "Who wants a poll question?" + * "option1": "Option one" + * "option2": "Option two" + * "option3": "Option three" + * } + * + */ + // @fixme there's no way to specify an XML node tree here, like + // @fixme there's no way to specify a JSON array or multi-level tree unless you break the XML attribs + // @fixme XML node contents don't get shown in JSON + $data = array('xmlns:poll' => self::POLL_OBJECT, + 'question' => $poll->question); + foreach ($poll->getOptions() as $i => $opt) { + $data['option' . ($i + 1)] = $opt; + } + $object->extra[] = array('poll:data', $data, ''); + return $object; + } + + /** + * @fixme WARNING WARNING WARNING parent class closes the final div that we + * open here, but we probably shouldn't open it here. Check parent class + * and Bookmark plugin for if that's right. + */ + function showNotice($notice, $out) + { + $user = common_current_user(); + + // @hack we want regular rendering, then just add stuff after that + $nli = new NoticeListItem($notice, $out); + $nli->showNotice(); + + $out->elementStart('div', array('class' => 'entry-content poll-content')); + $poll = Poll::getByNotice($notice); + if ($poll) { + if ($user) { + $profile = $user->getProfile(); + $response = $poll->getResponse($profile); + if ($response) { + // User has already responded; show the results. + $form = new PollResultForm($poll, $out); + } else { + $form = new PollResponseForm($poll, $out); + } + $form->show(); + } + } else { + $out->text('Poll data is missing'); + } + $out->elementEnd('div'); + + // @fixme + $out->elementStart('div', array('class' => 'entry-content')); + } + + function entryForm($out) + { + return new NewPollForm($out); + } + + // @fixme is this from parent? + function tag() + { + return 'poll'; + } + + function appTitle() + { + return _m('Poll'); + } +} diff --git a/plugins/Poll/Poll_response.php b/plugins/Poll/Poll_response.php new file mode 100644 index 0000000000..44bc421c22 --- /dev/null +++ b/plugins/Poll/Poll_response.php @@ -0,0 +1,110 @@ + + * @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); +} + +/** + * For storing the poll options and such + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Poll_response extends Managed_DataObject +{ + public $__table = 'poll_response'; // table name + public $poll_id; // char(36) primary key not null -> UUID + public $profile_id; // int -> profile.id + public $selection; // int -> choice # + 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 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('Poll_response', $k, $v); + } + + /** + * Get an instance by compound key + * + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits + * + */ + + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Poll_response', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'Record of responses to polls', + 'fields' => array( + 'poll_id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID'), + 'profile_id' => array('type' => 'int'), + 'selection' => array('type' => 'int'), + 'created' => array('type' => 'datetime', 'not null' => true), + ), + 'unique keys' => array( + 'poll_response_poll_id_profile_id_key' => array('poll_id', 'profile_id'), + ), + 'indexes' => array( + 'poll_response_profile_id_poll_id_index' => array('profile_id', 'poll_id'), + ) + ); + } +} diff --git a/plugins/Poll/README b/plugins/Poll/README new file mode 100644 index 0000000000..cd91a03945 --- /dev/null +++ b/plugins/Poll/README @@ -0,0 +1,21 @@ +Unfinished basic stuff: +* make pretty graphs for response counts +* ActivityStreams output of poll data is temporary; the interfaces need more flexibility +* ActivityStreams input not done yet +* need link -> show results in addition to showing results if you already voted +* way to change/cancel your vote + +Known issues: +* HTTP caching needs fixing on show-poll; may show you old data if you voted after + +Things todo: +* should we allow anonymous responses? or ways for remote profiles to respond locally? + +Fancier things todo: +* make sure backup/restore work +* make sure ostatus transfer works +* a way to do poll responses over ostatus directly? +* allow links, tags, @-references in poll question & answers? or not? + +Storage todo: +* probably separate the options into a table instead of squishing them in a text blob diff --git a/plugins/Poll/newpoll.php b/plugins/Poll/newpoll.php new file mode 100644 index 0000000000..66386affa9 --- /dev/null +++ b/plugins/Poll/newpoll.php @@ -0,0 +1,193 @@ +. + * + * @category Poll + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Add a new Poll + * + * @category Poll + * @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 NewPollAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $question = null; + protected $options = array(); + + /** + * Returns the title of the action + * + * @return string Action title + */ + + function title() + { + return _('New poll'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $this->user = common_current_user(); + + if (empty($this->user)) { + throw new ClientException(_("Must be logged in to post a poll."), + 403); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + $this->question = $this->trimmed('question'); + for ($i = 1; $i < 20; $i++) { + $opt = $this->trimmed('option' . $i); + if ($opt != '') { + $this->options[] = $opt; + } + } + + 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->newPoll(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new Poll + * + * @return void + */ + + function newPoll() + { + try { + if (empty($this->question)) { + throw new ClientException(_('Poll must have a question.')); + } + + if (count($this->options) < 2) { + throw new ClientException(_('Poll must have at least two options.')); + } + + + $saved = Poll::saveNew($this->user->getProfile(), + $this->question, + $this->options); + + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + common_redirect($saved->bestUrl(), 303); + } + + /** + * Show the Poll form + * + * @return void + */ + + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new NewPollForm($this, + $this->questions, + $this->options); + + $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/Poll/newpollform.php b/plugins/Poll/newpollform.php new file mode 100644 index 0000000000..fd5f28748b --- /dev/null +++ b/plugins/Poll/newpollform.php @@ -0,0 +1,150 @@ +. + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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 to add a new poll thingy + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class NewpollForm extends Form +{ + + protected $question = null; + protected $options = array(); + + /** + * Construct a new poll form + * + * @param HTMLOutputter $out output channel + * + * @return void + */ + + function __construct($out=null, $question=null, $options=null) + { + parent::__construct($out); + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'newpoll-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('newpoll'); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'newpoll-data')); + $this->out->elementStart('ul', 'form_data'); + + $this->li(); + $this->out->input('question', + _m('Question'), + $this->question, + _m('What question are people answering?')); + $this->unli(); + + $max = 5; + if (count($this->options) + 1 > $max) { + $max = count($this->options) + 2; + } + for ($i = 0; $i < $max; $i++) { + // @fixme make extensible + if (isset($this->options[$i])) { + $default = $this->options[$i]; + } else { + $default = ''; + } + $this->li(); + $this->out->input('option' . ($i + 1), + sprintf(_m('Option %d'), $i + 1), + $default); + $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/Poll/pollresponseform.php b/plugins/Poll/pollresponseform.php new file mode 100644 index 0000000000..87340f926f --- /dev/null +++ b/plugins/Poll/pollresponseform.php @@ -0,0 +1,135 @@ +. + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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 to add a new poll thingy + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class PollResponseForm extends Form +{ + protected $poll; + + /** + * Construct a new poll form + * + * @param Poll $poll + * @param HTMLOutputter $out output channel + * + * @return void + */ + + function __construct(Poll $poll, HTMLOutputter $out) + { + parent::__construct($out); + $this->poll = $poll; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'pollresponse-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('respondpoll', array('id' => $this->poll->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $poll = $this->poll; + $out = $this->out; + $id = "poll-" . $poll->id; + + $out->element('p', 'poll-question', $poll->question); + $out->elementStart('ul', 'poll-options'); + foreach ($poll->getOptions() as $i => $opt) { + $out->elementStart('li'); + $out->elementStart('label'); + $out->element('input', array('type' => 'radio', 'name' => 'pollselection', 'value' => $i + 1), ''); + $out->text(' ' . $opt); + $out->elementEnd('label'); + $out->elementEnd('li'); + } + $out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _m('BUTTON', 'Submit')); + } +} diff --git a/plugins/Poll/pollresultform.php b/plugins/Poll/pollresultform.php new file mode 100644 index 0000000000..eace105e0d --- /dev/null +++ b/plugins/Poll/pollresultform.php @@ -0,0 +1,131 @@ +. + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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 to add a new poll thingy + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class PollResultForm extends Form +{ + protected $poll; + + /** + * Construct a new poll form + * + * @param Poll $poll + * @param HTMLOutputter $out output channel + * + * @return void + */ + + function __construct(Poll $poll, HTMLOutputter $out) + { + parent::__construct($out); + $this->poll = $poll; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + + function id() + { + return 'pollresult-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + + function formClass() + { + return 'form_settings'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + + function action() + { + return common_local_url('respondpoll', array('id' => $this->poll->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + $poll = $this->poll; + $out = $this->out; + $counts = $poll->countResponses(); + + $out->element('p', 'poll-question', $poll->question); + $out->elementStart('ul', 'poll-options'); + foreach ($poll->getOptions() as $i => $opt) { + $out->elementStart('li'); + $out->text($counts[$i] . ' ' . $opt); + $out->elementEnd('li'); + } + $out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + } +} diff --git a/plugins/Poll/respondpoll.php b/plugins/Poll/respondpoll.php new file mode 100644 index 0000000000..8ae31443fc --- /dev/null +++ b/plugins/Poll/respondpoll.php @@ -0,0 +1,189 @@ +. + * + * @category Poll + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +if (!defined('STATUSNET')) { + // This check helps protect against security problems; + // your code file can't be executed directly from the web. + exit(1); +} + +/** + * Add a new Poll + * + * @category Poll + * @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 RespondPollAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $poll = null; + protected $selection = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + + function title() + { + return _m('Poll response'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $this->user = common_current_user(); + + if (empty($this->user)) { + throw new ClientException(_m("Must be logged in to respond to a poll."), + 403); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + $id = $this->trimmed('id'); + $this->poll = Poll::staticGet('id', $id); + if (empty($this->poll)) { + throw new ClientException(_m("Invalid or missing poll."), 404); + } + + $selection = intval($this->trimmed('pollselection')); + if ($selection < 1 || $selection > count($this->poll->getOptions())) { + throw new ClientException(_m('Invalid poll selection.')); + } + $this->selection = $selection; + + 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->respondPoll(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new Poll + * + * @return void + */ + + function respondPoll() + { + try { + $response = new Poll_response(); + $response->poll_id = $this->poll->id; + $response->profile_id = $this->user->id; + $response->selection = $this->selection; + $response->created = common_sql_now(); + $response->insert(); + + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + common_redirect($this->poll->bestUrl(), 303); + } + + /** + * Show the Poll form + * + * @return void + */ + + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new PollResponseForm($this->poll, $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/Poll/showpoll.php b/plugins/Poll/showpoll.php new file mode 100644 index 0000000000..f5002701a2 --- /dev/null +++ b/plugins/Poll/showpoll.php @@ -0,0 +1,111 @@ +. + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @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 Poll, with associated information + * + * @category PollPlugin + * @package StatusNet + * @author Brion Vibber + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ShowPollAction extends ShownoticeAction +{ + protected $poll = 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->poll = Poll::staticGet('id', $this->id); + + if (empty($this->poll)) { + throw new ClientException(_m('No such poll.'), 404); + } + + $this->notice = $this->poll->getNotice(); + + if (empty($this->notice)) { + // Did we used to have it, and it got deleted? + throw new ClientException(_m('No such poll notice.'), 404); + } + + $this->user = User::staticGet('id', $this->poll->profile_id); + + if (empty($this->user)) { + throw new ClientException(_m('No such user.'), 404); + } + + $this->profile = $this->user->getProfile(); + + if (empty($this->profile)) { + throw new ServerException(_m('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 poll: %s'), + $this->user->nickname, + $this->poll->question); + } + +}