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