diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php new file mode 100644 index 0000000000..ba3b6e329a --- /dev/null +++ b/plugins/QnA/QnAPlugin.php @@ -0,0 +1,465 @@ +. + * + * @category QnA + * @package StatusNet + * @author Zach Copley + * @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 + * @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; + } + + /** + * Load related modules when needed + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'QnanewquestionAction': + case 'QnanewanswerAction': + case 'QnashowquestionAction': + case 'QnashowanswerAction': + case 'QnavoteAction': + include_once $dir . '/actions/' + . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'QnaquestionForm': + case 'QnaanswerForm': + case 'QnavoteForm'; + include_once $dir . '/lib/' . strtolower($cls).'.php'; + break; + case 'QnA_Question': + case 'QnA_Answer': + case 'QnA_Vote': + include_once $dir . '/classes/' . $cls.'.php'; + return false; + break; + default: + return true; + } + } + + /** + * 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( + 'main/qna/newanswer/:id', + array('action' => 'qnanewanswer'), + array('id' => $UUIDregex) + ); + $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' => STATUSNET_VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:QnA', + 'description' => + _m('Question and Answers micro-app.') + ); + return true; + } + + function appTitle() { + return _m('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) { + throw new Exception('Too many activity objects.'); + } + + $questionObj = $activity->objects[0]; + + if ($questinoObj->type != QnA_Question::OBJECT_TYPE) { + throw new Exception('Wrong type for object.'); + } + + $notice = null; + + switch ($activity->verb) { + case ActivityVerb::POST: + $notice = Question::saveNew( + $actor, + $questionObj->title + // null, + // $questionObj->summary, + // $options + ); + break; + case Answer::NORMAL: + $question = QnA_Question::staticGet('uri', $questionObj->id); + if (empty($question)) { + // FIXME: save the question + throw new Exception("Answer to unknown question."); + } + $notice = QnA_Answer::saveNew($actor, $question, $activity->verb, $options); + break; + default: + throw new Exception("Unknown verb for question"); + } + + 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)) { + throw new Exception("Unknown object type."); + } + + $notice = $question->getNotice(); + + if (empty($notice)) { + throw new Exception("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; + } + + /** + * Change the verb on Answer notices + * + * @param Notice $notice + * + * @return ActivityObject + */ + + function onEndNoticeAsActivity($notice, &$act) { + switch ($notice->object_type) { + case Answer::NORMAL: + case Answer::ANONYMOUS: + $act->verb = $notice->object_type; + break; + } + 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: + $this->showQuestionNotice($notice, $out); + break; + case QnA_Answer::OBJECT_TYPE: + $this->showAnswerNotice($notice, $out); + break; + } + + // bad craziness + $out->elementStart('div', array('class' => 'question')); + + $profile = $notice->getProfile(); + $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); + + $out->element( + 'img', + array( + 'src' => ($avatar) + ? $avatar->displayUrl() + : Avatar::defaultImage(AVATAR_MINI_SIZE), + 'class' => 'avatar photo question-avatar', + 'width' => AVATAR_MINI_SIZE, + 'height' => AVATAR_MINI_SIZE, + 'alt' => $profile->getBestName() + ) + ); + + $out->raw(' '); // avoid   for AJAX XML compatibility + + // hack for belongsOnTimeline; JS needs to be able to find the author + $out->elementStart('span', 'vcard author'); + $out->element( + 'a', + array( + 'class' => 'url', + 'href' => $profile->profileurl, + 'title' => $profile->getBestName() + ), + $profile->nickname + ); + + $out->elementEnd('span'); + } + + function showAnswerNotice($notice, $out) + { + $answer = QnA_Answer::fromNotice($notice); + + assert(!empty($answer)); + + $out->elementStart('div', 'answer'); + $out->raw($answer->asHTML()); + $out->elementEnd('div'); + } + + function showQuestionNotice($notice, $out) + { + $profile = $notice->getProfile(); + $question = QnA_Question::fromNotice($notice); + + assert(!empty($question)); + assert(!empty($profile)); + + $out->elementStart('div', 'question-notice'); + + $out->elementStart('h3'); + + if (!empty($question->url)) { + $out->element( + 'a', + array( + 'href' => $question->url, + 'class' => 'question-title' + ), + $question->title + ); + } else { + $out->text($question->title); + } + + if (!empty($question->location)) { + $out->elementStart('div', 'question-location'); + $out->element('strong', null, _('Location: ')); + $out->element('span', 'location', $question->location); + $out->elementEnd('div'); + } + + if (!empty($question->description)) { + $out->elementStart('div', 'question-description'); + $out->element('strong', null, _('Description: ')); + $out->element('span', 'description', $question->description); + $out->elementEnd('div'); + } + + //$answers = $question->getAnswers(); + + $out->elementStart('div', 'question-answers'); + $out->element('strong', null, _('Answer: ')); + $out->element('span', 'question-answer'); + + $out->elementEnd('div'); + + $user = common_current_user(); + + if (!empty($user)) { + + $answer = $question->getAnswer($user->getProfile()); + + if (empty($answer)) { + $form = new QnaanswerForm($question, $out); + $form->show(); + } + + + } + + $out->elementEnd('div'); + } + + /** + * Form for our app + * + * @param HTMLOutputter $out + * @return Widget + */ + + function entryForm($out) + { + return new QnaquestionForm($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) + { + // XXX maybe some cool shiz here + } + + function onEndShowStyles($action) + { + $action->cssLink($this->path('css/qna.css')); + return true; + } +} diff --git a/plugins/QnA/actions/answer.php b/plugins/QnA/actions/qnanewanswer.php similarity index 85% rename from plugins/QnA/actions/answer.php rename to plugins/QnA/actions/qnanewanswer.php index 17e841e545..10b1046c3e 100644 --- a/plugins/QnA/actions/answer.php +++ b/plugins/QnA/actions/qnanewanswer.php @@ -43,14 +43,14 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class AnswerAction extends Action +class QnanewanswerAction extends Action { protected $user = null; protected $error = null; protected $complete = null; - protected $qustion = null; - protected $answer = null; + protected $question = null; + protected $answerText = null; /** * Returns the title of the action @@ -81,8 +81,10 @@ class AnswerAction extends Action if (empty($this->user)) { // TRANS: Client exception thrown trying to answer a question while not logged in. - throw new ClientException(_m("You must be logged in to answer to a question."), - 403); + throw new ClientException( + _m("You must be logged in to answer to a question."), + 403 + ); } if ($this->isPost()) { @@ -90,15 +92,18 @@ class AnswerAction extends Action } $id = $this->trimmed('id'); - $this->question = Question::staticGet('id', $id); + $this->question = QnA_Question::staticGet('id', $id); + if (empty($this->question)) { // TRANS: Client exception thrown trying to respond to a non-existing question. - throw new ClientException(_m('Invalid or missing question.'), 404); + throw new ClientException( + _m('Invalid or missing question.'), + 404 + ); } - $answer = $this->trimmed('answer'); - - + $this->answerText = $this->trimmed('answer'); + return true; } @@ -114,7 +119,7 @@ class AnswerAction extends Action parent::handle($argarray); if ($this->isPost()) { - $this->answer(); + $this->newAnswer(); } else { $this->showPage(); } @@ -127,13 +132,13 @@ class AnswerAction extends Action * * @return void */ - function answer() + function newAnswer() { try { - $notice = Answer::saveNew( + $notice = QnA_Answer::saveNew( $this->user->getProfile(), $this->question, - $this->answer + $this->answerText ); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -150,7 +155,7 @@ class AnswerAction extends Action $this->element('title', null, _m('Answers')); $this->elementEnd('head'); $this->elementStart('body'); - $form = new Answer($this->question, $this); + $form = new QnA_Answer($this->question, $this); $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); @@ -170,7 +175,7 @@ class AnswerAction extends Action $this->element('p', 'error', $this->error); } - $form = new AnswerForm($this->question, $this); + $form = new QnaanswerForm($this->question, $this); $form->show(); diff --git a/plugins/QnA/actions/newquestion.php b/plugins/QnA/actions/qnanewquestion.php similarity index 99% rename from plugins/QnA/actions/newquestion.php rename to plugins/QnA/actions/qnanewquestion.php index 0a486dfa43..8682f8dd47 100644 --- a/plugins/QnA/actions/newquestion.php +++ b/plugins/QnA/actions/qnanewquestion.php @@ -43,7 +43,7 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class NewquestionAction extends Action +class QnanewquestionAction extends Action { protected $user = null; protected $error = null; diff --git a/plugins/QnA/actions/showanswer.php b/plugins/QnA/actions/qnashowanswer.php similarity index 98% rename from plugins/QnA/actions/showanswer.php rename to plugins/QnA/actions/qnashowanswer.php index 7686d6d566..68baadfba8 100644 --- a/plugins/QnA/actions/showanswer.php +++ b/plugins/QnA/actions/qnashowanswer.php @@ -45,7 +45,7 @@ if (!defined('STATUSNET')) { * @link http://status.net/ */ -class ShowAnswerAction extends ShownoticeAction +class QnashowanswerAction extends ShownoticeAction { protected $answer = null; diff --git a/plugins/QnA/actions/showquestion.php b/plugins/QnA/actions/qnashowquestion.php similarity index 98% rename from plugins/QnA/actions/showquestion.php rename to plugins/QnA/actions/qnashowquestion.php index 41c1d809fe..e563753a01 100644 --- a/plugins/QnA/actions/showquestion.php +++ b/plugins/QnA/actions/qnashowquestion.php @@ -44,7 +44,7 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class ShowquestionAction extends ShownoticeAction +class QnashowquestionAction extends ShownoticeAction { protected $question = null; diff --git a/plugins/QnA/actions/qnavote.php b/plugins/QnA/actions/qnavote.php index 6c1b9f053e..94aec41c5b 100644 --- a/plugins/QnA/actions/qnavote.php +++ b/plugins/QnA/actions/qnavote.php @@ -150,7 +150,7 @@ class Qnavote extends Action $this->element('title', null, _m('Answers')); $this->elementEnd('head'); $this->elementStart('body'); - $form = new Answer($this->question, $this); + $form = new QnA_Answer($this->question, $this); $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); @@ -170,7 +170,7 @@ class Qnavote extends Action $this->element('p', 'error', $this->error); } - $form = new AnswerForm($this->question, $this); + $form = new QnaanswerForm($this->question, $this); $form->show(); diff --git a/plugins/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php index 102af70057..ff11ff8f14 100644 --- a/plugins/QnA/classes/QnA_Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -45,7 +45,7 @@ if (!defined('STATUSNET')) { class QnA_Answer extends Managed_DataObject { const OBJECT_TYPE = 'http://activityschema.org/object/answer'; - + public $__table = 'qna_answer'; // table name public $id; // char(36) primary key not null -> UUID public $question_id; // char(36) -> question.id UUID @@ -95,19 +95,19 @@ class QnA_Answer extends Managed_DataObject 'description' => 'Record of answers to questions', 'fields' => array( 'id' => array( - 'type' => 'char', - 'length' => 36, + 'type' => 'char', + 'length' => 36, 'not null' => true, 'description' => 'UUID of the response'), 'uri' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => true, + 'type' => 'varchar', + 'length' => 255, + 'not null' => true, 'description' => 'UUID to the answer notice' ), 'question_id' => array( - 'type' => 'char', - 'length' => 36, - 'not null' => true, + 'type' => 'char', + 'length' => 36, + 'not null' => true, 'description' => 'UUID of question being responded to' ), 'best' => array('type' => 'int', 'size' => 'tiny'), @@ -164,7 +164,7 @@ class QnA_Answer extends Managed_DataObject static function fromNotice($notice) { - return QnA_Answer::staticGet('uri', $notice->uri); + return self::staticGet('uri', $notice->uri); } /** @@ -182,7 +182,7 @@ class QnA_Answer extends Managed_DataObject $options = array(); } - $answer = new Answer(); + $answer = new QnA_Answer(); $answer->id = UUID::gen(); $answer->profile_id = $profile->id; $answer->question_id = $question->id; @@ -191,7 +191,7 @@ class QnA_Answer extends Managed_DataObject 'showanswer', array('id' => $answer->id) ); - + common_log(LOG_DEBUG, "Saving answer: $answer->id, $answer->uri"); $answer->insert(); @@ -201,6 +201,7 @@ class QnA_Answer extends Managed_DataObject _m('answered "%s"'), $answer->uri ); + $link = '' . htmlspecialchars($answer) . ''; // TRANS: Rendered version of the notice content answering a question. // TRANS: %s a link to the question with question title as the link content. diff --git a/plugins/QnA/lib/answerform.php b/plugins/QnA/lib/qnaanswerform.php similarity index 96% rename from plugins/QnA/lib/answerform.php rename to plugins/QnA/lib/qnaanswerform.php index d4f28bb6d2..f89f6c7889 100644 --- a/plugins/QnA/lib/answerform.php +++ b/plugins/QnA/lib/qnaanswerform.php @@ -44,7 +44,7 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class AnswerForm extends Form +class QnaanswerForm extends Form { protected $question; @@ -89,7 +89,7 @@ class AnswerForm extends Form */ function action() { - return common_local_url('answer', array('id' => $this->question->id)); + return common_local_url('qnanewanswer', array('id' => $this->question->id)); } /** diff --git a/plugins/QnA/lib/questionform.php b/plugins/QnA/lib/qnaquestionform.php similarity index 97% rename from plugins/QnA/lib/questionform.php rename to plugins/QnA/lib/qnaquestionform.php index a26bbb17be..9d0c2aad59 100644 --- a/plugins/QnA/lib/questionform.php +++ b/plugins/QnA/lib/qnaquestionform.php @@ -44,7 +44,7 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class QuestionForm extends Form +class QnaquestionForm extends Form { protected $title; protected $description; @@ -90,7 +90,7 @@ class QuestionForm extends Form */ function action() { - return common_local_url('newquestion'); + return common_local_url('qnanewquestion'); } /** diff --git a/plugins/QnA/lib/voteform.php b/plugins/QnA/lib/qnavoteform.php similarity index 95% rename from plugins/QnA/lib/voteform.php rename to plugins/QnA/lib/qnavoteform.php index 554f698d99..f6976c8834 100644 --- a/plugins/QnA/lib/voteform.php +++ b/plugins/QnA/lib/qnavoteform.php @@ -44,7 +44,7 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class AnswerForm extends Form +class QnavoteForm extends Form { protected $question; @@ -89,7 +89,7 @@ class AnswerForm extends Form */ function action() { - return common_local_url('answer', array('id' => $this->question->id)); + return common_local_url('qnavote', array('id' => $this->question->id)); } /** @@ -104,7 +104,7 @@ class AnswerForm extends Form $id = "question-" . $question->id; $out->element('p', 'answer', $question->question); - $out->element('input', array('type' => 'text', 'name' => 'answer')); + $out->element('input', array('type' => 'text', 'name' => 'vote')); } /**