3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2011, StatusNet, Inc.
6 * A plugin to enable social-bookmarking functionality
10 * This program is free software: you can redistribute it and/or modify
11 * it under the terms of the GNU Affero General Public License as published by
12 * the Free Software Foundation, either version 3 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU Affero General Public License for more details.
20 * You should have received a copy of the GNU Affero General Public License
21 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 * @category PollPlugin
25 * @author Brion Vibber <brion@status.net>
26 * @copyright 2011 StatusNet, Inc.
27 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
28 * @link http://status.net/
31 if (!defined('STATUSNET')) {
36 * Poll plugin main class
38 * @category PollPlugin
40 * @author Brion Vibber <brionv@status.net>
41 * @author Evan Prodromou <evan@status.net>
42 * @copyright 2011 StatusNet, Inc.
43 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
44 * @link http://status.net/
46 class PollPlugin extends MicroAppPlugin
48 const PLUGIN_VERSION = '0.1.1';
50 // @fixme which domain should we use for these namespaces?
51 const POLL_OBJECT = 'http://activityschema.org/object/poll';
52 const POLL_RESPONSE_OBJECT = 'http://activityschema.org/object/poll-response';
54 public $oldSaveNew = true;
57 * Database schema setup
59 * @return boolean hook value; true means continue processing, false means stop.
64 public function onCheckSchema()
66 $schema = Schema::get();
67 $schema->ensureTable('poll', Poll::schemaDef());
68 $schema->ensureTable('poll_response', Poll_response::schemaDef());
69 $schema->ensureTable('user_poll_prefs', User_poll_prefs::schemaDef());
74 * Show the CSS necessary for this plugin
76 * @param Action $action the action being run
78 * @return boolean hook value
80 public function onEndShowStyles($action)
82 $action->cssLink($this->path('css/poll.css'));
89 * @param URLMapper $m path-to-action mapper
91 * @return boolean hook value; true means continue processing, false means stop.
93 public function onRouterInitialized(URLMapper $m)
97 array('action' => 'newpoll')
102 array('action' => 'showpoll'),
103 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
107 'main/poll/response/:id',
108 array('action' => 'showpollresponse'),
109 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
113 'main/poll/:id/respond',
114 array('action' => 'respondpoll'),
115 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
120 array('action' => 'pollsettings')
127 * Plugin version data
129 * @param array &$versions array of version data
131 * @return bool true hook value
134 public function onPluginVersion(array &$versions)
136 $versions[] = array('name' => 'Poll',
137 'version' => self::PLUGIN_VERSION,
138 'author' => 'Brion Vibber',
139 'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Poll',
141 // TRANS: Plugin description.
142 _m('Simple extension for supporting basic polls.'));
146 public function types()
148 return array(self::POLL_OBJECT, self::POLL_RESPONSE_OBJECT);
152 * When a notice is deleted, delete the related Poll
154 * @param Notice $notice Notice being deleted
156 * @return boolean hook value
158 public function deleteRelated(Notice $notice)
160 $p = Poll::getByNotice($notice);
170 * Save a poll from an activity
172 * @param Activity $activity Activity to save
173 * @param Profile $profile Profile to use as author
174 * @param array $options Options to pass to bookmark-saving code
176 * @return Notice resulting notice
177 * @throws Exception if it failed
179 public function saveNoticeFromActivity(Activity $activity, Profile $profile, array $options = array())
182 common_log(LOG_DEBUG, "XXX activity: " . var_export($activity, true));
183 common_log(LOG_DEBUG, "XXX profile: " . var_export($profile, true));
184 common_log(LOG_DEBUG, "XXX options: " . var_export($options, true));
186 // Ok for now, we can grab stuff from the XML entry directly.
187 // This won't work when reading from JSON source
188 if ($activity->entry) {
189 $pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll');
190 $responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response');
191 if ($pollElements->length) {
195 $data = $pollElements->item(0);
196 foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'question') as $node) {
197 $question = $node->textContent;
199 foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'option') as $node) {
200 $opts[] = $node->textContent;
203 $notice = Poll::saveNew($profile, $question, $opts, $options);
204 common_log(LOG_DEBUG, "Saved Poll from ActivityStream data ok: notice id " . $notice->id);
206 } catch (Exception $e) {
207 common_log(LOG_DEBUG, "Poll save from ActivityStream data failed: " . $e->getMessage());
209 } elseif ($responseElements->length) {
210 $data = $responseElements->item(0);
211 $pollUri = $data->getAttribute('poll');
212 $selection = intval($data->getAttribute('selection'));
215 // TRANS: Exception thrown trying to respond to a poll without a poll reference.
216 throw new Exception(_m('Invalid poll response: No poll reference.'));
218 $poll = Poll::getKV('uri', $pollUri);
220 // TRANS: Exception thrown trying to respond to a non-existing poll.
221 throw new Exception(_m('Invalid poll response: Poll is unknown.'));
224 $notice = Poll_response::saveNew($profile, $poll, $selection, $options);
225 common_log(LOG_DEBUG, "Saved Poll_response ok, notice id: " . $notice->id);
227 } catch (Exception $e) {
228 common_log(LOG_DEBUG, "Poll response save fail: " . $e->getMessage());
229 // TRANS: Exception thrown trying to respond to a non-existing poll.
232 common_log(LOG_DEBUG, "YYY no poll data");
235 // If it didn't return before
236 throw new ServerException(_m('Failed to save Poll response.'));
239 public function activityObjectFromNotice(Notice $notice)
241 assert($this->isMyNotice($notice));
243 switch ($notice->object_type) {
244 case self::POLL_OBJECT:
245 return $this->activityObjectFromNoticePoll($notice);
246 case self::POLL_RESPONSE_OBJECT:
247 return $this->activityObjectFromNoticePollResponse($notice);
249 // TRANS: Exception thrown when performing an unexpected action on a poll.
250 // TRANS: %s is the unexpected object type.
251 throw new Exception(sprintf(_m('Unexpected type for poll plugin: %s.'), $notice->object_type));
255 public function activityObjectFromNoticePollResponse(Notice $notice)
257 $object = new ActivityObject();
258 $object->id = $notice->uri;
259 $object->type = self::POLL_RESPONSE_OBJECT;
260 $object->title = $notice->content;
261 $object->summary = $notice->content;
262 $object->link = $notice->getUrl();
264 $response = Poll_response::getByNotice($notice);
266 $poll = $response->getPoll();
268 // Stash data to be formatted later by
269 // $this->activityObjectOutputAtom() or
270 // $this->activityObjectOutputJson()...
271 $object->pollSelection = intval($response->selection);
272 $object->pollUri = $poll->uri;
278 public function activityObjectFromNoticePoll(Notice $notice)
280 $object = new ActivityObject();
281 $object->id = $notice->uri;
282 $object->type = self::POLL_OBJECT;
283 $object->title = $notice->content;
284 $object->summary = $notice->content;
285 $object->link = $notice->getUrl();
287 $poll = Poll::getByNotice($notice);
289 // Stash data to be formatted later by
290 // $this->activityObjectOutputAtom() or
291 // $this->activityObjectOutputJson()...
292 $object->pollQuestion = $poll->question;
293 $object->pollOptions = $poll->getOptions();
300 * Called when generating Atom XML ActivityStreams output from an
301 * ActivityObject belonging to this plugin. Gives the plugin
302 * a chance to add custom output.
304 * Note that you can only add output of additional XML elements,
305 * not change existing stuff here.
307 * If output is already handled by the base Activity classes,
308 * you can leave this base implementation as a no-op.
310 * @param ActivityObject $obj
311 * @param XMLOutputter $out to add elements at end of object
313 public function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
315 if (isset($obj->pollQuestion)) {
317 * <poll:poll xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
318 * <poll:question>Who wants a poll question?</poll:question>
319 * <poll:option>Option one</poll:option>
320 * <poll:option>Option two</poll:option>
321 * <poll:option>Option three</poll:option>
324 $data = array('xmlns:poll' => self::POLL_OBJECT);
325 $out->elementStart('poll:poll', $data);
326 $out->element('poll:question', array(), $obj->pollQuestion);
327 foreach ($obj->pollOptions as $opt) {
328 $out->element('poll:option', array(), $opt);
330 $out->elementEnd('poll:poll');
332 if (isset($obj->pollSelection)) {
334 * <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
335 * poll="http://..../poll/...."
338 $data = array('xmlns:poll' => self::POLL_OBJECT,
339 'poll' => $obj->pollUri,
340 'selection' => $obj->pollSelection);
341 $out->element('poll:response', $data, '');
346 * Called when generating JSON ActivityStreams output from an
347 * ActivityObject belonging to this plugin. Gives the plugin
348 * a chance to add custom output.
350 * Modify the array contents to your heart's content, and it'll
351 * all get serialized out as JSON.
353 * If output is already handled by the base Activity classes,
354 * you can leave this base implementation as a no-op.
356 * @param ActivityObject $obj
357 * @param array &$out JSON-targeted array which can be modified
359 public function activityObjectOutputJson(ActivityObject $obj, array &$out)
361 common_log(LOG_DEBUG, 'QQQ: ' . var_export($obj, true));
362 if (isset($obj->pollQuestion)) {
365 * "question": "Who wants a poll question?",
373 $data = array('question' => $obj->pollQuestion,
374 'options' => array());
375 foreach ($obj->pollOptions as $opt) {
376 $data['options'][] = $opt;
378 $out['poll'] = $data;
380 if (isset($obj->pollSelection)) {
383 * "poll": "http://..../poll/....",
387 $data = array('poll' => $obj->pollUri,
388 'selection' => $obj->pollSelection);
389 $out['pollResponse'] = $data;
393 public function entryForm($out)
395 return new NewPollForm($out);
398 // @fixme is this from parent?
399 public function tag()
404 public function appTitle()
406 // TRANS: Application title.
407 return _m('APPTITLE', 'Poll');
410 public function onStartAddNoticeReply($nli, $parent, $child)
412 // Filter out any poll responses
413 if ($parent->object_type == self::POLL_OBJECT &&
414 $child->object_type == self::POLL_RESPONSE_OBJECT) {
420 // Hide poll responses for @chuck
422 public function onEndNoticeWhoGets($notice, &$ni)
424 if ($notice->object_type == self::POLL_RESPONSE_OBJECT) {
425 foreach ($ni as $id => $source) {
426 $user = User::getKV('id', $id);
428 $pollPrefs = User_poll_prefs::getKV('user_id', $user->id);
429 if (!empty($pollPrefs) && ($pollPrefs->hide_responses)) {
439 * Menu item for personal subscriptions/groups area
441 * @param Action $action action being executed
443 * @return boolean hook return
446 public function onEndAccountSettingsNav($action)
448 $action_name = $action->trimmed('action');
451 common_local_url('pollsettings'),
452 // TRANS: Poll plugin menu item on user settings page.
454 // TRANS: Poll plugin tooltip for user settings menu item.
455 _m('Configure poll behavior'),
456 $action_name === 'pollsettings'
462 protected function showNoticeContent(Notice $stored, HTMLOutputter $out, Profile $scoped = null)
464 if ($stored->object_type == self::POLL_RESPONSE_OBJECT) {
465 parent::showNoticeContent($stored, $out, $scoped);
469 // If the stored notice is a POLL_OBJECT
470 $poll = Poll::getByNotice($stored);
471 if ($poll instanceof Poll) {
472 if (!$scoped instanceof Profile || $poll->getResponse($scoped) instanceof Poll_response) {
473 // Either the user is not logged in or it has already responded; show the results.
474 $form = new PollResultForm($poll, $out);
476 $form = new PollResponseForm($poll, $out);
480 // TRANS: Error text displayed if no poll data could be found.
481 $out->text(_m('Poll data is missing'));