]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/QnA/QnAPlugin.php
Merge branch 'master' into FeedPoller
[quix0rs-gnu-social.git] / plugins / QnA / QnAPlugin.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2011, StatusNet, Inc.
5  *
6  * Microapp plugin for Questions and Answers
7  *
8  * PHP version 5
9  *
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.
14  *
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.
19  *
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/>.
22  *
23  * @category  QnA
24  * @package   StatusNet
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/
29  */
30
31 if (!defined('STATUSNET')) {
32     // This check helps protect against security problems;
33     // your code file can't be executed directly from the web.
34     exit(1);
35 }
36
37 /**
38  * Question and Answer plugin
39  *
40  * @category  Plugin
41  * @package   StatusNet
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/
46  */
47 class QnAPlugin extends MicroAppPlugin
48 {
49     /**
50      * Set up our tables (question and answer)
51      *
52      * @see Schema
53      * @see ColumnDef
54      *
55      * @return boolean hook value; true means continue processing, false means stop.
56      */
57     function onCheckSchema()
58     {
59         $schema = Schema::get();
60
61         $schema->ensureTable('qna_question', QnA_Question::schemaDef());
62         $schema->ensureTable('qna_answer', QnA_Answer::schemaDef());
63         $schema->ensureTable('qna_vote', QnA_Vote::schemaDef());
64
65         return true;
66     }
67
68     public function newFormAction() {
69         return 'qnanewquestion';
70     }
71
72     /**
73      * Map URLs to actions
74      *
75      * @param Net_URL_Mapper $m path-to-action mapper
76      *
77      * @return boolean hook value; true means continue processing, false means stop.
78      */
79
80     function onRouterInitialized($m)
81     {
82         $UUIDregex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
83
84         $m->connect(
85             'main/qna/newquestion',
86             array('action' => 'qnanewquestion')
87         );
88         $m->connect(
89             'answer/qna/closequestion',
90             array('action' => 'qnaclosequestion')
91         );
92         $m->connect(
93             'main/qna/newanswer',
94             array('action' => 'qnanewanswer')
95         );
96         $m->connect(
97             'main/qna/reviseanswer',
98             array('action' => 'qnareviseanswer')
99         );
100         $m->connect(
101             'question/vote/:id',
102             array('action' => 'qnavote', 'type' => 'question'),
103             array('id' => $UUIDregex)
104         );
105         $m->connect(
106             'question/:id',
107             array('action' => 'qnashowquestion'),
108             array('id' => $UUIDregex)
109         );
110         $m->connect(
111             'answer/vote/:id',
112             array('action' => 'qnavote', 'type' => 'answer'),
113             array('id' => $UUIDregex)
114         );
115         $m->connect(
116             'answer/:id',
117             array('action' => 'qnashowanswer'),
118             array('id' => $UUIDregex)
119         );
120
121         return true;
122     }
123
124     function onPluginVersion(&$versions)
125     {
126         $versions[] = array(
127             'name'        => 'QnA',
128             'version'     => GNUSOCIAL_VERSION,
129             'author'      => 'Zach Copley',
130             'homepage'    => 'http://status.net/wiki/Plugin:QnA',
131             'description' =>
132              // TRANS: Plugin description.
133              _m('Question and Answers micro-app.')
134         );
135         return true;
136     }
137
138     function appTitle() {
139         // TRANS: Application title.
140         return _m('TITLE','Question');
141     }
142
143     function tag() {
144         return 'question';
145     }
146
147     function types() {
148         return array(
149             QnA_Question::OBJECT_TYPE,
150             QnA_Answer::OBJECT_TYPE
151         );
152     }
153
154     /**
155      * Given a parsed ActivityStreams activity, save it into a notice
156      * and other data structures.
157      *
158      * @param Activity $activity
159      * @param Profile $actor
160      * @param array $options=array()
161      *
162      * @return Notice the resulting notice
163      */
164     function saveNoticeFromActivity($activity, $actor, $options=array())
165     {
166         if (count($activity->objects) != 1) {
167             // TRANS: Exception thrown when there are too many activity objects.
168             throw new Exception(_m('Too many activity objects.'));
169         }
170
171         $questionObj = $activity->objects[0];
172
173         if ($questionObj->type != QnA_Question::OBJECT_TYPE) {
174             // TRANS: Exception thrown when an incorrect object type is encountered.
175             throw new Exception(_m('Wrong type for object.'));
176         }
177
178         $notice = null;
179
180         switch ($activity->verb) {
181         case ActivityVerb::POST:
182             $notice = QnA_Question::saveNew(
183                 $actor,
184                 $questionObj->title,
185                 $questionObj->summary,
186                 $options
187             );
188             break;
189         case Answer::ObjectType:
190             $question = QnA_Question::getKV('uri', $questionObj->id);
191             if (empty($question)) {
192                 // FIXME: save the question
193                 // TRANS: Exception thrown when answering a non-existing question.
194                 throw new Exception(_m('Answer to unknown question.'));
195             }
196             $notice = QnA_Answer::saveNew($actor, $question, $options);
197             break;
198         default:
199             // TRANS: Exception thrown when an object type is encountered that cannot be handled.
200             throw new Exception(_m('Unknown object type.'));
201         }
202
203         return $notice;
204     }
205
206     /**
207      * Turn a Notice into an activity object
208      *
209      * @param Notice $notice
210      *
211      * @return ActivityObject
212      */
213
214     function activityObjectFromNotice($notice)
215     {
216         $question = null;
217
218         switch ($notice->object_type) {
219         case QnA_Question::OBJECT_TYPE:
220             $question = QnA_Question::fromNotice($notice);
221             break;
222         case QnA_Answer::OBJECT_TYPE:
223             $answer   = QnA_Answer::fromNotice($notice);
224             $question = $answer->getQuestion();
225             break;
226         }
227
228         if (empty($question)) {
229             // TRANS: Exception thrown when an object type is encountered that cannot be handled.
230             throw new Exception(_m('Unknown object type.'));
231         }
232
233         $notice = $question->getNotice();
234
235         if (empty($notice)) {
236             // TRANS: Exception thrown when requesting a non-existing question notice.
237             throw new Exception(_m('Unknown question notice.'));
238         }
239
240         $obj = new ActivityObject();
241
242         $obj->id      = $question->uri;
243         $obj->type    = QnA_Question::OBJECT_TYPE;
244         $obj->title   = $question->title;
245         $obj->link    = $notice->bestUrl();
246
247         // XXX: probably need other stuff here
248
249         return $obj;
250     }
251
252     /**
253      * Output our CSS class for QnA notice list elements
254      *
255      * @param NoticeListItem $nli The item being shown
256      *
257      * @return boolean hook value
258      */
259
260     function onStartOpenNoticeListItemElement($nli)
261     {
262         $type = $nli->notice->object_type;
263
264         switch($type)
265         {
266         case QnA_Question::OBJECT_TYPE:
267             $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
268             $class = 'hentry notice question';
269             if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
270                 $class .= ' limited-scope';
271             }
272
273             $question = QnA_Question::getKV('uri', $nli->notice->uri);
274
275             if (!empty($question->closed)) {
276                 $class .= ' closed';
277             }
278
279             $nli->out->elementStart(
280                 'li', array(
281                     'class' => $class,
282                     'id'    => 'notice-' . $id
283                 )
284             );
285             Event::handle('EndOpenNoticeListItemElement', array($nli));
286             return false;
287             break;
288         case QnA_Answer::OBJECT_TYPE:
289             $id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
290
291             $cls = array('hentry', 'notice', 'answer');
292
293             $answer = QnA_Answer::getKV('uri', $nli->notice->uri);
294
295             if (!empty($answer) && !empty($answer->best)) {
296                 $cls[] = 'best';
297             }
298
299             $nli->out->elementStart(
300                 'li',
301                 array(
302                     'class' => implode(' ', $cls),
303                     'id'    => 'notice-' . $id
304                 )
305             );
306             Event::handle('EndOpenNoticeListItemElement', array($nli));
307             return false;
308             break;
309         default:
310             return true;
311         }
312
313         return true;
314     }
315
316     /**
317      * Custom HTML output for our notices
318      *
319      * @param Notice $notice
320      * @param HTMLOutputter $out
321      */
322     function showNotice($notice, $out)
323     {
324         switch ($notice->object_type) {
325         case QnA_Question::OBJECT_TYPE:
326             return $this->showNoticeQuestion($notice, $out);
327         case QnA_Answer::OBJECT_TYPE:
328             return $this->showNoticeAnswer($notice, $out);
329         default:
330             throw new Exception(
331                 // TRANS: Exception thrown when performing an unexpected action on a question.
332                 // TRANS: %s is the unpexpected object type.
333                 sprintf(_m('Unexpected type for QnA plugin: %s.'),
334                         $notice->object_type
335                 )
336             );
337         }
338     }
339
340     function showNoticeQuestion($notice, $out)
341     {
342         $user = common_current_user();
343
344         // @hack we want regular rendering, then just add stuff after that
345         $nli = new NoticeListItem($notice, $out);
346         $nli->showNotice();
347
348         $out->elementStart('div', array('class' => 'entry-content question-description'));
349
350         $question = QnA_Question::getByNotice($notice);
351
352         if (!empty($question)) {
353
354             $form = new QnashowquestionForm($out, $question);
355             $form->show();
356
357         } else {
358             // TRANS: Error message displayed when question data is not present.
359             $out->text(_m('Question data is missing.'));
360         }
361         $out->elementEnd('div');
362
363         // @fixme
364         $out->elementStart('div', array('class' => 'entry-content'));
365     }
366
367     /**
368      * Output the HTML for this kind of object in a list
369      *
370      * @param NoticeListItem $nli The list item being shown.
371      *
372      * @return boolean hook value
373      *
374      * @todo FIXME: WARNING WARNING WARNING this closes a 'div' that is implicitly opened in BookmarkPlugin's showNotice implementation
375      */
376     function onStartShowNoticeItem($nli)
377     {
378         if (!$this->isMyNotice($nli->notice)) {
379             return true;
380         }
381
382         $out = $nli->out;
383         $notice = $nli->notice;
384
385         $this->showNotice($notice, $out);
386
387         $nli->showNoticeLink();
388         $nli->showNoticeSource();
389         $nli->showNoticeLocation();
390         $nli->showContext();
391         $nli->showRepeat();
392
393         $out->elementEnd('div');
394
395         $nli->showNoticeOptions();
396
397         if ($notice->object_type == QnA_Question::OBJECT_TYPE) {
398             $user = common_current_user();
399             $question = QnA_Question::getByNotice($notice);
400
401             if (!empty($user)) {
402                 $profile = $user->getProfile();
403                 $answer = $question->getAnswer($profile);
404
405                 // Output a placeholder input -- clicking on it will
406                 // bring up a real answer form
407
408                 // NOTE: this whole ul is just a placeholder
409                 if (empty($question->closed) && empty($answer)) {
410                     $out->elementStart('ul', 'notices qna-dummy');
411                     $out->elementStart('li', 'qna-dummy-placeholder');
412                     $out->element(
413                         'input',
414                         array(
415                             'class' => 'placeholder',
416                             // TRANS: Placeholder value for a possible answer to a question
417                             // TRANS: by the logged in user.
418                             'value' => _m('Your answer...')
419                         )
420                     );
421                     $out->elementEnd('li');
422                     $out->elementEnd('ul');
423                 }
424             }
425         }
426
427         return false;
428     }
429
430     function showNoticeAnswer($notice, $out)
431     {
432         $user = common_current_user();
433
434         $answer   = QnA_Answer::getByNotice($notice);
435         $question = $answer->getQuestion();
436
437         $nli = new NoticeListItem($notice, $out);
438         $nli->showNotice();
439
440         $out->elementStart('div', array('class' => 'entry-content answer-content'));
441
442         if (!empty($answer)) {
443             $form = new QnashowanswerForm($out, $answer);
444             $form->show();
445         } else {
446             // TRANS: Error message displayed when answer data is not present.
447             $out->text(_m('Answer data is missing.'));
448         }
449
450         $out->elementEnd('div');
451
452         // @todo FIXME
453         $out->elementStart('div', array('class' => 'entry-content'));
454     }
455
456     static function shorten($content, $notice)
457     {
458         $short = null;
459
460         if (Notice::contentTooLong($content)) {
461             common_debug("content too long");
462             $max = Notice::maxContent();
463             // TRANS: Link description for link to full notice text if it is longer than
464             // TRANS: what will be dispplayed.
465             $ellipsis = _m('…');
466             $short = mb_substr($content, 0, $max - 1);
467             $short .= sprintf('<a href="%1$s" rel="more" title="%2$s">%3$s</a>',
468                               $notice->uri,
469                               // TRANS: Title for link that is an ellipsis in English.
470                               _m('more...'),
471                               $ellipsis);
472         } else {
473             $short = $content;
474         }
475
476         return $short;
477     }
478
479     /**
480      * Form for our app
481      *
482      * @param HTMLOutputter $out
483      * @return Widget
484      */
485     function entryForm($out)
486     {
487         return new QnanewquestionForm($out);
488     }
489
490     /**
491      * When a notice is deleted, clean up related tables.
492      *
493      * @param Notice $notice
494      */
495     function deleteRelated($notice)
496     {
497         switch ($notice->object_type) {
498         case QnA_Question::OBJECT_TYPE:
499             common_log(LOG_DEBUG, "Deleting question from notice...");
500             $question = QnA_Question::fromNotice($notice);
501             $question->delete();
502             break;
503         case QnA_Answer::OBJECT_TYPE:
504             common_log(LOG_DEBUG, "Deleting answer from notice...");
505             $answer = QnA_Answer::fromNotice($notice);
506             common_log(LOG_DEBUG, "to delete: $answer->id");
507             $answer->delete();
508             break;
509         default:
510             common_log(LOG_DEBUG, "Not deleting related, wtf...");
511         }
512     }
513
514     function onEndShowScripts($action)
515     {
516         $action->script($this->path('js/qna.js'));
517         return true;
518     }
519
520     function onEndShowStyles($action)
521     {
522         $action->cssLink($this->path('css/qna.css'));
523         return true;
524     }
525 }