3 * StatusNet - the distributed open-source microblogging tool
4 * Copyright (C) 2011, StatusNet, Inc.
6 * Microapp plugin for Questions and Answers
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/>.
25 * @author Zach Copley <zach@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')) {
32 // This check helps protect against security problems;
33 // your code file can't be executed directly from the web.
38 * Question and Answer plugin
42 * @author Zach Copley <zach@status.net>
43 * @copyright 2011 StatusNet, Inc.
44 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
45 * @link http://status.net/
47 class QnAPlugin extends MicroAppPlugin
49 const PLUGIN_VERSION = '2.0.0';
51 var $oldSaveNew = true;
54 * Set up our tables (question and answer)
59 * @return boolean hook value; true means continue processing, false means stop.
61 function onCheckSchema()
63 $schema = Schema::get();
65 $schema->ensureTable('qna_question', QnA_Question::schemaDef());
66 $schema->ensureTable('qna_answer', QnA_Answer::schemaDef());
67 $schema->ensureTable('qna_vote', QnA_Vote::schemaDef());
72 public function newFormAction() {
73 return 'qnanewquestion';
79 * @param URLMapper $m path-to-action mapper
81 * @return boolean hook value; true means continue processing, false means stop.
84 public function onRouterInitialized(URLMapper $m)
86 $UUIDregex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
88 $m->connect('main/qna/newquestion',
89 ['action' => 'qnanewquestion']);
91 $m->connect('answer/qna/closequestion',
92 ['action' => 'qnaclosequestion']);
94 $m->connect('main/qna/newanswer',
95 ['action' => 'qnanewanswer']);
97 $m->connect('main/qna/reviseanswer',
98 ['action' => 'qnareviseanswer']);
100 $m->connect('question/vote/:id',
101 ['action' => 'qnavote',
102 'type' => 'question'],
103 ['id' => $UUIDregex]);
105 $m->connect('question/:id',
106 ['action' => 'qnashowquestion'],
107 ['id' => $UUIDregex]);
109 $m->connect('answer/vote/:id',
110 ['action' => 'qnavote',
112 ['id' => $UUIDregex]);
114 $m->connect('answer/:id',
115 ['action' => 'qnashowanswer'],
116 ['id' => $UUIDregex]);
121 function onPluginVersion(array &$versions)
125 'version' => self::PLUGIN_VERSION,
126 'author' => 'Zach Copley',
127 'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/QnA',
129 // TRANS: Plugin description.
130 _m('Question and Answers micro-app.')
135 function appTitle() {
136 // TRANS: Application title.
137 return _m('TITLE','Question');
146 QnA_Question::OBJECT_TYPE,
147 QnA_Answer::OBJECT_TYPE
152 * Given a parsed ActivityStreams activity, save it into a notice
153 * and other data structures.
155 * @param Activity $activity
156 * @param Profile $actor
157 * @param array $options=array()
159 * @return Notice the resulting notice
161 function saveNoticeFromActivity(Activity $activity, Profile $actor, array $options=array())
163 if (count($activity->objects) != 1) {
164 // TRANS: Exception thrown when there are too many activity objects.
165 throw new Exception(_m('Too many activity objects.'));
168 $questionObj = $activity->objects[0];
170 if ($questionObj->type != QnA_Question::OBJECT_TYPE) {
171 // TRANS: Exception thrown when an incorrect object type is encountered.
172 throw new Exception(_m('Wrong type for object.'));
177 switch ($activity->verb) {
178 case ActivityVerb::POST:
179 $notice = QnA_Question::saveNew(
182 $questionObj->summary,
186 case Answer::ObjectType:
187 $question = QnA_Question::getKV('uri', $questionObj->id);
188 if (empty($question)) {
189 // FIXME: save the question
190 // TRANS: Exception thrown when answering a non-existing question.
191 throw new Exception(_m('Answer to unknown question.'));
193 $notice = QnA_Answer::saveNew($actor, $question, $options);
196 // TRANS: Exception thrown when an object type is encountered that cannot be handled.
197 throw new Exception(_m('Unknown object type.'));
204 * Turn a Notice into an activity object
206 * @param Notice $notice
208 * @return ActivityObject
211 function activityObjectFromNotice(Notice $notice)
215 switch ($notice->object_type) {
216 case QnA_Question::OBJECT_TYPE:
217 $question = QnA_Question::fromNotice($notice);
219 case QnA_Answer::OBJECT_TYPE:
220 $answer = QnA_Answer::fromNotice($notice);
221 $question = $answer->getQuestion();
225 if (empty($question)) {
226 // TRANS: Exception thrown when an object type is encountered that cannot be handled.
227 throw new Exception(_m('Unknown object type.'));
230 $notice = $question->getNotice();
232 if (empty($notice)) {
233 // TRANS: Exception thrown when requesting a non-existing question notice.
234 throw new Exception(_m('Unknown question notice.'));
237 $obj = new ActivityObject();
239 $obj->id = $question->uri;
240 $obj->type = QnA_Question::OBJECT_TYPE;
241 $obj->title = $question->title;
242 $obj->link = $notice->getUrl();
244 // XXX: probably need other stuff here
250 * Output our CSS class for QnA notice list elements
252 * @param NoticeListItem $nli The item being shown
254 * @return boolean hook value
257 function onStartOpenNoticeListItemElement(NoticeListItem $nli)
259 $type = $nli->notice->object_type;
263 case QnA_Question::OBJECT_TYPE:
264 $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
265 $class = 'h-entry notice question';
266 if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
267 $class .= ' limited-scope';
270 $question = QnA_Question::getKV('uri', $nli->notice->uri);
272 if (!empty($question->closed)) {
276 $nli->out->elementStart(
279 'id' => 'notice-' . $id
282 Event::handle('EndOpenNoticeListItemElement', array($nli));
285 case QnA_Answer::OBJECT_TYPE:
286 $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
288 $cls = array('h-entry', 'notice', 'answer');
290 $answer = QnA_Answer::getKV('uri', $nli->notice->uri);
292 if (!empty($answer) && !empty($answer->best)) {
296 $nli->out->elementStart(
299 'class' => implode(' ', $cls),
300 'id' => 'notice-' . $id
303 Event::handle('EndOpenNoticeListItemElement', array($nli));
314 * Output the HTML for this kind of object in a list
316 * @param NoticeListItem $nli The list item being shown.
318 * @return boolean hook value
320 * @todo FIXME: WARNING WARNING WARNING this closes a 'div' that is implicitly opened in BookmarkPlugin's showNotice implementation
322 function onStartShowNoticeItem(NoticeListItem $nli)
324 if (!$this->isMyNotice($nli->notice)) {
329 $notice = $nli->notice;
331 $nli->showNotice($notice, $out);
333 $nli->showNoticeLink();
334 $nli->showNoticeSource();
335 $nli->showNoticeLocation();
336 $nli->showPermalink();
338 $nli->showNoticeOptions();
340 if ($notice->object_type == QnA_Question::OBJECT_TYPE) {
341 $user = common_current_user();
342 $question = QnA_Question::getByNotice($notice);
344 if (!empty($user) and !empty($question)) {
345 $profile = $user->getProfile();
346 $answer = $question->getAnswer($profile);
348 // Output a placeholder input -- clicking on it will
349 // bring up a real answer form
351 // NOTE: this whole ul is just a placeholder
352 if (empty($question->closed) && empty($answer)) {
353 $out->elementStart('ul', 'notices qna-dummy');
354 $out->elementStart('li', 'qna-dummy-placeholder');
358 'class' => 'placeholder',
359 // TRANS: Placeholder value for a possible answer to a question
360 // TRANS: by the logged in user.
361 'value' => _m('Your answer...')
364 $out->elementEnd('li');
365 $out->elementEnd('ul');
373 function adaptNoticeListItem($nli) {
374 return new QnAListItem($nli);
377 static function shorten($content, $notice)
381 if (Notice::contentTooLong($content)) {
382 common_debug("content too long");
383 $max = Notice::maxContent();
384 // TRANS: Link description for link to full notice text if it is longer than
385 // TRANS: what will be dispplayed.
387 $short = mb_substr($content, 0, $max - 1);
388 $short .= sprintf('<a href="%1$s" rel="more" title="%2$s">%3$s</a>',
390 // TRANS: Title for link that is an ellipsis in English.
403 * @param HTMLOutputter $out
406 function entryForm($out)
408 return new QnanewquestionForm($out);
412 * When a notice is deleted, clean up related tables.
414 * @param Notice $notice
416 function deleteRelated(Notice $notice)
418 switch ($notice->object_type) {
419 case QnA_Question::OBJECT_TYPE:
420 common_log(LOG_DEBUG, "Deleting question from notice...");
421 $question = QnA_Question::fromNotice($notice);
424 case QnA_Answer::OBJECT_TYPE:
425 common_log(LOG_DEBUG, "Deleting answer from notice...");
426 $answer = QnA_Answer::fromNotice($notice);
427 common_log(LOG_DEBUG, "to delete: $answer->id");
431 common_log(LOG_DEBUG, "Not deleting related, wtf...");
435 function onEndShowScripts($action)
437 $action->script($this->path('js/qna.js'));
441 function onEndShowStyles($action)
443 $action->cssLink($this->path('css/qna.css'));