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 VERSION = '0.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';
55 * Database schema setup
60 * @return boolean hook value; true means continue processing, false means stop.
62 function onCheckSchema()
64 $schema = Schema::get();
65 $schema->ensureTable('poll', Poll::schemaDef());
66 $schema->ensureTable('poll_response', Poll_response::schemaDef());
67 $schema->ensureTable('user_poll_prefs', User_poll_prefs::schemaDef());
72 * Show the CSS necessary for this plugin
74 * @param Action $action the action being run
76 * @return boolean hook value
78 function onEndShowStyles($action)
80 $action->cssLink($this->path('poll.css'));
87 * @param Net_URL_Mapper $m path-to-action mapper
89 * @return boolean hook value; true means continue processing, false means stop.
91 function onRouterInitialized($m)
93 $m->connect('main/poll/new',
94 array('action' => 'newpoll'));
96 $m->connect('main/poll/:id',
97 array('action' => 'showpoll'),
98 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
100 $m->connect('main/poll/response/:id',
101 array('action' => 'showpollresponse'),
102 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
104 $m->connect('main/poll/:id/respond',
105 array('action' => 'respondpoll'),
106 array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
108 $m->connect('settings/poll',
109 array('action' => 'pollsettings'));
115 * Plugin version data
117 * @param array &$versions array of version data
121 function onPluginVersion(&$versions)
123 $versions[] = array('name' => 'Poll',
124 'version' => self::VERSION,
125 'author' => 'Brion Vibber',
126 'homepage' => 'http://status.net/wiki/Plugin:Poll',
128 // TRANS: Plugin description.
129 _m('Simple extension for supporting basic polls.'));
135 return array(self::POLL_OBJECT, self::POLL_RESPONSE_OBJECT);
139 * When a notice is deleted, delete the related Poll
141 * @param Notice $notice Notice being deleted
143 * @return boolean hook value
145 function deleteRelated($notice)
147 $p = Poll::getByNotice($notice);
157 * Save a poll from an activity
159 * @param Profile $profile Profile to use as author
160 * @param Activity $activity Activity to save
161 * @param array $options Options to pass to bookmark-saving code
163 * @return Notice resulting notice
165 function saveNoticeFromActivity($activity, $profile, $options=array())
168 common_log(LOG_DEBUG, "XXX activity: " . var_export($activity, true));
169 common_log(LOG_DEBUG, "XXX profile: " . var_export($profile, true));
170 common_log(LOG_DEBUG, "XXX options: " . var_export($options, true));
172 // Ok for now, we can grab stuff from the XML entry directly.
173 // This won't work when reading from JSON source
174 if ($activity->entry) {
175 $pollElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'poll');
176 $responseElements = $activity->entry->getElementsByTagNameNS(self::POLL_OBJECT, 'response');
177 if ($pollElements->length) {
181 $data = $pollElements->item(0);
182 foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'question') as $node) {
183 $question = $node->textContent;
185 foreach ($data->getElementsByTagNameNS(self::POLL_OBJECT, 'option') as $node) {
186 $opts[] = $node->textContent;
189 $notice = Poll::saveNew($profile, $question, $opts, $options);
190 common_log(LOG_DEBUG, "Saved Poll from ActivityStream data ok: notice id " . $notice->id);
192 } catch (Exception $e) {
193 common_log(LOG_DEBUG, "Poll save from ActivityStream data failed: " . $e->getMessage());
195 } else if ($responseElements->length) {
196 $data = $responseElements->item(0);
197 $pollUri = $data->getAttribute('poll');
198 $selection = intval($data->getAttribute('selection'));
201 // TRANS: Exception thrown trying to respond to a poll without a poll reference.
202 throw new Exception(_m('Invalid poll response: No poll reference.'));
204 $poll = Poll::getKV('uri', $pollUri);
206 // TRANS: Exception thrown trying to respond to a non-existing poll.
207 throw new Exception(_m('Invalid poll response: Poll is unknown.'));
210 $notice = Poll_response::saveNew($profile, $poll, $selection, $options);
211 common_log(LOG_DEBUG, "Saved Poll_response ok, notice id: " . $notice->id);
213 } catch (Exception $e) {
214 common_log(LOG_DEBUG, "Poll response save fail: " . $e->getMessage());
217 common_log(LOG_DEBUG, "YYY no poll data");
222 function activityObjectFromNotice($notice)
224 assert($this->isMyNotice($notice));
226 switch ($notice->object_type) {
227 case self::POLL_OBJECT:
228 return $this->activityObjectFromNoticePoll($notice);
229 case self::POLL_RESPONSE_OBJECT:
230 return $this->activityObjectFromNoticePollResponse($notice);
232 // TRANS: Exception thrown when performing an unexpected action on a poll.
233 // TRANS: %s is the unexpected object type.
234 throw new Exception(sprintf(_m('Unexpected type for poll plugin: %s.'), $notice->object_type));
238 function activityObjectFromNoticePollResponse($notice)
240 $object = new ActivityObject();
241 $object->id = $notice->uri;
242 $object->type = self::POLL_RESPONSE_OBJECT;
243 $object->title = $notice->content;
244 $object->summary = $notice->content;
245 $object->link = $notice->bestUrl();
247 $response = Poll_response::getByNotice($notice);
249 $poll = $response->getPoll();
251 // Stash data to be formatted later by
252 // $this->activityObjectOutputAtom() or
253 // $this->activityObjectOutputJson()...
254 $object->pollSelection = intval($response->selection);
255 $object->pollUri = $poll->uri;
261 function activityObjectFromNoticePoll($notice)
263 $object = new ActivityObject();
264 $object->id = $notice->uri;
265 $object->type = self::POLL_OBJECT;
266 $object->title = $notice->content;
267 $object->summary = $notice->content;
268 $object->link = $notice->bestUrl();
270 $poll = Poll::getByNotice($notice);
272 // Stash data to be formatted later by
273 // $this->activityObjectOutputAtom() or
274 // $this->activityObjectOutputJson()...
275 $object->pollQuestion = $poll->question;
276 $object->pollOptions = $poll->getOptions();
283 * Called when generating Atom XML ActivityStreams output from an
284 * ActivityObject belonging to this plugin. Gives the plugin
285 * a chance to add custom output.
287 * Note that you can only add output of additional XML elements,
288 * not change existing stuff here.
290 * If output is already handled by the base Activity classes,
291 * you can leave this base implementation as a no-op.
293 * @param ActivityObject $obj
294 * @param XMLOutputter $out to add elements at end of object
296 function activityObjectOutputAtom(ActivityObject $obj, XMLOutputter $out)
298 if (isset($obj->pollQuestion)) {
300 * <poll:poll xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
301 * <poll:question>Who wants a poll question?</poll:question>
302 * <poll:option>Option one</poll:option>
303 * <poll:option>Option two</poll:option>
304 * <poll:option>Option three</poll:option>
307 $data = array('xmlns:poll' => self::POLL_OBJECT);
308 $out->elementStart('poll:poll', $data);
309 $out->element('poll:question', array(), $obj->pollQuestion);
310 foreach ($obj->pollOptions as $opt) {
311 $out->element('poll:option', array(), $opt);
313 $out->elementEnd('poll:poll');
315 if (isset($obj->pollSelection)) {
317 * <poll:response xmlns:poll="http://apinamespace.org/activitystreams/object/poll">
318 * poll="http://..../poll/...."
321 $data = array('xmlns:poll' => self::POLL_OBJECT,
322 'poll' => $obj->pollUri,
323 'selection' => $obj->pollSelection);
324 $out->element('poll:response', $data, '');
329 * Called when generating JSON ActivityStreams output from an
330 * ActivityObject belonging to this plugin. Gives the plugin
331 * a chance to add custom output.
333 * Modify the array contents to your heart's content, and it'll
334 * all get serialized out as JSON.
336 * If output is already handled by the base Activity classes,
337 * you can leave this base implementation as a no-op.
339 * @param ActivityObject $obj
340 * @param array &$out JSON-targeted array which can be modified
342 public function activityObjectOutputJson(ActivityObject $obj, array &$out)
344 common_log(LOG_DEBUG, 'QQQ: ' . var_export($obj, true));
345 if (isset($obj->pollQuestion)) {
348 * "question": "Who wants a poll question?",
356 $data = array('question' => $obj->pollQuestion,
357 'options' => array());
358 foreach ($obj->pollOptions as $opt) {
359 $data['options'][] = $opt;
361 $out['poll'] = $data;
363 if (isset($obj->pollSelection)) {
366 * "poll": "http://..../poll/....",
370 $data = array('poll' => $obj->pollUri,
371 'selection' => $obj->pollSelection);
372 $out['pollResponse'] = $data;
378 * @fixme WARNING WARNING WARNING parent class closes the final div that we
379 * open here, but we probably shouldn't open it here. Check parent class
380 * and Bookmark plugin for if that's right.
382 function showNotice($notice, $out)
384 switch ($notice->object_type) {
385 case self::POLL_OBJECT:
386 return $this->showNoticePoll($notice, $out);
387 case self::POLL_RESPONSE_OBJECT:
388 return $this->showNoticePollResponse($notice, $out);
390 // TRANS: Exception thrown when performing an unexpected action on a poll.
391 // TRANS: %s is the unexpected object type.
392 throw new Exception(sprintf(_m('Unexpected type for poll plugin: %s.'), $notice->object_type));
396 function showNoticePoll($notice, $out)
398 $user = common_current_user();
400 // @hack we want regular rendering, then just add stuff after that
401 $nli = new NoticeListItem($notice, $out);
404 $out->elementStart('div', array('class' => 'entry-content poll-content'));
405 $poll = Poll::getByNotice($notice);
408 $profile = $user->getProfile();
409 $response = $poll->getResponse($profile);
411 // User has already responded; show the results.
412 $form = new PollResultForm($poll, $out);
414 $form = new PollResponseForm($poll, $out);
419 // TRANS: Error text displayed if no poll data could be found.
420 $out->text(_m('Poll data is missing'));
422 $out->elementEnd('div');
425 $out->elementStart('div', array('class' => 'entry-content'));
428 function showNoticePollResponse($notice, $out)
430 $user = common_current_user();
432 // @hack we want regular rendering, then just add stuff after that
433 $nli = new NoticeListItem($notice, $out);
437 $out->elementStart('div', array('class' => 'entry-content'));
440 function entryForm($out)
442 return new NewPollForm($out);
445 // @fixme is this from parent?
453 // TRANS: Application title.
454 return _m('APPTITLE','Poll');
457 function onStartAddNoticeReply($nli, $parent, $child)
459 // Filter out any poll responses
460 if ($parent->object_type == self::POLL_OBJECT &&
461 $child->object_type == self::POLL_RESPONSE_OBJECT) {
467 // Hide poll responses for @chuck
469 function onEndNoticeWhoGets($notice, &$ni) {
470 if ($notice->object_type == self::POLL_RESPONSE_OBJECT) {
471 foreach ($ni as $id => $source) {
472 $user = User::getKV('id', $id);
474 $pollPrefs = User_poll_prefs::getKV('user_id', $user->id);
475 if (!empty($pollPrefs) && ($pollPrefs->hide_responses)) {
485 * Menu item for personal subscriptions/groups area
487 * @param Action $action action being executed
489 * @return boolean hook return
492 function onEndAccountSettingsNav($action)
494 $action_name = $action->trimmed('action');
496 $action->menuItem(common_local_url('pollsettings'),
497 // TRANS: Poll plugin menu item on user settings page.
499 // TRANS: Poll plugin tooltip for user settings menu item.
500 _m('Configure poll behavior'),
501 $action_name === 'pollsettings');