1e37f374b3
Many of the microapps are pretty javascript dependant, but at least we should allow users to get to the new notice field without allowing javascript to run in the browser. :)
526 lines
15 KiB
PHP
526 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* StatusNet - the distributed open-source microblogging tool
|
|
* Copyright (C) 2011, StatusNet, Inc.
|
|
*
|
|
* Microapp plugin for Questions and Answers
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @category QnA
|
|
* @package StatusNet
|
|
* @author Zach Copley <zach@status.net>
|
|
* @copyright 2011 StatusNet, Inc.
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
|
* @link http://status.net/
|
|
*/
|
|
|
|
if (!defined('STATUSNET')) {
|
|
// This check helps protect against security problems;
|
|
// your code file can't be executed directly from the web.
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* Question and Answer plugin
|
|
*
|
|
* @category Plugin
|
|
* @package StatusNet
|
|
* @author Zach Copley <zach@status.net>
|
|
* @copyright 2011 StatusNet, Inc.
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
|
* @link http://status.net/
|
|
*/
|
|
class QnAPlugin extends MicroAppPlugin
|
|
{
|
|
/**
|
|
* Set up our tables (question and answer)
|
|
*
|
|
* @see Schema
|
|
* @see ColumnDef
|
|
*
|
|
* @return boolean hook value; true means continue processing, false means stop.
|
|
*/
|
|
function onCheckSchema()
|
|
{
|
|
$schema = Schema::get();
|
|
|
|
$schema->ensureTable('qna_question', QnA_Question::schemaDef());
|
|
$schema->ensureTable('qna_answer', QnA_Answer::schemaDef());
|
|
$schema->ensureTable('qna_vote', QnA_Vote::schemaDef());
|
|
|
|
return true;
|
|
}
|
|
|
|
public function newFormAction() {
|
|
return 'qnanewquestion';
|
|
}
|
|
|
|
/**
|
|
* Map URLs to actions
|
|
*
|
|
* @param Net_URL_Mapper $m path-to-action mapper
|
|
*
|
|
* @return boolean hook value; true means continue processing, false means stop.
|
|
*/
|
|
|
|
function onRouterInitialized($m)
|
|
{
|
|
$UUIDregex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
|
|
|
|
$m->connect(
|
|
'main/qna/newquestion',
|
|
array('action' => 'qnanewquestion')
|
|
);
|
|
$m->connect(
|
|
'answer/qna/closequestion',
|
|
array('action' => 'qnaclosequestion')
|
|
);
|
|
$m->connect(
|
|
'main/qna/newanswer',
|
|
array('action' => 'qnanewanswer')
|
|
);
|
|
$m->connect(
|
|
'main/qna/reviseanswer',
|
|
array('action' => 'qnareviseanswer')
|
|
);
|
|
$m->connect(
|
|
'question/vote/:id',
|
|
array('action' => 'qnavote', 'type' => 'question'),
|
|
array('id' => $UUIDregex)
|
|
);
|
|
$m->connect(
|
|
'question/:id',
|
|
array('action' => 'qnashowquestion'),
|
|
array('id' => $UUIDregex)
|
|
);
|
|
$m->connect(
|
|
'answer/vote/:id',
|
|
array('action' => 'qnavote', 'type' => 'answer'),
|
|
array('id' => $UUIDregex)
|
|
);
|
|
$m->connect(
|
|
'answer/:id',
|
|
array('action' => 'qnashowanswer'),
|
|
array('id' => $UUIDregex)
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
function onPluginVersion(&$versions)
|
|
{
|
|
$versions[] = array(
|
|
'name' => 'QnA',
|
|
'version' => GNUSOCIAL_VERSION,
|
|
'author' => 'Zach Copley',
|
|
'homepage' => 'http://status.net/wiki/Plugin:QnA',
|
|
'description' =>
|
|
// TRANS: Plugin description.
|
|
_m('Question and Answers micro-app.')
|
|
);
|
|
return true;
|
|
}
|
|
|
|
function appTitle() {
|
|
// TRANS: Application title.
|
|
return _m('TITLE','Question');
|
|
}
|
|
|
|
function tag() {
|
|
return 'question';
|
|
}
|
|
|
|
function types() {
|
|
return array(
|
|
QnA_Question::OBJECT_TYPE,
|
|
QnA_Answer::OBJECT_TYPE
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Given a parsed ActivityStreams activity, save it into a notice
|
|
* and other data structures.
|
|
*
|
|
* @param Activity $activity
|
|
* @param Profile $actor
|
|
* @param array $options=array()
|
|
*
|
|
* @return Notice the resulting notice
|
|
*/
|
|
function saveNoticeFromActivity($activity, $actor, $options=array())
|
|
{
|
|
if (count($activity->objects) != 1) {
|
|
// TRANS: Exception thrown when there are too many activity objects.
|
|
throw new Exception(_m('Too many activity objects.'));
|
|
}
|
|
|
|
$questionObj = $activity->objects[0];
|
|
|
|
if ($questionObj->type != QnA_Question::OBJECT_TYPE) {
|
|
// TRANS: Exception thrown when an incorrect object type is encountered.
|
|
throw new Exception(_m('Wrong type for object.'));
|
|
}
|
|
|
|
$notice = null;
|
|
|
|
switch ($activity->verb) {
|
|
case ActivityVerb::POST:
|
|
$notice = QnA_Question::saveNew(
|
|
$actor,
|
|
$questionObj->title,
|
|
$questionObj->summary,
|
|
$options
|
|
);
|
|
break;
|
|
case Answer::ObjectType:
|
|
$question = QnA_Question::getKV('uri', $questionObj->id);
|
|
if (empty($question)) {
|
|
// FIXME: save the question
|
|
// TRANS: Exception thrown when answering a non-existing question.
|
|
throw new Exception(_m('Answer to unknown question.'));
|
|
}
|
|
$notice = QnA_Answer::saveNew($actor, $question, $options);
|
|
break;
|
|
default:
|
|
// TRANS: Exception thrown when an object type is encountered that cannot be handled.
|
|
throw new Exception(_m('Unknown object type.'));
|
|
}
|
|
|
|
return $notice;
|
|
}
|
|
|
|
/**
|
|
* Turn a Notice into an activity object
|
|
*
|
|
* @param Notice $notice
|
|
*
|
|
* @return ActivityObject
|
|
*/
|
|
|
|
function activityObjectFromNotice($notice)
|
|
{
|
|
$question = null;
|
|
|
|
switch ($notice->object_type) {
|
|
case QnA_Question::OBJECT_TYPE:
|
|
$question = QnA_Question::fromNotice($notice);
|
|
break;
|
|
case QnA_Answer::OBJECT_TYPE:
|
|
$answer = QnA_Answer::fromNotice($notice);
|
|
$question = $answer->getQuestion();
|
|
break;
|
|
}
|
|
|
|
if (empty($question)) {
|
|
// TRANS: Exception thrown when an object type is encountered that cannot be handled.
|
|
throw new Exception(_m('Unknown object type.'));
|
|
}
|
|
|
|
$notice = $question->getNotice();
|
|
|
|
if (empty($notice)) {
|
|
// TRANS: Exception thrown when requesting a non-existing question notice.
|
|
throw new Exception(_m('Unknown question notice.'));
|
|
}
|
|
|
|
$obj = new ActivityObject();
|
|
|
|
$obj->id = $question->uri;
|
|
$obj->type = QnA_Question::OBJECT_TYPE;
|
|
$obj->title = $question->title;
|
|
$obj->link = $notice->bestUrl();
|
|
|
|
// XXX: probably need other stuff here
|
|
|
|
return $obj;
|
|
}
|
|
|
|
/**
|
|
* Output our CSS class for QnA notice list elements
|
|
*
|
|
* @param NoticeListItem $nli The item being shown
|
|
*
|
|
* @return boolean hook value
|
|
*/
|
|
|
|
function onStartOpenNoticeListItemElement($nli)
|
|
{
|
|
$type = $nli->notice->object_type;
|
|
|
|
switch($type)
|
|
{
|
|
case QnA_Question::OBJECT_TYPE:
|
|
$id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
|
|
$class = 'hentry notice question';
|
|
if ($nli->notice->scope != 0 && $nli->notice->scope != 1) {
|
|
$class .= ' limited-scope';
|
|
}
|
|
|
|
$question = QnA_Question::getKV('uri', $nli->notice->uri);
|
|
|
|
if (!empty($question->closed)) {
|
|
$class .= ' closed';
|
|
}
|
|
|
|
$nli->out->elementStart(
|
|
'li', array(
|
|
'class' => $class,
|
|
'id' => 'notice-' . $id
|
|
)
|
|
);
|
|
Event::handle('EndOpenNoticeListItemElement', array($nli));
|
|
return false;
|
|
break;
|
|
case QnA_Answer::OBJECT_TYPE:
|
|
$id = (empty($nli->repeat)) ? $nli->notice->id : $nli->repeat->id;
|
|
|
|
$cls = array('hentry', 'notice', 'answer');
|
|
|
|
$answer = QnA_Answer::getKV('uri', $nli->notice->uri);
|
|
|
|
if (!empty($answer) && !empty($answer->best)) {
|
|
$cls[] = 'best';
|
|
}
|
|
|
|
$nli->out->elementStart(
|
|
'li',
|
|
array(
|
|
'class' => implode(' ', $cls),
|
|
'id' => 'notice-' . $id
|
|
)
|
|
);
|
|
Event::handle('EndOpenNoticeListItemElement', array($nli));
|
|
return false;
|
|
break;
|
|
default:
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Custom HTML output for our notices
|
|
*
|
|
* @param Notice $notice
|
|
* @param HTMLOutputter $out
|
|
*/
|
|
function showNotice($notice, $out)
|
|
{
|
|
switch ($notice->object_type) {
|
|
case QnA_Question::OBJECT_TYPE:
|
|
return $this->showNoticeQuestion($notice, $out);
|
|
case QnA_Answer::OBJECT_TYPE:
|
|
return $this->showNoticeAnswer($notice, $out);
|
|
default:
|
|
throw new Exception(
|
|
// TRANS: Exception thrown when performing an unexpected action on a question.
|
|
// TRANS: %s is the unpexpected object type.
|
|
sprintf(_m('Unexpected type for QnA plugin: %s.'),
|
|
$notice->object_type
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
function showNoticeQuestion($notice, $out)
|
|
{
|
|
$user = common_current_user();
|
|
|
|
// @hack we want regular rendering, then just add stuff after that
|
|
$nli = new NoticeListItem($notice, $out);
|
|
$nli->showNotice();
|
|
|
|
$out->elementStart('div', array('class' => 'entry-content question-description'));
|
|
|
|
$question = QnA_Question::getByNotice($notice);
|
|
|
|
if (!empty($question)) {
|
|
|
|
$form = new QnashowquestionForm($out, $question);
|
|
$form->show();
|
|
|
|
} else {
|
|
// TRANS: Error message displayed when question data is not present.
|
|
$out->text(_m('Question data is missing.'));
|
|
}
|
|
$out->elementEnd('div');
|
|
|
|
// @fixme
|
|
$out->elementStart('div', array('class' => 'entry-content'));
|
|
}
|
|
|
|
/**
|
|
* Output the HTML for this kind of object in a list
|
|
*
|
|
* @param NoticeListItem $nli The list item being shown.
|
|
*
|
|
* @return boolean hook value
|
|
*
|
|
* @todo FIXME: WARNING WARNING WARNING this closes a 'div' that is implicitly opened in BookmarkPlugin's showNotice implementation
|
|
*/
|
|
function onStartShowNoticeItem($nli)
|
|
{
|
|
if (!$this->isMyNotice($nli->notice)) {
|
|
return true;
|
|
}
|
|
|
|
$out = $nli->out;
|
|
$notice = $nli->notice;
|
|
|
|
$this->showNotice($notice, $out);
|
|
|
|
$nli->showNoticeLink();
|
|
$nli->showNoticeSource();
|
|
$nli->showNoticeLocation();
|
|
$nli->showContext();
|
|
$nli->showRepeat();
|
|
|
|
$out->elementEnd('div');
|
|
|
|
$nli->showNoticeOptions();
|
|
|
|
if ($notice->object_type == QnA_Question::OBJECT_TYPE) {
|
|
$user = common_current_user();
|
|
$question = QnA_Question::getByNotice($notice);
|
|
|
|
if (!empty($user)) {
|
|
$profile = $user->getProfile();
|
|
$answer = $question->getAnswer($profile);
|
|
|
|
// Output a placeholder input -- clicking on it will
|
|
// bring up a real answer form
|
|
|
|
// NOTE: this whole ul is just a placeholder
|
|
if (empty($question->closed) && empty($answer)) {
|
|
$out->elementStart('ul', 'notices qna-dummy');
|
|
$out->elementStart('li', 'qna-dummy-placeholder');
|
|
$out->element(
|
|
'input',
|
|
array(
|
|
'class' => 'placeholder',
|
|
// TRANS: Placeholder value for a possible answer to a question
|
|
// TRANS: by the logged in user.
|
|
'value' => _m('Your answer...')
|
|
)
|
|
);
|
|
$out->elementEnd('li');
|
|
$out->elementEnd('ul');
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function showNoticeAnswer($notice, $out)
|
|
{
|
|
$user = common_current_user();
|
|
|
|
$answer = QnA_Answer::getByNotice($notice);
|
|
$question = $answer->getQuestion();
|
|
|
|
$nli = new NoticeListItem($notice, $out);
|
|
$nli->showNotice();
|
|
|
|
$out->elementStart('div', array('class' => 'entry-content answer-content'));
|
|
|
|
if (!empty($answer)) {
|
|
$form = new QnashowanswerForm($out, $answer);
|
|
$form->show();
|
|
} else {
|
|
// TRANS: Error message displayed when answer data is not present.
|
|
$out->text(_m('Answer data is missing.'));
|
|
}
|
|
|
|
$out->elementEnd('div');
|
|
|
|
// @todo FIXME
|
|
$out->elementStart('div', array('class' => 'entry-content'));
|
|
}
|
|
|
|
static function shorten($content, $notice)
|
|
{
|
|
$short = null;
|
|
|
|
if (Notice::contentTooLong($content)) {
|
|
common_debug("content too long");
|
|
$max = Notice::maxContent();
|
|
// TRANS: Link description for link to full notice text if it is longer than
|
|
// TRANS: what will be dispplayed.
|
|
$ellipsis = _m('…');
|
|
$short = mb_substr($content, 0, $max - 1);
|
|
$short .= sprintf('<a href="%1$s" rel="more" title="%2$s">%3$s</a>',
|
|
$notice->uri,
|
|
// TRANS: Title for link that is an ellipsis in English.
|
|
_m('more...'),
|
|
$ellipsis);
|
|
} else {
|
|
$short = $content;
|
|
}
|
|
|
|
return $short;
|
|
}
|
|
|
|
/**
|
|
* Form for our app
|
|
*
|
|
* @param HTMLOutputter $out
|
|
* @return Widget
|
|
*/
|
|
function entryForm($out)
|
|
{
|
|
return new QnanewquestionForm($out);
|
|
}
|
|
|
|
/**
|
|
* When a notice is deleted, clean up related tables.
|
|
*
|
|
* @param Notice $notice
|
|
*/
|
|
function deleteRelated($notice)
|
|
{
|
|
switch ($notice->object_type) {
|
|
case QnA_Question::OBJECT_TYPE:
|
|
common_log(LOG_DEBUG, "Deleting question from notice...");
|
|
$question = QnA_Question::fromNotice($notice);
|
|
$question->delete();
|
|
break;
|
|
case QnA_Answer::OBJECT_TYPE:
|
|
common_log(LOG_DEBUG, "Deleting answer from notice...");
|
|
$answer = QnA_Answer::fromNotice($notice);
|
|
common_log(LOG_DEBUG, "to delete: $answer->id");
|
|
$answer->delete();
|
|
break;
|
|
default:
|
|
common_log(LOG_DEBUG, "Not deleting related, wtf...");
|
|
}
|
|
}
|
|
|
|
function onEndShowScripts($action)
|
|
{
|
|
$action->script($this->path('js/qna.js'));
|
|
return true;
|
|
}
|
|
|
|
function onEndShowStyles($action)
|
|
{
|
|
$action->cssLink($this->path('css/qna.css'));
|
|
return true;
|
|
}
|
|
}
|