From 73c3344cc3b867dc5e701554d410a87c18315e5a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 21 Mar 2011 15:50:36 -0700 Subject: [PATCH] * Fix plugin filename * Make questions save! --- plugins/QnA/QnAPlugin | 457 --------------------------- plugins/QnA/actions/newquestion.php | 33 +- plugins/QnA/actions/qnavote.php | 10 +- plugins/QnA/classes/QnA_Answer.php | 45 +-- plugins/QnA/classes/QnA_Question.php | 33 +- plugins/QnA/classes/QnA_Vote.php | 4 +- plugins/QnA/lib/answerform.php | 2 +- plugins/QnA/lib/questionform.php | 31 +- 8 files changed, 92 insertions(+), 523 deletions(-) delete mode 100644 plugins/QnA/QnAPlugin diff --git a/plugins/QnA/QnAPlugin b/plugins/QnA/QnAPlugin deleted file mode 100644 index 76bd304a87..0000000000 --- a/plugins/QnA/QnAPlugin +++ /dev/null @@ -1,457 +0,0 @@ -. - * - * @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 -{ - - // @fixme which domain should we use for these namespaces? - const QUESTION_OBJECT = 'http://activityschema.org/object/question'; - const ANSWER_OBJECT = 'http://activityschema.org/object/answer'; - - /** - * 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 'NewquestionAction': - case 'NewanswerAction': - case 'ShowquestionAction': - case 'ShowanswerAction': - case 'QnavoteAction': - include_once $dir . '/actions/' - . strtolower(mb_substr($cls, 0, -6)) . '.php'; - return false; - case 'QuestionForm': - case 'AnswerForm': - case 'VoteForm'; - 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) - { - $regexId = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'; - - $m->connect( - 'main/question/new', - array('action' => 'newquestion') - ); - $m->connect( - 'main/question/answer', - array('action' => 'newanswer') - ); - $m->connect( - 'question/vote/:id', - array('action' => 'qnavote', 'type' => 'question'), - array('id' => $regexId) - ); - $m->connect( - 'question/:id', - array('action' => 'showquestion'), - array('id' => $regexId) - ); - $m->connect( - 'answer/vote/:id', - array('action' => 'qnavote', 'type' => 'answer'), - array('id' => $regexId) - ); - $m->connect( - 'answer/:id', - array('action' => 'showanswer'), - array('id' => $regexId) - ); - - 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( - Question::OBJECT_TYPE, - Answer::NORMAL - ); - } - - /** - * 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 Question::OBJECT_TYPE: - $question = Qeustion::fromNotice($notice); - break; - case Answer::NORMAL: - case Answer::ANONYMOUS: - $answer = 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 = 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 Question::OBJECT_TYPE: - $this->showQuestionNotice($notice, $out); - break; - case Answer::NORMAL: - case Answer::ANONYMOUS: - case RSVP::POSSIBLE: - $this->showAnswerNotice($notice, $out); - break; - } - - $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 bookmark-avatar', - 'width' => AVATAR_MINI_SIZE, - 'height' => AVATAR_MINI_SIZE, - 'alt' => $profile->getBestName())); - - $out->raw(' '); // avoid   for AJAX XML compatibility - - $out->elementStart('span', 'vcard author'); // hack for belongsOnTimeline; JS needs to be able to find the author - $out->element('a', - array('class' => 'url', - 'href' => $profile->profileurl, - 'title' => $profile->getBestName()), - $profile->nickname); - $out->elementEnd('span'); - } - - function showAnswerNotice($notice, $out) - { - $rsvp = Answer::fromNotice($notice); - - $out->elementStart('div', 'answer'); - $out->raw($answer->asHTML()); - $out->elementEnd('div'); - return; - } - - function showQuestionNotice($notice, $out) - { - $profile = $notice->getProfile(); - $question = 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'); - - // XXX I dunno - - $out->elementEnd('div'); - - $user = common_current_user(); - - if (!empty($user)) { - $question = $question->getAnswer($user->getProfile()); - - if (empty($answer)) { - $form = new AnswerForm($question, $out); - } - - $form->show(); - } - - $out->elementEnd('div'); - } - - /** - * Form for our app - * - * @param HTMLOutputter $out - * @return Widget - */ - - function entryForm($out) - { - return new QuestionForm($out); - } - - /** - * When a notice is deleted, clean up related tables. - * - * @param Notice $notice - */ - - function deleteRelated($notice) - { - switch ($notice->object_type) { - case Question::OBJECT_TYPE: - common_log(LOG_DEBUG, "Deleting question from notice..."); - $question = Question::fromNotice($notice); - $question->delete(); - break; - case Answer::NORMAL: - case Answer::ANONYMOUS: - common_log(LOG_DEBUG, "Deleting answer from notice..."); - $answer = 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/questionandanswer.css')); - return true; - } -} diff --git a/plugins/QnA/actions/newquestion.php b/plugins/QnA/actions/newquestion.php index 83b1022d6b..0a486dfa43 100644 --- a/plugins/QnA/actions/newquestion.php +++ b/plugins/QnA/actions/newquestion.php @@ -48,8 +48,8 @@ class NewquestionAction extends Action protected $user = null; protected $error = null; protected $complete = null; - - protected $question = null; + protected $title = null; + protected $description = null; /** * Returns the title of the action @@ -77,14 +77,19 @@ class NewquestionAction extends Action if (empty($this->user)) { // TRANS: Client exception thrown trying to create a Question while not logged in. - throw new ClientException(_m('You must be logged in to post a question.'), - 403); + throw new ClientException( + _m('You must be logged in to post a question.'), + 403 + ); } if ($this->isPost()) { $this->checkSessionToken(); } + $this->title = $this->trimmed('title'); + $this->description = $this->trimmed('description'); + return true; } @@ -119,14 +124,15 @@ class NewquestionAction extends Action StatusNet::setApi(true); } try { - if (empty($this->question)) { - // TRANS: Client exception thrown trying to create a Question without a question. - throw new ClientException(_m('Question must have a question.')); + if (empty($this->title)) { + // TRANS: Client exception thrown trying to create a question without a title. + throw new ClientException(_m('Question must have a title.')); } - $saved = Question::saveNew( + $saved = QnA_Question::saveNew( $this->user->getProfile(), - $this->question + $this->title, + $this->description ); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -140,7 +146,7 @@ class NewquestionAction extends Action $this->elementStart('html'); $this->elementStart('head'); // TRANS: Page title after sending a notice. - $this->element('title', null, _m('Notice posted')); + $this->element('title', null, _m('Question posted')); $this->elementEnd('head'); $this->elementStart('body'); $this->showNotice($saved); @@ -178,10 +184,10 @@ class NewquestionAction extends Action $this->element('p', 'error', $this->error); } - $form = new NewQuestionForm( + $form = new QuestionForm( $this, - $this->question, - $this->options + $this->title, + $this->description ); $form->show(); @@ -208,4 +214,3 @@ class NewquestionAction extends Action } } } - diff --git a/plugins/QnA/actions/qnavote.php b/plugins/QnA/actions/qnavote.php index 17e841e545..6c1b9f053e 100644 --- a/plugins/QnA/actions/qnavote.php +++ b/plugins/QnA/actions/qnavote.php @@ -3,7 +3,7 @@ * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2011, StatusNet, Inc. * - * Answer a question + * Vote on a questino or answer * * PHP version 5 * @@ -20,7 +20,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category QuestonAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -34,7 +34,7 @@ if (!defined('STATUSNET')) { } /** - * Answer a question + * Vote on a question or answer * * @category QnA * @package StatusNet @@ -43,13 +43,13 @@ 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 Qnavote extends Action { protected $user = null; protected $error = null; protected $complete = null; - protected $qustion = null; + protected $question = null; protected $answer = null; /** diff --git a/plugins/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php index d88e6bda41..102af70057 100644 --- a/plugins/QnA/classes/QnA_Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -44,7 +44,7 @@ if (!defined('STATUSNET')) { */ class QnA_Answer extends Managed_DataObject { - CONST ANSWER = 'http://activityschema.org/object/answer'; + const OBJECT_TYPE = 'http://activityschema.org/object/answer'; public $__table = 'qna_answer'; // table name public $id; // char(36) primary key not null -> UUID @@ -162,6 +162,11 @@ class QnA_Answer extends Managed_DataObject return Question::staticGet('id', $this->question_id); } + static function fromNotice($notice) + { + return QnA_Answer::staticGet('uri', $notice->uri); + } + /** * Save a new answer notice * @@ -171,32 +176,32 @@ class QnA_Answer extends Managed_DataObject * * @return Notice saved notice */ - static function saveNew($profile, $question, $options=null) + static function saveNew($profile, $question, $options = null) { if (empty($options)) { $options = array(); } - $a = new Answer(); - $a->id = UUID::gen(); - $a->profile_id = $profile->id; - $a->question_id = $question->id; - $a->created = common_sql_now(); - $a->uri = common_local_url( + $answer = new Answer(); + $answer->id = UUID::gen(); + $answer->profile_id = $profile->id; + $answer->question_id = $question->id; + $answer->created = common_sql_now(); + $answer->uri = common_local_url( 'showanswer', - array('id' => $pr->id) + array('id' => $answer->id) ); - common_log(LOG_DEBUG, "Saving answer: $pr->id $pr->uri"); - $a->insert(); + common_log(LOG_DEBUG, "Saving answer: $answer->id, $answer->uri"); + $answer->insert(); // TRANS: Notice content answering a question. // TRANS: %s is the answer $content = sprintf( _m('answered "%s"'), - $answer + $answer->uri ); - $link = '' . htmlspecialchars($answer) . ''; + $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. $rendered = sprintf(_m('answered "%s"'), $link); @@ -206,17 +211,17 @@ class QnA_Answer extends Managed_DataObject $options = array_merge( array( - 'urls' => array(), - 'rendered' => $rendered, - 'tags' => $tags, - 'replies' => $replies, - 'reply_to' => $question->getNotice()->id, - 'object_type' => QnA::ANSWER_OBJECT), + 'urls' => array(), + 'rendered' => $rendered, + 'tags' => $tags, + 'replies' => $replies, + 'reply_to' => $question->getNotice()->id, + 'object_type' => self::OBJECT_TYPE), $options ); if (!array_key_exists('uri', $options)) { - $options['uri'] = $pr->uri; + $options['uri'] = $answer->uri; } $saved = Notice::saveNew( diff --git a/plugins/QnA/classes/QnA_Question.php b/plugins/QnA/classes/QnA_Question.php index 1a298ae4e9..308e87b99f 100644 --- a/plugins/QnA/classes/QnA_Question.php +++ b/plugins/QnA/classes/QnA_Question.php @@ -45,9 +45,8 @@ if (!defined('STATUSNET')) { class QnA_Question extends Managed_DataObject { - - const QUESTION = 'http://activityschema.org/object/question'; - + const OBJECT_TYPE = 'http://activityschema.org/object/question'; + public $__table = 'qna_question'; // table name public $id; // char(36) primary key not null -> UUID public $uri; @@ -99,22 +98,22 @@ class QnA_Question extends Managed_DataObject 'description' => 'Per-notice question data for QNA plugin', 'fields' => array( 'id' => array( - 'type' => 'char', - 'length' => 36, - 'not null' => true, + 'type' => 'char', + 'length' => 36, + 'not null' => true, 'description' => 'UUID' ), 'uri' => array( - 'type' => 'varchar', - 'length' => 255, + 'type' => 'varchar', + 'length' => 255, 'not null' => true ), 'profile_id' => array('type' => 'int'), 'title' => array('type' => 'text'), - 'closed' => array('type' => 'int', size => 'tiny'), + 'closed' => array('type' => 'int', 'size' => 'tiny'), 'description' => array('type' => 'text'), 'created' => array( - 'type' => 'datetime', + 'type' => 'datetime', 'not null' => true ), ), @@ -174,6 +173,12 @@ class QnA_Question extends Managed_DataObject return $a-count(); } + static function fromNotice($notice) + { + common_debug('xxxxxxxxxxxxxxx notice-uri = ' . $notice->uri); + return QnA_Question::staticGet('uri', $notice->uri); + } + /** * Save a new question notice * @@ -185,7 +190,7 @@ class QnA_Question extends Managed_DataObject * * @return Notice saved notice */ - static function saveNew($profile, $question, $title, $description, $options = array()) + static function saveNew($profile, $title, $description, $options = array()) { $q = new QnA_Question(); @@ -219,7 +224,7 @@ class QnA_Question extends Managed_DataObject $title, $q->uri ); - + $link = '' . htmlspecialchars($title) . ''; // TRANS: Rendered version of the notice content creating a question. // TRANS: %s a link to the question as link description. @@ -234,13 +239,13 @@ class QnA_Question extends Managed_DataObject 'rendered' => $rendered, 'tags' => $tags, 'replies' => $replies, - 'object_type' => QnAPlugin::QUESTION_OBJECT + 'object_type' => self::OBJECT_TYPE ), $options ); if (!array_key_exists('uri', $options)) { - $options['uri'] = $p->uri; + $options['uri'] = $q->uri; } $saved = Notice::saveNew( diff --git a/plugins/QnA/classes/QnA_Vote.php b/plugins/QnA/classes/QnA_Vote.php index ec2e75afbb..ad579666b8 100644 --- a/plugins/QnA/classes/QnA_Vote.php +++ b/plugins/QnA/classes/QnA_Vote.php @@ -47,11 +47,11 @@ class QnA_Vote extends Managed_DataObject const UP = 'http://activitystrea.ms/schema/1.0/like'; const DOWN = 'http://activityschema.org/object/dislike'; // Gar! - public $__table = 'qa_vote'; // table name + public $__table = 'qna_vote'; // table name public $id; // char(36) primary key not null -> UUID public $question_id; // char(36) -> question.id UUID public $answer_id; // char(36) -> question.id UUID - public $type // tinyint -> vote: up (1) or down (-1) + public $type; // tinyint -> vote: up (1) or down (-1) public $profile_id; // int -> question.id public $created; // datetime diff --git a/plugins/QnA/lib/answerform.php b/plugins/QnA/lib/answerform.php index 554f698d99..d4f28bb6d2 100644 --- a/plugins/QnA/lib/answerform.php +++ b/plugins/QnA/lib/answerform.php @@ -103,7 +103,7 @@ class AnswerForm extends Form $out = $this->out; $id = "question-" . $question->id; - $out->element('p', 'answer', $question->question); + $out->element('p', 'answer', $question->title); $out->element('input', array('type' => 'text', 'name' => 'answer')); } diff --git a/plugins/QnA/lib/questionform.php b/plugins/QnA/lib/questionform.php index 4f9ea6d808..a26bbb17be 100644 --- a/plugins/QnA/lib/questionform.php +++ b/plugins/QnA/lib/questionform.php @@ -20,7 +20,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category QuestonAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -44,9 +44,10 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class NewQuestionForm extends Form +class QuestionForm extends Form { - protected $question = null; + protected $title; + protected $description; /** * Construct a new question form @@ -55,9 +56,11 @@ class NewQuestionForm extends Form * * @return void */ - function __construct($out=null, $question=null, $options=null) + function __construct($out = null, $title = null, $description = null, $options = null) { parent::__construct($out); + $this->title = $title; + $this->description = $description; } /** @@ -101,12 +104,20 @@ class NewQuestionForm extends Form $this->out->elementStart('ul', 'form_data'); $this->li(); - $this->out->input('question', - // TRANS: Field label on the page to create a question. - _m('Question'), - $this->question, - // TRANS: Field title on the page to create a question. - _m('What is your question?')); + $this->out->input( + 'title', + _m('Title'), + $this->title, + _m('Title of your question') + ); + $this->unli(); + $this->li(); + $this->out->textarea( + 'description', + _m('Description'), + $this->description, + _m('Your question in detail') + ); $this->unli(); $this->out->elementEnd('ul');