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);
+ }
+
+}