git.gnu.io has moved to IP address 209.51.188.249 -- please double check where you are logging in.

Commit 3438a78c authored by Brion Vibber's avatar Brion Vibber

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.
parent b9e2c727
<?php
/**
* Data class to mark notices as bookmarks
*
* PHP version 5
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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 <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* For storing the poll options and such
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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: <a href="%s">%s</a>'),
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;
}
}
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* A plugin to enable social-bookmarking functionality
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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 <brionv@status.net>
* @author Evan Prodromou <evan@status.net>
* @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"></poll:data>
*
* "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 <poll><option/><option/></poll>
// @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');
}
}
<?php
/**
* Data class to record responses to polls
*
* PHP version 5
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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 <http://www.gnu.org/licenses/>.
*/
if (!defined('STATUSNET')) {
exit(1);
}
/**
* For storing the poll options and such
*
* @category PollPlugin
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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'),
)
);
}
}
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
<?php
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2011, StatusNet, Inc.
*
* Add a new Poll
*
* PHP version 5
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Poll
* @package StatusNet
* @author Brion Vibber <brion@status.net>
* @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 <evan@status.net>
* @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);