]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/QnA/QnAPlugin
Renamed QuestionAndAnswerPlugin to QnAPlugin
[quix0rs-gnu-social.git] / plugins / QnA / QnAPlugin
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     // @fixme which domain should we use for these namespaces?
51     const QUESTION_OBJECT = 'http://activityschema.org/object/question';
52     const ANSWER_OBJECT   = 'http://activityschema.org/object/answer';
53   
54     /**
55      * Set up our tables (question and answer)
56      *
57      * @see Schema
58      * @see ColumnDef
59      *
60      * @return boolean hook value; true means continue processing, false means stop.
61      */
62     function onCheckSchema()
63     {
64         $schema = Schema::get();
65
66         $schema->ensureTable('qna_question', QnA_Question::schemaDef());
67         $schema->ensureTable('qna_answer', QnA_Answer::schemaDef());
68         $schema->ensureTable('qna_vote', QnA_Vote::schemaDef());
69         
70         return true;
71     }
72
73     /**
74      * Load related modules when needed
75      *
76      * @param string $cls Name of the class to be loaded
77      *
78      * @return boolean hook value; true means continue processing, false means stop.
79      */
80     function onAutoload($cls)
81     {
82         $dir = dirname(__FILE__);
83
84         switch ($cls)
85         {
86         case 'NewquestionAction':
87         case 'NewanswerAction':
88         case 'ShowquestionAction':
89         case 'ShowanswerAction':
90         case 'QnavoteAction':
91             include_once $dir . '/actions/'
92                 . strtolower(mb_substr($cls, 0, -6)) . '.php';
93             return false;
94         case 'QuestionForm':
95         case 'AnswerForm':
96         case 'VoteForm';
97             include_once $dir . '/lib/' . strtolower($cls).'.php';
98             break;
99         case 'QnA_Question':
100         case 'QnA_Answer':
101         case 'QnA_Vote':
102             include_once $dir . '/classes/' . $cls.'.php';
103             return false;
104             break;
105         default:
106             return true;
107         }
108     }
109
110     /**
111      * Map URLs to actions
112      *
113      * @param Net_URL_Mapper $m path-to-action mapper
114      *
115      * @return boolean hook value; true means continue processing, false means stop.
116      */
117
118     function onRouterInitialized($m)
119     {
120         $regexId = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
121       
122         $m->connect(
123             'main/question/new', 
124             array('action' => 'newquestion')
125         );
126         $m->connect(
127             'main/question/answer',
128             array('action' => 'newanswer')
129         );
130         $m->connect(
131             'question/vote/:id',
132             array('action' => 'qnavote', 'type' => 'question'),
133             array('id' => $regexId)
134         );
135         $m->connect(
136             'question/:id',
137             array('action' => 'showquestion'),
138             array('id' => $regexId)
139         );
140         $m->connect(
141             'answer/vote/:id',
142             array('action' => 'qnavote', 'type' => 'answer'),
143             array('id' => $regexId)
144         );
145         $m->connect(
146             'answer/:id',
147             array('action' => 'showanswer'),
148             array('id' => $regexId)
149         );
150         
151         return true;
152     }
153
154     function onPluginVersion(&$versions)
155     {
156         $versions[] = array(
157             'name'        => 'QnA',
158             'version'     => STATUSNET_VERSION,
159             'author'      => 'Zach Copley',
160             'homepage'    => 'http://status.net/wiki/Plugin:QnA',
161             'description' =>
162              _m('Question and Answers micro-app.')
163         );
164         return true;
165     }
166
167     function appTitle() {
168         return _m('Question');
169     }
170
171     function tag() {
172         return 'question';
173     }
174
175     function types() {
176         return array(
177             Question::OBJECT_TYPE,
178             Answer::NORMAL
179         );
180     }
181
182     /**
183      * Given a parsed ActivityStreams activity, save it into a notice
184      * and other data structures.
185      *
186      * @param Activity $activity
187      * @param Profile $actor
188      * @param array $options=array()
189      *
190      * @return Notice the resulting notice
191      */
192     function saveNoticeFromActivity($activity, $actor, $options=array())
193     {
194         if (count($activity->objects) != 1) {
195             throw new Exception('Too many activity objects.');
196         }
197
198         $questionObj = $activity->objects[0];
199
200         if ($questinoObj->type != QnA_Question::OBJECT_TYPE) {
201             throw new Exception('Wrong type for object.');
202         }
203
204         $notice = null;
205
206         switch ($activity->verb) {
207         case ActivityVerb::POST:
208             $notice = Question::saveNew(
209                 $actor,
210                 $questionObj->title
211                // null,
212                // $questionObj->summary,
213                // $options
214             );
215             break;
216         case Answer::NORMAL:
217             $question = QnA_Question::staticGet('uri', $questionObj->id);
218             if (empty($question)) {
219                 // FIXME: save the question
220                 throw new Exception("Answer to unknown question.");
221             }
222             $notice = QnA_Answer::saveNew($actor, $question, $activity->verb, $options);
223             break;
224         default:
225             throw new Exception("Unknown verb for question");
226         }
227
228         return $notice;
229     }
230
231     /**
232      * Turn a Notice into an activity object
233      *
234      * @param Notice $notice
235      *
236      * @return ActivityObject
237      */
238
239     function activityObjectFromNotice($notice)
240     {
241         $question = null;
242
243         switch ($notice->object_type) {
244         case Question::OBJECT_TYPE:
245             $question = Qeustion::fromNotice($notice);
246             break;
247         case Answer::NORMAL:
248         case Answer::ANONYMOUS:
249             $answer   = Answer::fromNotice($notice);
250             $question = $answer->getQuestion();
251             break;
252         }
253
254         if (empty($question)) {
255             throw new Exception("Unknown object type.");
256         }
257
258         $notice = $question->getNotice();
259
260         if (empty($notice)) {
261             throw new Exception("Unknown question notice.");
262         }
263
264         $obj = new ActivityObject();
265
266         $obj->id      = $question->uri;
267         $obj->type    = Question::OBJECT_TYPE;
268         $obj->title   = $question->title;
269         $obj->link    = $notice->bestUrl();
270
271         // XXX: probably need other stuff here
272
273         return $obj;
274     }
275
276     /**
277      * Change the verb on Answer notices
278      *
279      * @param Notice $notice
280      *
281      * @return ActivityObject
282      */
283
284     function onEndNoticeAsActivity($notice, &$act) {
285         switch ($notice->object_type) {
286         case Answer::NORMAL:
287         case Answer::ANONYMOUS:
288             $act->verb = $notice->object_type;
289             break;
290         }
291         return true;
292     }
293
294     /**
295      * Custom HTML output for our notices
296      *
297      * @param Notice $notice
298      * @param HTMLOutputter $out
299      */
300
301     function showNotice($notice, $out)
302     {
303         switch ($notice->object_type) {
304         case Question::OBJECT_TYPE:
305             $this->showQuestionNotice($notice, $out);
306             break;
307         case Answer::NORMAL:
308         case Answer::ANONYMOUS:
309         case RSVP::POSSIBLE:
310             $this->showAnswerNotice($notice, $out);
311             break;
312         }
313
314         $out->elementStart('div', array('class' => 'question'));
315
316         $profile = $notice->getProfile();
317         $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
318
319         $out->element('img',
320                       array('src' => ($avatar) ?
321                             $avatar->displayUrl() :
322                             Avatar::defaultImage(AVATAR_MINI_SIZE),
323                             'class' => 'avatar photo bookmark-avatar',
324                             'width' => AVATAR_MINI_SIZE,
325                             'height' => AVATAR_MINI_SIZE,
326                             'alt' => $profile->getBestName()));
327
328         $out->raw('&#160;'); // avoid &nbsp; for AJAX XML compatibility
329
330         $out->elementStart('span', 'vcard author'); // hack for belongsOnTimeline; JS needs to be able to find the author
331         $out->element('a',
332                       array('class' => 'url',
333                             'href' => $profile->profileurl,
334                             'title' => $profile->getBestName()),
335                       $profile->nickname);
336         $out->elementEnd('span');
337     }
338
339     function showAnswerNotice($notice, $out)
340     {
341         $rsvp = Answer::fromNotice($notice);
342
343         $out->elementStart('div', 'answer');
344         $out->raw($answer->asHTML());
345         $out->elementEnd('div');
346         return;
347     }
348
349     function showQuestionNotice($notice, $out)
350     {
351         $profile  = $notice->getProfile();
352         $question = Question::fromNotice($notice);
353
354         assert(!empty($question));
355         assert(!empty($profile));
356
357         $out->elementStart('div', 'question-notice');
358
359         $out->elementStart('h3');
360
361         if (!empty($question->url)) {
362             $out->element('a',
363                           array('href' => $question->url,
364                                 'class' => 'question-title'),
365                           $question->title);
366         } else {
367             $out->text($question->title);
368         }
369
370         if (!empty($question->location)) {
371             $out->elementStart('div', 'question-location');
372             $out->element('strong', null, _('Location: '));
373             $out->element('span', 'location', $question->location);
374             $out->elementEnd('div');
375         }
376
377         if (!empty($question->description)) {
378             $out->elementStart('div', 'question-description');
379             $out->element('strong', null, _('Description: '));
380             $out->element('span', 'description', $question->description);
381             $out->elementEnd('div');
382         }
383
384         $answers = $question->getAnswers();
385
386         $out->elementStart('div', 'question-answers');
387         $out->element('strong', null, _('Answer: '));
388         $out->element('span', 'question-answer');
389
390         // XXX I dunno
391
392         $out->elementEnd('div');
393
394         $user = common_current_user();
395
396         if (!empty($user)) {
397             $question = $question->getAnswer($user->getProfile());
398
399             if (empty($answer)) {
400                 $form = new AnswerForm($question, $out);
401             }
402
403             $form->show();
404         }
405
406         $out->elementEnd('div');
407     }
408
409     /**
410      * Form for our app
411      *
412      * @param HTMLOutputter $out
413      * @return Widget
414      */
415
416     function entryForm($out)
417     {
418         return new QuestionForm($out);
419     }
420
421     /**
422      * When a notice is deleted, clean up related tables.
423      *
424      * @param Notice $notice
425      */
426
427     function deleteRelated($notice)
428     {
429         switch ($notice->object_type) {
430         case Question::OBJECT_TYPE:
431             common_log(LOG_DEBUG, "Deleting question from notice...");
432             $question = Question::fromNotice($notice);
433             $question->delete();
434             break;
435         case Answer::NORMAL:
436         case Answer::ANONYMOUS:
437             common_log(LOG_DEBUG, "Deleting answer from notice...");
438             $answer = Answer::fromNotice($notice);
439             common_log(LOG_DEBUG, "to delete: $answer->id");
440             $answer->delete();
441             break;
442         default:
443             common_log(LOG_DEBUG, "Not deleting related, wtf...");
444         }
445     }
446
447     function onEndShowScripts($action)
448     {
449         // XXX maybe some cool shiz here
450     }
451
452     function onEndShowStyles($action)
453     {
454         $action->cssLink($this->path('css/questionandanswer.css'));
455         return true;
456     }
457 }