From 30cdc6944b162e83c8151d38dde45bf49c54be29 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 16 Mar 2011 18:15:45 -0700 Subject: [PATCH 1/9] Skeleton / Stub for Question and Answers micro-app plugin --- .../QuestionAndAnswerPlugin.php | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) create mode 100644 plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php diff --git a/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php b/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php new file mode 100644 index 0000000000..0d7cb96c91 --- /dev/null +++ b/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php @@ -0,0 +1,429 @@ +. + * + * @category QuestionAndAnswer + * @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 QuestionAndAnswerPlugin 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('question', Question::schemaDef()); + $schema->ensureTable('answer', Answer::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': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'QuestionForm': + case 'AnswerForm': + include_once $dir . '/'.strtolower($cls).'.php'; + break; + case 'Question': + case 'Answer': + include_once $dir . '/'.$cls.'.php'; + return false; + 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) + { + $m->connect('main/question/new', + array('action' => 'newquestion')); + $m->connect('main/question/answer', + array('action' => 'newanswer')); + $m->connect('question/:id', + array('action' => 'showquestion'), + array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + $m->connect('answer/:id', + array('action' => 'showanswer'), + array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array( + 'name' => 'QuestionAndAnswer', + 'version' => STATUSNET_VERSION, + 'author' => 'Zach Copley', + 'homepage' => 'http://status.net/wiki/Plugin:QuestionAndAnswer', + 'description' => + _m('Question and Answers micro-app.') + ); + return true; + } + + function appTitle() { + return _m('Question'); + } + + function tag() { + return 'question'; + } + + function types() { + /* + return array(Happening::OBJECT_TYPE, + RSVP::POSITIVE, + RSVP::NEGATIVE, + RSVP::POSSIBLE); + + */ + } + + /** + * 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 != 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: + case Answer::ANONYMOUS: + $question = Question::staticGet('uri', $questionObj->id); + if (empty($question)) { + // FIXME: save the question + throw new Exception("Answer to unknown question."); + } + $notice = 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('questionandanswer.css')); + return true; + } +} From 46793caf4b8ce2bb176bd9684ff43781128d4023 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 17 Mar 2011 17:43:13 -0700 Subject: [PATCH 2/9] Most objects and forms are in place, now I just have to make it work. --- .../QuestionAndAnswerPlugin.php | 21 +- .../QuestionAndAnswer/actions/Newquestion.php | 211 +++++++++++++++ plugins/QuestionAndAnswer/actions/answer.php | 198 +++++++++++++++ .../QuestionAndAnswer/actions/showanswer.php | 125 +++++++++ .../actions/showquestion.php | 130 ++++++++++ plugins/QuestionAndAnswer/classes/Answer.php | 213 ++++++++++++++++ .../QuestionAndAnswer/classes/Question.php | 240 ++++++++++++++++++ .../css/questionandanswer.css | 1 + plugins/QuestionAndAnswer/lib/answerform.php | 122 +++++++++ .../QuestionAndAnswer/lib/questionform.php | 126 +++++++++ 10 files changed, 1376 insertions(+), 11 deletions(-) create mode 100644 plugins/QuestionAndAnswer/actions/Newquestion.php create mode 100644 plugins/QuestionAndAnswer/actions/answer.php create mode 100644 plugins/QuestionAndAnswer/actions/showanswer.php create mode 100644 plugins/QuestionAndAnswer/actions/showquestion.php create mode 100644 plugins/QuestionAndAnswer/classes/Answer.php create mode 100644 plugins/QuestionAndAnswer/classes/Question.php create mode 100644 plugins/QuestionAndAnswer/css/questionandanswer.css create mode 100644 plugins/QuestionAndAnswer/lib/answerform.php create mode 100644 plugins/QuestionAndAnswer/lib/questionform.php diff --git a/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php b/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php index 0d7cb96c91..e519dac64f 100644 --- a/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php +++ b/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php @@ -81,16 +81,18 @@ class QuestionAndAnswerPlugin extends MicroappPlugin case 'NewanswerAction': case 'ShowquestionAction': case 'ShowanswerAction': - include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + include_once $dir . '/actions/' + . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'QuestionForm': case 'AnswerForm': - include_once $dir . '/'.strtolower($cls).'.php'; + include_once $dir . '/lib/' . strtolower($cls).'.php'; break; case 'Question': case 'Answer': - include_once $dir . '/'.$cls.'.php'; + include_once $dir . '/classes/' . $cls.'.php'; return false; + break; default: return true; } @@ -141,13 +143,10 @@ class QuestionAndAnswerPlugin extends MicroappPlugin } function types() { - /* - return array(Happening::OBJECT_TYPE, - RSVP::POSITIVE, - RSVP::NEGATIVE, - RSVP::POSSIBLE); - - */ + return array( + Question::OBJECT_TYPE, + Answer::NORMAL + ); } /** @@ -423,7 +422,7 @@ class QuestionAndAnswerPlugin extends MicroappPlugin function onEndShowStyles($action) { - $action->cssLink($this->path('questionandanswer.css')); + $action->cssLink($this->path('css/questionandanswer.css')); return true; } } diff --git a/plugins/QuestionAndAnswer/actions/Newquestion.php b/plugins/QuestionAndAnswer/actions/Newquestion.php new file mode 100644 index 0000000000..cd1c2ffb13 --- /dev/null +++ b/plugins/QuestionAndAnswer/actions/Newquestion.php @@ -0,0 +1,211 @@ +. + * + * @category QuestionAndAnswer + * @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); +} + +/** + * Add a new Question + * + * @category Plugin + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class NewquestionAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $question = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + function title() + { + // TRANS: Title for Question page. + return _m('New question'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + + $this->user = common_current_user(); + + 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); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->newQuestion(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new Question + * + * @return void + */ + function newQuestion() + { + if ($this->boolean('ajax')) { + 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.')); + } + + $saved = Question::saveNew( + $this->user->getProfile(), + $this->question + ); + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending a notice. + $this->element('title', null, _m('Notice posted')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->showNotice($saved); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($saved->bestUrl(), 303); + } + } + + /** + * Output a notice + * + * Used to generate the notice code for Ajax results. + * + * @param Notice $notice Notice that was saved + * + * @return void + */ + function showNotice($notice) + { + class_exists('NoticeList'); // @fixme hack for autoloader + $nli = new NoticeListItem($notice, $this); + $nli->show(); + } + + /** + * Show the Question form + * + * @return void + */ + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new NewQuestionForm( + $this, + $this->question, + $this->options + ); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } +} + diff --git a/plugins/QuestionAndAnswer/actions/answer.php b/plugins/QuestionAndAnswer/actions/answer.php new file mode 100644 index 0000000000..49bb73aa54 --- /dev/null +++ b/plugins/QuestionAndAnswer/actions/answer.php @@ -0,0 +1,198 @@ +. + * + * @category QuestonAndAnswer + * @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); +} + +/** + * Answer a question + * + * @category QuestionAndAnswer + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AnswerAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $qustion = null; + protected $answer = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + function title() + { + // TRANS: Page title for and answer to a question. + return _m('Answer'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); + } + + $this->user = common_current_user(); + + 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); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + $id = $this->trimmed('id'); + $this->question = 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); + } + + $answer = $this->trimmed('answer'); + + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->answer(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new answer + * + * @return void + */ + function answer() + { + try { + $notice = Answer::saveNew( + $this->user->getProfile(), + $this->question, + $this->answer + ); + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending an answer. + $this->element('title', null, _m('Answers')); + $this->elementEnd('head'); + $this->elementStart('body'); + $form = new Answer($this->question, $this); + $form->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($this->question->bestUrl(), 303); + } + } + + /** + * Show the Answer form + * + * @return void + */ + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new AnswerForm($this->question, $this); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } +} diff --git a/plugins/QuestionAndAnswer/actions/showanswer.php b/plugins/QuestionAndAnswer/actions/showanswer.php new file mode 100644 index 0000000000..d3202cd51d --- /dev/null +++ b/plugins/QuestionAndAnswer/actions/showanswer.php @@ -0,0 +1,125 @@ +. + * + * @category QuestionAndAnswer + * @package StatusNet + * @author Zach Copley + * @copyright 2010 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); +} + +/** + * Show an answer to a question, and associated data + * + * @category QuestionAndAnswer + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +class ShowAnswerAction extends ShownoticeAction +{ + protected $answer = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + OwnerDesignAction::prepare($argarray); + + $this->id = $this->trimmed('id'); + + $this->answer = Answer::staticGet('id', $this->id); + + if (empty($this->answer)) { + throw new ClientException(_('No such answer.'), 404); + } + + $this->notice = Notice::staticGet('uri', $this->answer->uri); + + if (empty($this->notice)) { + // Did we used to have it, and it got deleted? + throw new ClientException(_('No such answer.'), 404); + } + + $this->user = User::staticGet('id', $this->answer->profile_id); + + if (empty($this->user)) { + throw new ClientException(_('No such user.'), 404); + } + + $this->profile = $this->user->getProfile(); + + if (empty($this->profile)) { + throw new ServerException(_('User without a profile.')); + } + + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + return true; + } + + /** + * Title of the page + * + * Used by Action class for layout. + * + * @return string page tile + */ + + function title() + { + return sprintf(_('%s\'s answer to "%s"'), + $this->user->nickname, + $this->answer->title); + } + + /** + * Overload page title display to show answer link + * + * @return void + */ + + function showPageTitle() + { + $this->elementStart('h1'); + $this->element('a', + array('href' => $this->answer->url), + $this->asnwer->title); + $this->elementEnd('h1'); + } +} diff --git a/plugins/QuestionAndAnswer/actions/showquestion.php b/plugins/QuestionAndAnswer/actions/showquestion.php new file mode 100644 index 0000000000..50f56fd161 --- /dev/null +++ b/plugins/QuestionAndAnswer/actions/showquestion.php @@ -0,0 +1,130 @@ +. + * + * @category QuestionAndAnswer + * @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); +} + +/** + * Show a question + * + * @category QuestionAndAnswer + * @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 ShowquestionAction extends ShownoticeAction +{ + protected $question = null; + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + OwnerDesignAction::prepare($argarray); + + $this->id = $this->trimmed('id'); + + $this->question = Question::staticGet('id', $this->id); + + if (empty($this->question)) { + // TRANS: Client exception thrown trying to view a non-existing question. + throw new ClientException(_m('No such question.'), 404); + } + + $this->notice = $this->question->getNotice(); + + if (empty($this->notice)) { + // Did we used to have it, and it got deleted? + // TRANS: Client exception thrown trying to view a non-existing question notice. + throw new ClientException(_m('No such question notice.'), 404); + } + + $this->user = User::staticGet('id', $this->question->profile_id); + + if (empty($this->user)) { + // TRANS: Client exception thrown trying to view a question of a non-existing user. + throw new ClientException(_m('No such user.'), 404); + } + + $this->profile = $this->user->getProfile(); + + if (empty($this->profile)) { + // TRANS: Server exception thrown trying to view a question for a user for which the profile could not be loaded. + throw new ServerException(_m('User without a profile.')); + } + + $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE); + + return true; + } + + /** + * Title of the page + * + * Used by Action class for layout. + * + * @return string page tile + */ + function title() + { + // TRANS: Page title for a question. + // TRANS: %1$s is the nickname of the user who asked the question, %2$s is the question. + return sprintf(_m('%1$s\'s question: %2$s'), + $this->user->nickname, + $this->question->question); + } + + /** + * @fixme combine the notice time with question update time + */ + function lastModified() + { + return Action::lastModified(); + } + + + /** + * @fixme combine the notice time with question update time + */ + function etag() + { + return Action::etag(); + } +} diff --git a/plugins/QuestionAndAnswer/classes/Answer.php b/plugins/QuestionAndAnswer/classes/Answer.php new file mode 100644 index 0000000000..45e52d0d39 --- /dev/null +++ b/plugins/QuestionAndAnswer/classes/Answer.php @@ -0,0 +1,213 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * For storing answers + * + * @category QuestionAndAnswer + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ +class Answer extends Managed_DataObject +{ + public $__table = 'answer'; // table name + public $id; // char(36) primary key not null -> UUID + public $question_id; // char(36) -> question.id UUID + public $profile_id; // int -> question.id + public $votes; // int -> total number of votes (up & down) + public $best; // (int) boolean -> whether the question asker has marked this as the best answer + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Answer', $k, $v); + } + + /** + * Get an instance by compound key + * + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits + * + */ + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Answer', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'Record of answers to questions', + 'fields' => array( + 'id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID of the response'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'UUID to the answer notice'), + 'question_id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID of question being responded to'), + 'votes' => array('type' => 'int'), + 'best' => array('type' => 'int'), + 'profile_id' => array('type' => 'int'), + 'created' => array('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'question_uri_key' => array('uri'), + 'question_id_profile_id_key' => array('question_id', 'profile_id'), + ), + 'indexes' => array( + 'profile_id_question_Id_index' => array('profile_id', 'question_id'), + ) + ); + } + + /** + * Get an answer based on a notice + * + * @param Notice $notice Notice to check for + * + * @return Answer found response or null + */ + function getByNotice($notice) + { + return self::staticGet('uri', $notice->uri); + } + + /** + * Get the notice that belongs to this answer + * + * @return Notice + */ + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + function bestUrl() + { + return $this->getNotice()->bestUrl(); + } + + /** + * Get the Question this is an answer to + * + * @return Question + */ + function getQuestion() + { + return Question::staticGet('id', $this->question_id); + } + /** + * Save a new answer notice + * + * @param Profile $profile + * @param Question $Question the question being answered + * @param array + * + * @return Notice saved notice + */ + 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( + 'showanswer', + array('id' => $pr->id) + ); + + common_log(LOG_DEBUG, "Saving answer: $pr->id $pr->uri"); + $a->insert(); + + // TRANS: Notice content answering a question. + // TRANS: %s is the answer + $content = sprintf( + _m('answered "%s"'), + $answer + ); + $link = '' . htmlspecialchars($answer) . ''; + // TRANS: Rendered version of the notice content answering a question. + // TRANS: %s a link to the question with the chosen option as link description. + $rendered = sprintf(_m('answered "%s"'), $link); + + $tags = array(); + $replies = array(); + + $options = array_merge(array('urls' => array(), + 'rendered' => $rendered, + 'tags' => $tags, + 'replies' => $replies, + 'reply_to' => $question->getNotice()->id, + 'object_type' => QuestionAndAnswer::ANSWER_OBJECT), + $options); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $pr->uri; + } + + $saved = Notice::saveNew($profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options); + + return $saved; + } +} diff --git a/plugins/QuestionAndAnswer/classes/Question.php b/plugins/QuestionAndAnswer/classes/Question.php new file mode 100644 index 0000000000..95ceeb45e2 --- /dev/null +++ b/plugins/QuestionAndAnswer/classes/Question.php @@ -0,0 +1,240 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * For storing a question + * + * @category QuestionAndAnswer + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class Question extends Managed_DataObject +{ + public $__table = 'question'; // table name + public $id; // char(36) primary key not null -> UUID + public $uri; + public $profile_id; // int -> profile.id + public $title; // text + public $description; // text + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('Question', $k, $v); + } + + /** + * Get an instance by compound key + * + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return Bookmark object found, or null for no hits + * + */ + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Question', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'Per-notice question data for QuestionAndAnswer plugin', + 'fields' => array( + 'id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID'), + 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), + 'profile_id' => array('type' => 'int'), + 'title' => array('type' => 'text'), + 'description' => array('type' => 'text'), + 'created' => array('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'question_uri_key' => array('uri'), + ), + ); + } + + /** + * Get a question based on a notice + * + * @param Notice $notice Notice to check for + * + * @return Question found question or null + */ + function getByNotice($notice) + { + return self::staticGet('uri', $notice->uri); + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + function bestUrl() + { + return $this->getNotice()->bestUrl(); + } + + /** + * Get the answer from a particular user to this question, if any. + * + * @param Profile $profile + * + * @return Answer object or null + */ + function getAnswer(Profile $profile) + { + $a = new Answer(); + $a->question_id = $this->id; + $a->profile_id = $profile->id; + $a->find(); + if ($a->fetch()) { + return $a; + } else { + return null; + } + } + + function countAnswers() + { + $a = new Answer(); + + $a->question_id = $this->id; + return $a-count(); + } + + /** + * Save a new question notice + * + * @param Profile $profile + * @param string $question + * @param string $title + * @param string $description + * @param array $option // and whatnot + * + * @return Notice saved notice + */ + static function saveNew($profile, $question, $title, $description, $options = array()) + { + $q = new Question(); + + $q->id = UUID::gen(); + $q->profile_id = $profile->id; + $q->title = $title; + $q->description = $description; + + if (array_key_exists('created', $options)) { + $q->created = $options['created']; + } else { + $q->created = common_sql_now(); + } + + if (array_key_exists('uri', $options)) { + $q->uri = $options['uri']; + } else { + $q->uri = common_local_url( + 'showquestion', + array('id' => $q->id) + ); + } + + common_log(LOG_DEBUG, "Saving question: $q->id $q->uri"); + $q->insert(); + + // TRANS: Notice content creating a question. + // TRANS: %1$s is the title of the question, %2$s is a link to the question. + $content = sprintf( + _m('question: %1$s %2$s'), + $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. + $rendered = sprintf(_m('Question: %s'), $link); + + $tags = array('question'); + $replies = array(); + + $options = array_merge( + array( + 'urls' => array(), + 'rendered' => $rendered, + 'tags' => $tags, + 'replies' => $replies, + 'object_type' => QuestionAndAnswerPlugin::QUESTION_OBJECT + ), + $options + ); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $p->uri; + } + + $saved = Notice::saveNew( + $profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options + ); + + return $saved; + } +} diff --git a/plugins/QuestionAndAnswer/css/questionandanswer.css b/plugins/QuestionAndAnswer/css/questionandanswer.css new file mode 100644 index 0000000000..4701b5ab03 --- /dev/null +++ b/plugins/QuestionAndAnswer/css/questionandanswer.css @@ -0,0 +1 @@ +/* stubb for q&a css */ diff --git a/plugins/QuestionAndAnswer/lib/answerform.php b/plugins/QuestionAndAnswer/lib/answerform.php new file mode 100644 index 0000000000..d093863708 --- /dev/null +++ b/plugins/QuestionAndAnswer/lib/answerform.php @@ -0,0 +1,122 @@ +. + * + * @category QuestionAndAnswer + * @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); +} + +/** + * Form to add a new answer to a question + * + * @category QuestionAndAnswer + * @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 AnswerForm extends Form +{ + protected $question; + + /** + * Construct a new answer form + * + * @param Question $question + * @param HTMLOutputter $out output channel + * + * @return void + */ + function __construct(Question $question, HTMLOutputter $out) + { + parent::__construct($out); + $this->question = $question; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'answer-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('answer', array('id' => $this->question->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $question = $this->question; + $out = $this->out; + $id = "question-" . $question->id; + + $out->element('p', 'answer', $question->question); + $out->element('input', array('type' => 'text', 'name' => 'answer')); + + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text for submitting a poll response. + $this->out->submit('submit', _m('BUTTON', 'Submit')); + } +} + diff --git a/plugins/QuestionAndAnswer/lib/questionform.php b/plugins/QuestionAndAnswer/lib/questionform.php new file mode 100644 index 0000000000..5892464218 --- /dev/null +++ b/plugins/QuestionAndAnswer/lib/questionform.php @@ -0,0 +1,126 @@ +. + * + * @category QuestonAndAnswer + * @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); +} + +/** + * Form to add a new question + * + * @category QuestionAndAnswer + * @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 NewQuestionForm extends Form +{ + protected $question = null; + + /** + * Construct a new question form + * + * @param HTMLOutputter $out output channel + * + * @return void + */ + function __construct($out=null, $question=null, $options=null) + { + parent::__construct($out); + } + + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'newquestion-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings ajax-notice'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('newquestion'); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $this->out->elementStart('fieldset', array('id' => 'newquestion-data')); + $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->unli(); + + $this->out->elementEnd('ul'); + $this->out->elementEnd('fieldset'); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text for saving a new question. + $this->out->submit('submit', _m('BUTTON', 'Save')); + } +} From 2167454eb260d23ec532f7b8629a887a434edae8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Sun, 20 Mar 2011 19:24:35 -0700 Subject: [PATCH 3/9] Renamed QuestionAndAnswerPlugin to QnAPlugin --- .../QnAPlugin} | 75 +++++-- .../actions/answer.php | 2 +- .../actions/newquestion.php} | 2 +- plugins/QnA/actions/qnavote.php | 198 ++++++++++++++++++ .../actions/showanswer.php | 4 +- .../actions/showquestion.php | 4 +- .../Answer.php => QnA/classes/QnA_Answer.php} | 85 +++++--- .../classes/QnA_Question.php} | 54 +++-- plugins/QnA/classes/QnA_Vote.php | 160 ++++++++++++++ .../questionandanswer.css => QnA/css/qna.css} | 0 .../lib/answerform.php | 13 +- .../lib/questionform.php | 2 +- plugins/QnA/lib/voteform.php | 121 +++++++++++ 13 files changed, 631 insertions(+), 89 deletions(-) rename plugins/{QuestionAndAnswer/QuestionAndAnswerPlugin.php => QnA/QnAPlugin} (85%) rename plugins/{QuestionAndAnswer => QnA}/actions/answer.php (99%) rename plugins/{QuestionAndAnswer/actions/Newquestion.php => QnA/actions/newquestion.php} (99%) create mode 100644 plugins/QnA/actions/qnavote.php rename plugins/{QuestionAndAnswer => QnA}/actions/showanswer.php (98%) rename plugins/{QuestionAndAnswer => QnA}/actions/showquestion.php (98%) rename plugins/{QuestionAndAnswer/classes/Answer.php => QnA/classes/QnA_Answer.php} (68%) rename plugins/{QuestionAndAnswer/classes/Question.php => QnA/classes/QnA_Question.php} (79%) create mode 100644 plugins/QnA/classes/QnA_Vote.php rename plugins/{QuestionAndAnswer/css/questionandanswer.css => QnA/css/qna.css} (100%) rename plugins/{QuestionAndAnswer => QnA}/lib/answerform.php (92%) rename plugins/{QuestionAndAnswer => QnA}/lib/questionform.php (99%) create mode 100644 plugins/QnA/lib/voteform.php diff --git a/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php b/plugins/QnA/QnAPlugin similarity index 85% rename from plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php rename to plugins/QnA/QnAPlugin index e519dac64f..76bd304a87 100644 --- a/plugins/QuestionAndAnswer/QuestionAndAnswerPlugin.php +++ b/plugins/QnA/QnAPlugin @@ -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 QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -44,8 +44,13 @@ if (!defined('STATUSNET')) { * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class QuestionAndAnswerPlugin extends MicroappPlugin +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) * @@ -58,9 +63,10 @@ class QuestionAndAnswerPlugin extends MicroappPlugin { $schema = Schema::get(); - $schema->ensureTable('question', Question::schemaDef()); - $schema->ensureTable('answer', Answer::schemaDef()); - + $schema->ensureTable('qna_question', QnA_Question::schemaDef()); + $schema->ensureTable('qna_answer', QnA_Answer::schemaDef()); + $schema->ensureTable('qna_vote', QnA_Vote::schemaDef()); + return true; } @@ -81,15 +87,18 @@ class QuestionAndAnswerPlugin extends MicroappPlugin 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 'Question': - case 'Answer': + case 'QnA_Question': + case 'QnA_Answer': + case 'QnA_Vote': include_once $dir . '/classes/' . $cls.'.php'; return false; break; @@ -108,26 +117,47 @@ class QuestionAndAnswerPlugin extends MicroappPlugin function onRouterInitialized($m) { - $m->connect('main/question/new', - array('action' => 'newquestion')); - $m->connect('main/question/answer', - array('action' => 'newanswer')); - $m->connect('question/:id', - array('action' => 'showquestion'), - array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); - $m->connect('answer/:id', - array('action' => 'showanswer'), - array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + $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' => 'QuestionAndAnswer', + 'name' => 'QnA', 'version' => STATUSNET_VERSION, 'author' => 'Zach Copley', - 'homepage' => 'http://status.net/wiki/Plugin:QuestionAndAnswer', + 'homepage' => 'http://status.net/wiki/Plugin:QnA', 'description' => _m('Question and Answers micro-app.') ); @@ -167,7 +197,7 @@ class QuestionAndAnswerPlugin extends MicroappPlugin $questionObj = $activity->objects[0]; - if ($questinoObj->type != Question::OBJECT_TYPE) { + if ($questinoObj->type != QnA_Question::OBJECT_TYPE) { throw new Exception('Wrong type for object.'); } @@ -184,13 +214,12 @@ class QuestionAndAnswerPlugin extends MicroappPlugin ); break; case Answer::NORMAL: - case Answer::ANONYMOUS: - $question = Question::staticGet('uri', $questionObj->id); + $question = QnA_Question::staticGet('uri', $questionObj->id); if (empty($question)) { // FIXME: save the question throw new Exception("Answer to unknown question."); } - $notice = Answer::saveNew($actor, $question, $activity->verb, $options); + $notice = QnA_Answer::saveNew($actor, $question, $activity->verb, $options); break; default: throw new Exception("Unknown verb for question"); diff --git a/plugins/QuestionAndAnswer/actions/answer.php b/plugins/QnA/actions/answer.php similarity index 99% rename from plugins/QuestionAndAnswer/actions/answer.php rename to plugins/QnA/actions/answer.php index 49bb73aa54..17e841e545 100644 --- a/plugins/QuestionAndAnswer/actions/answer.php +++ b/plugins/QnA/actions/answer.php @@ -36,7 +36,7 @@ if (!defined('STATUSNET')) { /** * Answer a question * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2010 StatusNet, Inc. diff --git a/plugins/QuestionAndAnswer/actions/Newquestion.php b/plugins/QnA/actions/newquestion.php similarity index 99% rename from plugins/QuestionAndAnswer/actions/Newquestion.php rename to plugins/QnA/actions/newquestion.php index cd1c2ffb13..83b1022d6b 100644 --- a/plugins/QuestionAndAnswer/actions/Newquestion.php +++ b/plugins/QnA/actions/newquestion.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 QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. diff --git a/plugins/QnA/actions/qnavote.php b/plugins/QnA/actions/qnavote.php new file mode 100644 index 0000000000..17e841e545 --- /dev/null +++ b/plugins/QnA/actions/qnavote.php @@ -0,0 +1,198 @@ +. + * + * @category QuestonAndAnswer + * @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); +} + +/** + * Answer a question + * + * @category QnA + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class AnswerAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $qustion = null; + protected $answer = null; + + /** + * Returns the title of the action + * + * @return string Action title + */ + function title() + { + // TRANS: Page title for and answer to a question. + return _m('Answer'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + function prepare($argarray) + { + parent::prepare($argarray); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); + } + + $this->user = common_current_user(); + + 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); + } + + if ($this->isPost()) { + $this->checkSessionToken(); + } + + $id = $this->trimmed('id'); + $this->question = 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); + } + + $answer = $this->trimmed('answer'); + + + return true; + } + + /** + * Handler method + * + * @param array $argarray is ignored since it's now passed in in prepare() + * + * @return void + */ + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->answer(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new answer + * + * @return void + */ + function answer() + { + try { + $notice = Answer::saveNew( + $this->user->getProfile(), + $this->question, + $this->answer + ); + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after sending an answer. + $this->element('title', null, _m('Answers')); + $this->elementEnd('head'); + $this->elementStart('body'); + $form = new Answer($this->question, $this); + $form->show(); + $this->elementEnd('body'); + $this->elementEnd('html'); + } else { + common_redirect($this->question->bestUrl(), 303); + } + } + + /** + * Show the Answer form + * + * @return void + */ + function showContent() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + + $form = new AnswerForm($this->question, $this); + + $form->show(); + + return; + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } + } +} diff --git a/plugins/QuestionAndAnswer/actions/showanswer.php b/plugins/QnA/actions/showanswer.php similarity index 98% rename from plugins/QuestionAndAnswer/actions/showanswer.php rename to plugins/QnA/actions/showanswer.php index d3202cd51d..7686d6d566 100644 --- a/plugins/QuestionAndAnswer/actions/showanswer.php +++ b/plugins/QnA/actions/showanswer.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 QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2010 StatusNet, Inc. @@ -37,7 +37,7 @@ if (!defined('STATUSNET')) { /** * Show an answer to a question, and associated data * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2010 StatusNet, Inc. diff --git a/plugins/QuestionAndAnswer/actions/showquestion.php b/plugins/QnA/actions/showquestion.php similarity index 98% rename from plugins/QuestionAndAnswer/actions/showquestion.php rename to plugins/QnA/actions/showquestion.php index 50f56fd161..41c1d809fe 100644 --- a/plugins/QuestionAndAnswer/actions/showquestion.php +++ b/plugins/QnA/actions/showquestion.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 QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -37,7 +37,7 @@ if (!defined('STATUSNET')) { /** * Show a question * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. diff --git a/plugins/QuestionAndAnswer/classes/Answer.php b/plugins/QnA/classes/QnA_Answer.php similarity index 68% rename from plugins/QuestionAndAnswer/classes/Answer.php rename to plugins/QnA/classes/QnA_Answer.php index 45e52d0d39..d88e6bda41 100644 --- a/plugins/QuestionAndAnswer/classes/Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -4,7 +4,7 @@ * * PHP version 5 * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 @@ -34,7 +34,7 @@ if (!defined('STATUSNET')) { /** * For storing answers * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 @@ -42,13 +42,14 @@ if (!defined('STATUSNET')) { * * @see DB_DataObject */ -class Answer extends Managed_DataObject +class QnA_Answer extends Managed_DataObject { - public $__table = 'answer'; // table name + CONST ANSWER = '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 public $profile_id; // int -> question.id - public $votes; // int -> total number of votes (up & down) public $best; // (int) boolean -> whether the question asker has marked this as the best answer public $created; // datetime @@ -57,15 +58,15 @@ class Answer extends Managed_DataObject * * This is a utility method to get a single instance with a given key value. * - * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param string $k Key to use to lookup * @param mixed $v Value to lookup * - * @return User_greeting_count object found, or null for no hits + * @return QnA_Answer object found, or null for no hits * */ function staticGet($k, $v=null) { - return Memcached_DataObject::staticGet('Answer', $k, $v); + return Memcached_DataObject::staticGet('QnA_Answer', $k, $v); } /** @@ -77,12 +78,12 @@ class Answer extends Managed_DataObject * * @param array $kv array of key-value mappings * - * @return Bookmark object found, or null for no hits + * @return QA_Answer object found, or null for no hits * */ function pkeyGet($kv) { - return Memcached_DataObject::pkeyGet('Answer', $kv); + return Memcached_DataObject::pkeyGet('QnA_Answer', $kv); } /** @@ -93,13 +94,25 @@ class Answer extends Managed_DataObject return array( 'description' => 'Record of answers to questions', 'fields' => array( - 'id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID of the response'), - 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true, 'description' => 'UUID to the answer notice'), - 'question_id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID of question being responded to'), - 'votes' => array('type' => 'int'), - 'best' => array('type' => 'int'), - 'profile_id' => array('type' => 'int'), - 'created' => array('type' => 'datetime', 'not null' => true), + 'id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, 'description' => 'UUID of the response'), + 'uri' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => true, + 'description' => 'UUID to the answer notice' + ), + 'question_id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID of question being responded to' + ), + 'best' => array('type' => 'int', 'size' => 'tiny'), + 'profile_id' => array('type' => 'int'), + 'created' => array('type' => 'datetime', 'not null' => true), ), 'primary key' => array('id'), 'unique keys' => array( @@ -107,7 +120,7 @@ class Answer extends Managed_DataObject 'question_id_profile_id_key' => array('question_id', 'profile_id'), ), 'indexes' => array( - 'profile_id_question_Id_index' => array('profile_id', 'question_id'), + 'profile_id_question_id_index' => array('profile_id', 'question_id'), ) ); } @@ -117,7 +130,7 @@ class Answer extends Managed_DataObject * * @param Notice $notice Notice to check for * - * @return Answer found response or null + * @return QnA_Answer found response or null */ function getByNotice($notice) { @@ -142,12 +155,13 @@ class Answer extends Managed_DataObject /** * Get the Question this is an answer to * - * @return Question + * @return QnA_Question */ function getQuestion() { return Question::staticGet('id', $this->question_id); } + /** * Save a new answer notice * @@ -184,29 +198,34 @@ class Answer extends Managed_DataObject ); $link = '' . htmlspecialchars($answer) . ''; // TRANS: Rendered version of the notice content answering a question. - // TRANS: %s a link to the question with the chosen option as link description. + // TRANS: %s a link to the question with question title as the link content. $rendered = sprintf(_m('answered "%s"'), $link); $tags = array(); $replies = array(); - $options = array_merge(array('urls' => array(), - 'rendered' => $rendered, - 'tags' => $tags, - 'replies' => $replies, - 'reply_to' => $question->getNotice()->id, - 'object_type' => QuestionAndAnswer::ANSWER_OBJECT), - $options); + $options = array_merge( + array( + 'urls' => array(), + 'rendered' => $rendered, + 'tags' => $tags, + 'replies' => $replies, + 'reply_to' => $question->getNotice()->id, + 'object_type' => QnA::ANSWER_OBJECT), + $options + ); if (!array_key_exists('uri', $options)) { $options['uri'] = $pr->uri; } - $saved = Notice::saveNew($profile->id, - $content, - array_key_exists('source', $options) ? - $options['source'] : 'web', - $options); + $saved = Notice::saveNew( + $profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options + ); return $saved; } diff --git a/plugins/QuestionAndAnswer/classes/Question.php b/plugins/QnA/classes/QnA_Question.php similarity index 79% rename from plugins/QuestionAndAnswer/classes/Question.php rename to plugins/QnA/classes/QnA_Question.php index 95ceeb45e2..1a298ae4e9 100644 --- a/plugins/QuestionAndAnswer/classes/Question.php +++ b/plugins/QnA/classes/QnA_Question.php @@ -4,7 +4,7 @@ * * PHP version 5 * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 @@ -34,7 +34,7 @@ if (!defined('STATUSNET')) { /** * For storing a question * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 @@ -43,14 +43,18 @@ if (!defined('STATUSNET')) { * @see DB_DataObject */ -class Question extends Managed_DataObject +class QnA_Question extends Managed_DataObject { - public $__table = 'question'; // table name + + const QUESTION = 'http://activityschema.org/object/question'; + + public $__table = 'qna_question'; // table name public $id; // char(36) primary key not null -> UUID public $uri; public $profile_id; // int -> profile.id public $title; // text public $description; // text + public $closed; // int (boolean) whether a question is closed public $created; // datetime /** @@ -58,15 +62,15 @@ class Question extends Managed_DataObject * * This is a utility method to get a single instance with a given key value. * - * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param string $k Key to use to lookup * @param mixed $v Value to lookup * - * @return User_greeting_count object found, or null for no hits + * @return QnA_Question object found, or null for no hits * */ function staticGet($k, $v=null) { - return Memcached_DataObject::staticGet('Question', $k, $v); + return Memcached_DataObject::staticGet('QnA_Question', $k, $v); } /** @@ -83,7 +87,7 @@ class Question extends Managed_DataObject */ function pkeyGet($kv) { - return Memcached_DataObject::pkeyGet('Question', $kv); + return Memcached_DataObject::pkeyGet('QnA_Question', $kv); } /** @@ -92,14 +96,27 @@ class Question extends Managed_DataObject public static function schemaDef() { return array( - 'description' => 'Per-notice question data for QuestionAndAnswer plugin', + 'description' => 'Per-notice question data for QNA plugin', 'fields' => array( - 'id' => array('type' => 'char', 'length' => 36, 'not null' => true, 'description' => 'UUID'), - 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), - 'profile_id' => array('type' => 'int'), - 'title' => array('type' => 'text'), + 'id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID' + ), + 'uri' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => true + ), + 'profile_id' => array('type' => 'int'), + 'title' => array('type' => 'text'), + 'closed' => array('type' => 'int', size => 'tiny'), 'description' => array('type' => 'text'), - 'created' => array('type' => 'datetime', 'not null' => true), + 'created' => array( + 'type' => 'datetime', + 'not null' => true + ), ), 'primary key' => array('id'), 'unique keys' => array( @@ -139,7 +156,7 @@ class Question extends Managed_DataObject */ function getAnswer(Profile $profile) { - $a = new Answer(); + $a = new QnA_Answer(); $a->question_id = $this->id; $a->profile_id = $profile->id; $a->find(); @@ -152,8 +169,7 @@ class Question extends Managed_DataObject function countAnswers() { - $a = new Answer(); - + $a = new QnA_Answer(); $a->question_id = $this->id; return $a-count(); } @@ -171,7 +187,7 @@ class Question extends Managed_DataObject */ static function saveNew($profile, $question, $title, $description, $options = array()) { - $q = new Question(); + $q = new QnA_Question(); $q->id = UUID::gen(); $q->profile_id = $profile->id; @@ -218,7 +234,7 @@ class Question extends Managed_DataObject 'rendered' => $rendered, 'tags' => $tags, 'replies' => $replies, - 'object_type' => QuestionAndAnswerPlugin::QUESTION_OBJECT + 'object_type' => QnAPlugin::QUESTION_OBJECT ), $options ); diff --git a/plugins/QnA/classes/QnA_Vote.php b/plugins/QnA/classes/QnA_Vote.php new file mode 100644 index 0000000000..ec2e75afbb --- /dev/null +++ b/plugins/QnA/classes/QnA_Vote.php @@ -0,0 +1,160 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * 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 . + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * For storing votes on question and answers + * + * @category QnA + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ +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 $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 $profile_id; // int -> question.id + public $created; // datetime + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup + * @param mixed $v Value to lookup + * + * @return QnA_Vote object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('QnA_Vote', $k, $v); + } + + /** + * Get an instance by compound key + * + * This is a utility method to get a single instance with a given set of + * key-value pairs. Usually used for the primary key for a compound key; thus + * the name. + * + * @param array $kv array of key-value mappings + * + * @return QnA_Vote object found, or null for no hits + * + */ + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('QnA_Vote', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + 'description' => 'For storing votes on questions and answers', + 'fields' => array( + 'id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID of the vote' + ), + 'question_id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID of question being voted on' + ), + 'answer_id' => array( + 'type' => 'char', + 'length' => 36, + 'not null' => true, + 'description' => 'UUID of answer being voted on' + ), + 'vote' => array('type' => 'int', 'size' => 'tiny'), + 'profile_id' => array('type' => 'int'), + 'created' => array('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'profile_id_question_Id_index' => array( + 'profile_id', + 'question_id' + ), + 'profile_id_question_Id_index' => array( + 'profile_id', + 'answer_id' + ) + ) + ); + } + + /** + * Save a vote on a question or answer + * + * @param Profile $profile + * @param QnA_Question the question being voted on + * @param QnA_Answer the answer being voted on + * @param vote + * @param array + * + * @return Void + */ + static function save($profile, $question, $answer, $vote) + { + $v = new QnA_Vote(); + $v->id = UUID::gen(); + $v->profile_id = $profile->id; + $v->question_id = $question->id; + $v->answer_id = $answer->id; + $v->vote = $vote; + $v->created = common_sql_now(); + + common_log(LOG_DEBUG, "Saving vote: $v->id $v->vote"); + + $v->insert(); + } +} diff --git a/plugins/QuestionAndAnswer/css/questionandanswer.css b/plugins/QnA/css/qna.css similarity index 100% rename from plugins/QuestionAndAnswer/css/questionandanswer.css rename to plugins/QnA/css/qna.css diff --git a/plugins/QuestionAndAnswer/lib/answerform.php b/plugins/QnA/lib/answerform.php similarity index 92% rename from plugins/QuestionAndAnswer/lib/answerform.php rename to plugins/QnA/lib/answerform.php index d093863708..554f698d99 100644 --- a/plugins/QuestionAndAnswer/lib/answerform.php +++ b/plugins/QnA/lib/answerform.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 QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -37,7 +37,7 @@ if (!defined('STATUSNET')) { /** * Form to add a new answer to a question * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. @@ -51,12 +51,12 @@ class AnswerForm extends Form /** * Construct a new answer form * - * @param Question $question + * @param QnA_Question $question * @param HTMLOutputter $out output channel * * @return void */ - function __construct(Question $question, HTMLOutputter $out) + function __construct(QnA_Question $question, HTMLOutputter $out) { parent::__construct($out); $this->question = $question; @@ -100,12 +100,11 @@ class AnswerForm extends Form function formData() { $question = $this->question; - $out = $this->out; - $id = "question-" . $question->id; + $out = $this->out; + $id = "question-" . $question->id; $out->element('p', 'answer', $question->question); $out->element('input', array('type' => 'text', 'name' => 'answer')); - } /** diff --git a/plugins/QuestionAndAnswer/lib/questionform.php b/plugins/QnA/lib/questionform.php similarity index 99% rename from plugins/QuestionAndAnswer/lib/questionform.php rename to plugins/QnA/lib/questionform.php index 5892464218..4f9ea6d808 100644 --- a/plugins/QuestionAndAnswer/lib/questionform.php +++ b/plugins/QnA/lib/questionform.php @@ -37,7 +37,7 @@ if (!defined('STATUSNET')) { /** * Form to add a new question * - * @category QuestionAndAnswer + * @category QnA * @package StatusNet * @author Zach Copley * @copyright 2011 StatusNet, Inc. diff --git a/plugins/QnA/lib/voteform.php b/plugins/QnA/lib/voteform.php new file mode 100644 index 0000000000..554f698d99 --- /dev/null +++ b/plugins/QnA/lib/voteform.php @@ -0,0 +1,121 @@ +. + * + * @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); +} + +/** + * Form to add a new answer to a question + * + * @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/ + */ +class AnswerForm extends Form +{ + protected $question; + + /** + * Construct a new answer form + * + * @param QnA_Question $question + * @param HTMLOutputter $out output channel + * + * @return void + */ + function __construct(QnA_Question $question, HTMLOutputter $out) + { + parent::__construct($out); + $this->question = $question; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'answer-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('answer', array('id' => $this->question->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $question = $this->question; + $out = $this->out; + $id = "question-" . $question->id; + + $out->element('p', 'answer', $question->question); + $out->element('input', array('type' => 'text', 'name' => 'answer')); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text for submitting a poll response. + $this->out->submit('submit', _m('BUTTON', 'Submit')); + } +} + From 73c3344cc3b867dc5e701554d410a87c18315e5a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 21 Mar 2011 15:50:36 -0700 Subject: [PATCH 4/9] * 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'); From b0ed4cb89ab35ad82ebdba9c5529bd50b8138846 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 21 Mar 2011 16:51:38 -0700 Subject: [PATCH 5/9] * Move stuff around again * Make answers save --- plugins/QnA/QnAPlugin.php | 465 ++++++++++++++++++ .../actions/{answer.php => qnanewanswer.php} | 37 +- .../{newquestion.php => qnanewquestion.php} | 2 +- .../{showanswer.php => qnashowanswer.php} | 2 +- .../{showquestion.php => qnashowquestion.php} | 2 +- plugins/QnA/actions/qnavote.php | 4 +- plugins/QnA/classes/QnA_Answer.php | 25 +- .../lib/{answerform.php => qnaanswerform.php} | 4 +- .../{questionform.php => qnaquestionform.php} | 4 +- .../QnA/lib/{voteform.php => qnavoteform.php} | 6 +- 10 files changed, 511 insertions(+), 40 deletions(-) create mode 100644 plugins/QnA/QnAPlugin.php rename plugins/QnA/actions/{answer.php => qnanewanswer.php} (85%) rename plugins/QnA/actions/{newquestion.php => qnanewquestion.php} (99%) rename plugins/QnA/actions/{showanswer.php => qnashowanswer.php} (98%) rename plugins/QnA/actions/{showquestion.php => qnashowquestion.php} (98%) rename plugins/QnA/lib/{answerform.php => qnaanswerform.php} (96%) rename plugins/QnA/lib/{questionform.php => qnaquestionform.php} (97%) rename plugins/QnA/lib/{voteform.php => qnavoteform.php} (95%) 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')); } /** From 7f4bd6b69f280810b793c0154d36176b3ed48e15 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 21 Mar 2011 20:57:19 -0700 Subject: [PATCH 6/9] Work on QnA notice display -- in progress --- plugins/QnA/QnAPlugin.php | 172 ++++++++---------------- plugins/QnA/actions/qnashowanswer.php | 10 +- plugins/QnA/actions/qnashowquestion.php | 4 +- plugins/QnA/actions/qnavote.php | 2 +- plugins/QnA/classes/QnA_Answer.php | 129 +++++++++++++++--- plugins/QnA/classes/QnA_Question.php | 5 +- plugins/QnA/lib/qnaansweredform.php | 122 +++++++++++++++++ 7 files changed, 298 insertions(+), 146 deletions(-) create mode 100644 plugins/QnA/lib/qnaansweredform.php diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php index ba3b6e329a..992641b18e 100644 --- a/plugins/QnA/QnAPlugin.php +++ b/plugins/QnA/QnAPlugin.php @@ -88,7 +88,8 @@ class QnAPlugin extends MicroAppPlugin return false; case 'QnaquestionForm': case 'QnaanswerForm': - case 'QnavoteForm'; + case 'QnaansweredForm': + case 'QnavoteForm': include_once $dir . '/lib/' . strtolower($cls).'.php'; break; case 'QnA_Question': @@ -201,24 +202,23 @@ class QnAPlugin extends MicroAppPlugin switch ($activity->verb) { case ActivityVerb::POST: - $notice = Question::saveNew( + $notice = QnA_Question::saveNew( $actor, - $questionObj->title - // null, - // $questionObj->summary, - // $options + $questionObj->title, + $questionObj->summary, + $options ); break; - case Answer::NORMAL: + case Answer::ObjectType: $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); + $notice = QnA_Answer::saveNew($actor, $question, $options); break; default: - throw new Exception("Unknown verb for question"); + throw new Exception("Unknown object type received by QnA Plugin"); } return $notice; @@ -292,127 +292,67 @@ class QnAPlugin extends MicroAppPlugin * @param Notice $notice * @param HTMLOutputter $out */ - function showNotice($notice, $out) { switch ($notice->object_type) { case QnA_Question::OBJECT_TYPE: - $this->showQuestionNotice($notice, $out); - break; + return $this->showNoticeQuestion($notice, $out); 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 + return $this->showNoticeAnswer($notice, $out); + default: + // TRANS: Exception thrown when performing an unexpected action on a question. + // TRANS: %s is the unpexpected object type. + throw new Exception( + sprintf( + _m('Unexpected type for QnA plugin: %s.'), + $notice->object_type + ) ); - } 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'); - + } + + function showNoticeQuestion($notice, $out) + { $user = common_current_user(); - if (!empty($user)) { + // @hack we want regular rendering, then just add stuff after that + $nli = new NoticeListItem($notice, $out); + $nli->showNotice(); - $answer = $question->getAnswer($user->getProfile()); - - if (empty($answer)) { - $form = new QnaanswerForm($question, $out); + $out->elementStart('div', array('class' => 'entry-content question-content')); + $question = QnA_Question::getByNotice($notice); + + if ($question) { + if ($user) { + $profile = $user->getProfile(); + $answer = $question->getAnswer($profile); + if ($answer) { + // User has already answer; show the results. + $form = new QnaansweredForm($answer, $out); + } else { + $form = new QnaanswerForm($question, $out); + } $form->show(); } - - + } else { + $out->text(_m('Question data is missing')); } - $out->elementEnd('div'); + + // @fixme + $out->elementStart('div', array('class' => 'entry-content')); + } + + function showNoticeAnswer($notice, $out) + { + $user = common_current_user(); + + // @hack we want regular rendering, then just add stuff after that + $nli = new NoticeListItem($notice, $out); + $nli->showNotice(); + + // @fixme + $out->elementStart('div', array('class' => 'entry-content')); } /** diff --git a/plugins/QnA/actions/qnashowanswer.php b/plugins/QnA/actions/qnashowanswer.php index 68baadfba8..9721f22da3 100644 --- a/plugins/QnA/actions/qnashowanswer.php +++ b/plugins/QnA/actions/qnashowanswer.php @@ -63,7 +63,7 @@ class QnashowanswerAction extends ShownoticeAction $this->id = $this->trimmed('id'); - $this->answer = Answer::staticGet('id', $this->id); + $this->answer = QnA_Answer::staticGet('id', $this->id); if (empty($this->answer)) { throw new ClientException(_('No such answer.'), 404); @@ -117,9 +117,11 @@ class QnashowanswerAction extends ShownoticeAction function showPageTitle() { $this->elementStart('h1'); - $this->element('a', - array('href' => $this->answer->url), - $this->asnwer->title); + $this->element( + 'a', + array('href' => $this->answer->url), + $this->answer->title + ); $this->elementEnd('h1'); } } diff --git a/plugins/QnA/actions/qnashowquestion.php b/plugins/QnA/actions/qnashowquestion.php index e563753a01..6719125354 100644 --- a/plugins/QnA/actions/qnashowquestion.php +++ b/plugins/QnA/actions/qnashowquestion.php @@ -61,7 +61,7 @@ class QnashowquestionAction extends ShownoticeAction $this->id = $this->trimmed('id'); - $this->question = Question::staticGet('id', $this->id); + $this->question = QnA_Question::staticGet('id', $this->id); if (empty($this->question)) { // TRANS: Client exception thrown trying to view a non-existing question. @@ -108,7 +108,7 @@ class QnashowquestionAction extends ShownoticeAction // TRANS: %1$s is the nickname of the user who asked the question, %2$s is the question. return sprintf(_m('%1$s\'s question: %2$s'), $this->user->nickname, - $this->question->question); + $this->question->title); } /** diff --git a/plugins/QnA/actions/qnavote.php b/plugins/QnA/actions/qnavote.php index 94aec41c5b..8098cb87d0 100644 --- a/plugins/QnA/actions/qnavote.php +++ b/plugins/QnA/actions/qnavote.php @@ -90,7 +90,7 @@ class Qnavote 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); diff --git a/plugins/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php index ff11ff8f14..57c08afe4e 100644 --- a/plugins/QnA/classes/QnA_Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -50,7 +50,9 @@ class QnA_Answer extends Managed_DataObject public $id; // char(36) primary key not null -> UUID public $question_id; // char(36) -> question.id UUID public $profile_id; // int -> question.id - public $best; // (int) boolean -> whether the question asker has marked this as the best answer + public $best; // (boolean) int -> whether the question asker has marked this as the best answer + public $revisions; // int -> count of revisions to this answer + public $text; // text -> response text public $created; // datetime /** @@ -105,14 +107,15 @@ class QnA_Answer extends Managed_DataObject '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'), - 'profile_id' => array('type' => 'int'), - 'created' => array('type' => 'datetime', 'not null' => true), + 'best' => array('type' => 'int', 'size' => 'tiny'), + 'revisions' => array('type' => 'int'), + 'profile_id' => array('type' => 'int'), + 'created' => array('type' => 'datetime', 'not null' => true), ), 'primary key' => array('id'), 'unique keys' => array( @@ -134,7 +137,11 @@ class QnA_Answer extends Managed_DataObject */ function getByNotice($notice) { - return self::staticGet('uri', $notice->uri); + $answer = self::staticGet('uri', $notice->uri); + if (empty($answer)) { + throw new Exception("No answer with URI {$this->notice->uri}"); + } + return $answer; } /** @@ -159,14 +166,93 @@ class QnA_Answer extends Managed_DataObject */ function getQuestion() { - return Question::staticGet('id', $this->question_id); + $question = self::staticGet('id', $this->question_id); + if (empty($question)) { + throw new Exception("No question with ID {$this->question_id}"); + } + return question; + } + + function getProfile() + { + $profile = Profile::staticGet('id', $this->profile_id); + if (empty($profile)) { + throw new Exception("No profile with ID {$this->profile_id}"); + } + return $profile; } - static function fromNotice($notice) + function asHTML() { - return self::staticGet('uri', $notice->uri); + return self::toHTML( + $this->getProfile(), + $this->getQuestion() + ); } + function asString() + { + return self::toString( + $this->getProfile(), + $this->getQuestion() + ); + } + + static function toHTML($profile, $event, $response) + { + $fmt = null; + + $notice = $event->getNotice(); + + switch ($response) { + case 'Y': + $fmt = _("%2s is attending %4s."); + break; + case 'N': + $fmt = _("%2s is not attending %4s."); + break; + case '?': + $fmt = _("%2s might attend %4s."); + break; + default: + throw new Exception("Unknown response code {$response}"); + break; + } + + return sprintf($fmt, + htmlspecialchars($profile->profileurl), + htmlspecialchars($profile->getBestName()), + htmlspecialchars($notice->bestUrl()), + htmlspecialchars($event->title)); + } + + static function toString($profile, $event, $response) + { + $fmt = null; + + $notice = $event->getNotice(); + + switch ($response) { + case 'Y': + $fmt = _("%1s is attending %2s."); + break; + case 'N': + $fmt = _("%1s is not attending %2s."); + break; + case '?': + $fmt = _("%1s might attend %2s.>"); + break; + default: + throw new Exception("Unknown response code {$response}"); + break; + } + + return sprintf($fmt, + $profile->getBestName(), + $event->title); + } + + /** * Save a new answer notice * @@ -176,7 +262,7 @@ class QnA_Answer extends Managed_DataObject * * @return Notice saved notice */ - static function saveNew($profile, $question, $options = null) + static function saveNew($profile, $question, $text, $options = null) { if (empty($options)) { $options = array(); @@ -186,23 +272,24 @@ class QnA_Answer extends Managed_DataObject $answer->id = UUID::gen(); $answer->profile_id = $profile->id; $answer->question_id = $question->id; + $answer->revisions = 0; + $answer->best = 0; + $answer->text = $text; $answer->created = common_sql_now(); $answer->uri = common_local_url( - 'showanswer', + 'qnashowanswer', array('id' => $answer->id) ); 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->uri + $question->title ); - $link = '' . htmlspecialchars($answer) . ''; + $link = '' . htmlspecialchars($question->title) . ''; // 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); @@ -213,13 +300,15 @@ class QnA_Answer extends Managed_DataObject $options = array_merge( array( 'urls' => array(), + 'content' => $content, 'rendered' => $rendered, 'tags' => $tags, 'replies' => $replies, 'reply_to' => $question->getNotice()->id, - 'object_type' => self::OBJECT_TYPE), - $options - ); + 'object_type' => self::OBJECT_TYPE + ), + $options + ); if (!array_key_exists('uri', $options)) { $options['uri'] = $answer->uri; diff --git a/plugins/QnA/classes/QnA_Question.php b/plugins/QnA/classes/QnA_Question.php index 308e87b99f..5230923590 100644 --- a/plugins/QnA/classes/QnA_Question.php +++ b/plugins/QnA/classes/QnA_Question.php @@ -113,7 +113,7 @@ class QnA_Question extends Managed_DataObject 'closed' => array('type' => 'int', 'size' => 'tiny'), 'description' => array('type' => 'text'), 'created' => array( - 'type' => 'datetime', + 'type' => 'datetime', 'not null' => true ), ), @@ -175,7 +175,6 @@ class QnA_Question extends Managed_DataObject static function fromNotice($notice) { - common_debug('xxxxxxxxxxxxxxx notice-uri = ' . $notice->uri); return QnA_Question::staticGet('uri', $notice->uri); } @@ -209,7 +208,7 @@ class QnA_Question extends Managed_DataObject $q->uri = $options['uri']; } else { $q->uri = common_local_url( - 'showquestion', + 'qnashowquestion', array('id' => $q->id) ); } diff --git a/plugins/QnA/lib/qnaansweredform.php b/plugins/QnA/lib/qnaansweredform.php new file mode 100644 index 0000000000..a229e7f870 --- /dev/null +++ b/plugins/QnA/lib/qnaansweredform.php @@ -0,0 +1,122 @@ +. + * + * @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); +} + +/** + * Form to add a new answer to a question + * + * @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/ + */ +class QnaansweredForm extends Form +{ + protected $question; + protected $answer; + + /** + * Construct a new answer form + * + * @param QnA_Answer $answer + * @param HTMLOutputter $out output channel + * + * @return void + */ + function __construct(QnA_Answer $answer, HTMLOutputter $out) + { + parent::__construct($out); + $this->question = $answer->getQuestion(); + $this->answer = $answer; + } + + /** + * ID of the form + * + * @return int ID of the form + */ + function id() + { + return 'answered-form'; + } + + /** + * class of the form + * + * @return string class of the form + */ + function formClass() + { + return 'form_settings ajax'; + } + + /** + * Action of the form + * + * @return string URL of the action + */ + function action() + { + return common_local_url('qnareviseanswer', array('id' => $this->question->id)); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $question = $this->question; + $out = $this->out; + $id = "question-" . $question->id; + + $out->element('p', 'Your answer to:', $question->title); + $out->element('input', array('type' => 'text', 'name' => 'answer')); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text for submitting a poll response. + $this->out->submit('submit', _m('BUTTON', 'Submit')); + } +} From 7669bed9f3e975548c9269d245f120ea180c6f78 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 30 Mar 2011 10:33:15 -0700 Subject: [PATCH 7/9] More plumbing --- plugins/QnA/actions/qnanewanswer.php | 4 +- plugins/QnA/actions/qnashowanswer.php | 4 +- plugins/QnA/actions/qnashowquestion.php | 5 ++ plugins/QnA/classes/QnA_Answer.php | 80 +++++++++-------------- plugins/QnA/classes/QnA_Question.php | 85 +++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 53 deletions(-) diff --git a/plugins/QnA/actions/qnanewanswer.php b/plugins/QnA/actions/qnanewanswer.php index 10b1046c3e..781ded36e6 100644 --- a/plugins/QnA/actions/qnanewanswer.php +++ b/plugins/QnA/actions/qnanewanswer.php @@ -50,7 +50,7 @@ class QnanewanswerAction extends Action protected $complete = null; protected $question = null; - protected $answerText = null; + protected $content = null; /** * Returns the title of the action @@ -155,7 +155,7 @@ class QnanewanswerAction extends Action $this->element('title', null, _m('Answers')); $this->elementEnd('head'); $this->elementStart('body'); - $form = new QnA_Answer($this->question, $this); + $form = new QnaanswerForm($this->question, $this); $form->show(); $this->elementEnd('body'); $this->elementEnd('html'); diff --git a/plugins/QnA/actions/qnashowanswer.php b/plugins/QnA/actions/qnashowanswer.php index 9721f22da3..d90b5c7ac6 100644 --- a/plugins/QnA/actions/qnashowanswer.php +++ b/plugins/QnA/actions/qnashowanswer.php @@ -103,9 +103,11 @@ class QnashowanswerAction extends ShownoticeAction function title() { + $question = $this->answer->getQuestion(); + return sprintf(_('%s\'s answer to "%s"'), $this->user->nickname, - $this->answer->title); + $question->title); } /** diff --git a/plugins/QnA/actions/qnashowquestion.php b/plugins/QnA/actions/qnashowquestion.php index 6719125354..d128eeec2f 100644 --- a/plugins/QnA/actions/qnashowquestion.php +++ b/plugins/QnA/actions/qnashowquestion.php @@ -95,6 +95,11 @@ class QnashowquestionAction extends ShownoticeAction return true; } + function showContent() + { + $this->raw($this->question->asHTML()); + } + /** * Title of the page * diff --git a/plugins/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php index 57c08afe4e..349bbb0196 100644 --- a/plugins/QnA/classes/QnA_Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -52,7 +52,7 @@ class QnA_Answer extends Managed_DataObject public $profile_id; // int -> question.id public $best; // (boolean) int -> whether the question asker has marked this as the best answer public $revisions; // int -> count of revisions to this answer - public $text; // text -> response text + public $content; // text -> response text public $created; // datetime /** @@ -112,6 +112,7 @@ class QnA_Answer extends Managed_DataObject 'not null' => true, 'description' => 'UUID of question being responded to' ), + 'content' => array('type' => 'text'), // got a better name? 'best' => array('type' => 'int', 'size' => 'tiny'), 'revisions' => array('type' => 'int'), 'profile_id' => array('type' => 'int'), @@ -172,7 +173,7 @@ class QnA_Answer extends Managed_DataObject } return question; } - + function getProfile() { $profile = Profile::staticGet('id', $this->profile_id); @@ -186,7 +187,8 @@ class QnA_Answer extends Managed_DataObject { return self::toHTML( $this->getProfile(), - $this->getQuestion() + $this->getQuestion(), + $this ); } @@ -194,65 +196,43 @@ class QnA_Answer extends Managed_DataObject { return self::toString( $this->getProfile(), - $this->getQuestion() + $this->getQuestion(), + $this ); } - static function toHTML($profile, $event, $response) + static function toHTML($profile, $question, $answer) { - $fmt = null; + $notice = $question->getNotice(); - $notice = $event->getNotice(); + $fmt = 'answer by %3s'; + $fmt .= '%4s'; - switch ($response) { - case 'Y': - $fmt = _("%2s is attending %4s."); - break; - case 'N': - $fmt = _("%2s is not attending %4s."); - break; - case '?': - $fmt = _("%2s might attend %4s."); - break; - default: - throw new Exception("Unknown response code {$response}"); - break; - } - - return sprintf($fmt, - htmlspecialchars($profile->profileurl), - htmlspecialchars($profile->getBestName()), - htmlspecialchars($notice->bestUrl()), - htmlspecialchars($event->title)); + return sprintf( + $fmt, + htmlspecialchars($notice->bestUrl()), + htmlspecialchars($profile->profileurl), + htmlspecialchars($profile->getBestName()), + htmlspecialchars($answer->content) + ); } - static function toString($profile, $event, $response) + static function toString($profile, $question, $answer) { - $fmt = null; + $notice = $question->getNotice(); - $notice = $event->getNotice(); + $fmt = _( + '%1s answered the question "%2s": %3s' + ); - switch ($response) { - case 'Y': - $fmt = _("%1s is attending %2s."); - break; - case 'N': - $fmt = _("%1s is not attending %2s."); - break; - case '?': - $fmt = _("%1s might attend %2s.>"); - break; - default: - throw new Exception("Unknown response code {$response}"); - break; - } - - return sprintf($fmt, - $profile->getBestName(), - $event->title); + return sprintf( + $fmt, + htmlspecialchars($profile->getBestName()), + htmlspecialchars($question->title), + htmlspecialchars($answer->content) + ); } - /** * Save a new answer notice * @@ -274,7 +254,7 @@ class QnA_Answer extends Managed_DataObject $answer->question_id = $question->id; $answer->revisions = 0; $answer->best = 0; - $answer->text = $text; + $answer->content = $text; $answer->created = common_sql_now(); $answer->uri = common_local_url( 'qnashowanswer', diff --git a/plugins/QnA/classes/QnA_Question.php b/plugins/QnA/classes/QnA_Question.php index 5230923590..1022f2c3a6 100644 --- a/plugins/QnA/classes/QnA_Question.php +++ b/plugins/QnA/classes/QnA_Question.php @@ -146,6 +146,15 @@ class QnA_Question extends Managed_DataObject return $this->getNotice()->bestUrl(); } + function getProfile() + { + $profile = Profile::staticGet('id', $this->profile_id); + if (empty($profile)) { + throw new Exception("No profile with ID {$this->profile_id}"); + } + return $profile; + } + /** * Get the answer from a particular user to this question, if any. * @@ -166,6 +175,18 @@ class QnA_Question extends Managed_DataObject } } + function getAnswers() + { + $a = new QnA_Answer(); + $a->question_id = $this->id; + $cnt = $a->find(); + if (!empty($cnt)) { + return $a; + } else { + return null; + } + } + function countAnswers() { $a = new QnA_Answer(); @@ -178,6 +199,70 @@ class QnA_Question extends Managed_DataObject return QnA_Question::staticGet('uri', $notice->uri); } + function asHTML() + { + return self::toHTML( + $this->getProfile(), + $this, + $this->getAnswers() + ); + } + + function asString() + { + return self::toString( + $this->getProfile(), + $this, + $this->getAnswers() + ); + } + + static function toHTML($profile, $question, $answer) + { + $notice = $question->getNotice(); + + $fmt = '
'; + $fmt .= '%2s'; + $fmt .= '%3s'; + $fmt .= 'asked by %5s'; + $fmt .= '
'; + + $q = sprintf( + $fmt, + htmlspecialchars($notice->bestUrl()), + htmlspecialchars($question->title), + htmlspecialchars($question->description), + htmlspecialchars($profile->profileurl), + htmlspecialchars($profile->getBestName()) + ); + + $ans = array(); + + $ans[] = '
'; + + while($answer->fetch()) { + $ans[] = $answer->asHTML(); + } + + $ans[] .= '
'; + + return $q . implode($ans); + } + + static function toString($profile, $question, $answers) + { + $fmt = _( + '%1s asked the question "%2s": %3s' + ); + + return sprintf( + $fmt, + htmlspecialchars($profile->getBestName()), + htmlspecialchars($question->title), + htmlspecialchars($question->description) + ); + } + /** * Save a new question notice * From eeff6285ae833a540552a35a83b709a9675d93f2 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 31 Mar 2011 09:58:26 -0700 Subject: [PATCH 8/9] Make new answers work --- plugins/QnA/QnAPlugin.php | 11 +++++------ plugins/QnA/actions/qnanewanswer.php | 24 +++++++++++++----------- plugins/QnA/actions/qnashowanswer.php | 23 ++++++++++++++++++----- plugins/QnA/classes/QnA_Answer.php | 4 ++-- plugins/QnA/lib/qnaansweredform.php | 2 +- plugins/QnA/lib/qnaanswerform.php | 3 ++- 6 files changed, 41 insertions(+), 26 deletions(-) diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php index 992641b18e..228b571a6e 100644 --- a/plugins/QnA/QnAPlugin.php +++ b/plugins/QnA/QnAPlugin.php @@ -120,9 +120,8 @@ class QnAPlugin extends MicroAppPlugin array('action' => 'qnanewquestion') ); $m->connect( - 'main/qna/newanswer/:id', - array('action' => 'qnanewanswer'), - array('id' => $UUIDregex) + 'main/qna/newanswer', + array('action' => 'qnanewanswer') ); $m->connect( 'question/vote/:id', @@ -304,13 +303,13 @@ class QnAPlugin extends MicroAppPlugin // TRANS: %s is the unpexpected object type. throw new Exception( sprintf( - _m('Unexpected type for QnA plugin: %s.'), + _m('Unexpected type for QnA plugin: %s.'), $notice->object_type ) ); } } - + function showNoticeQuestion($notice, $out) { $user = common_current_user(); @@ -321,7 +320,7 @@ class QnAPlugin extends MicroAppPlugin $out->elementStart('div', array('class' => 'entry-content question-content')); $question = QnA_Question::getByNotice($notice); - + if ($question) { if ($user) { $profile = $user->getProfile(); diff --git a/plugins/QnA/actions/qnanewanswer.php b/plugins/QnA/actions/qnanewanswer.php index 781ded36e6..d2558380e9 100644 --- a/plugins/QnA/actions/qnanewanswer.php +++ b/plugins/QnA/actions/qnanewanswer.php @@ -45,12 +45,12 @@ if (!defined('STATUSNET')) { */ class QnanewanswerAction extends Action { - protected $user = null; - protected $error = null; - protected $complete = null; + protected $user = null; + protected $error = null; + protected $complete = null; - protected $question = null; - protected $content = null; + protected $question = null; + protected $content = null; /** * Returns the title of the action @@ -91,19 +91,22 @@ class QnanewanswerAction extends Action $this->checkSessionToken(); } - $id = $this->trimmed('id'); + $id = substr($this->trimmed('id'), 9); + + common_debug("XXXXXXXXXXXXXXXXXX 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.'), + _m('Invalid or missing question.'), 404 ); } $this->answerText = $this->trimmed('answer'); - + return true; } @@ -145,8 +148,8 @@ class QnanewanswerAction extends Action $this->showPage(); return; } - if ($this->boolean('ajax')) { + common_debug("ajaxy part"); header('Content-Type: text/xml;charset=utf-8'); $this->xw->startDocument('1.0', 'UTF-8'); $this->elementStart('html'); @@ -176,7 +179,6 @@ class QnanewanswerAction extends Action } $form = new QnaanswerForm($this->question, $this); - $form->show(); return; diff --git a/plugins/QnA/actions/qnashowanswer.php b/plugins/QnA/actions/qnashowanswer.php index d90b5c7ac6..5f3bc2eed9 100644 --- a/plugins/QnA/actions/qnashowanswer.php +++ b/plugins/QnA/actions/qnashowanswer.php @@ -69,6 +69,12 @@ class QnashowanswerAction extends ShownoticeAction throw new ClientException(_('No such answer.'), 404); } + $this->question = $this->answer->getQuestion(); + + if (empty($this->question)) { + throw new ClientException(_('No question for this answer.'), 404); + } + $this->notice = Notice::staticGet('uri', $this->answer->uri); if (empty($this->notice)) { @@ -105,9 +111,11 @@ class QnashowanswerAction extends ShownoticeAction { $question = $this->answer->getQuestion(); - return sprintf(_('%s\'s answer to "%s"'), - $this->user->nickname, - $question->title); + return sprintf( + _('%s\'s answer to "%s"'), + $this->user->nickname, + $question->title + ); } /** @@ -121,9 +129,14 @@ class QnashowanswerAction extends ShownoticeAction $this->elementStart('h1'); $this->element( 'a', - array('href' => $this->answer->url), - $this->answer->title + array('href' => $this->answer->uri), + $this->question->title ); $this->elementEnd('h1'); } + + function showContent() + { + $this->raw($this->answer->asHTML()); + } } diff --git a/plugins/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php index 349bbb0196..06e88354c9 100644 --- a/plugins/QnA/classes/QnA_Answer.php +++ b/plugins/QnA/classes/QnA_Answer.php @@ -167,11 +167,11 @@ class QnA_Answer extends Managed_DataObject */ function getQuestion() { - $question = self::staticGet('id', $this->question_id); + $question = QnA_Question::staticGet('id', $this->question_id); if (empty($question)) { throw new Exception("No question with ID {$this->question_id}"); } - return question; + return $question; } function getProfile() diff --git a/plugins/QnA/lib/qnaansweredform.php b/plugins/QnA/lib/qnaansweredform.php index a229e7f870..b1500140f3 100644 --- a/plugins/QnA/lib/qnaansweredform.php +++ b/plugins/QnA/lib/qnaansweredform.php @@ -60,7 +60,7 @@ class QnaansweredForm extends Form function __construct(QnA_Answer $answer, HTMLOutputter $out) { parent::__construct($out); - $this->question = $answer->getQuestion(); + $this->question = $answer->getQuestion(); $this->answer = $answer; } diff --git a/plugins/QnA/lib/qnaanswerform.php b/plugins/QnA/lib/qnaanswerform.php index f89f6c7889..8d78213d7c 100644 --- a/plugins/QnA/lib/qnaanswerform.php +++ b/plugins/QnA/lib/qnaanswerform.php @@ -89,7 +89,7 @@ class QnaanswerForm extends Form */ function action() { - return common_local_url('qnanewanswer', array('id' => $this->question->id)); + return common_local_url('qnanewanswer'); } /** @@ -104,6 +104,7 @@ class QnaanswerForm extends Form $id = "question-" . $question->id; $out->element('p', 'answer', $question->title); + $out->hidden('id', $id); $out->element('input', array('type' => 'text', 'name' => 'answer')); } From 9cea85065c6b039c99aa373132a91eae2e320ebb Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 1 Apr 2011 02:35:05 -0700 Subject: [PATCH 9/9] Some work towards allowing revisions --- plugins/QnA/QnAPlugin.php | 5 +++-- plugins/QnA/actions/qnanewanswer.php | 3 +-- .../lib/{qnaansweredform.php => qnareviseanswerform.php} | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) rename plugins/QnA/lib/{qnaansweredform.php => qnareviseanswerform.php} (93%) diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php index 228b571a6e..9a05eeb0b2 100644 --- a/plugins/QnA/QnAPlugin.php +++ b/plugins/QnA/QnAPlugin.php @@ -82,13 +82,14 @@ class QnAPlugin extends MicroAppPlugin case 'QnanewanswerAction': case 'QnashowquestionAction': case 'QnashowanswerAction': + case 'QnareviseanswerAction': case 'QnavoteAction': include_once $dir . '/actions/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'QnaquestionForm': case 'QnaanswerForm': - case 'QnaansweredForm': + case 'QnareviseanswerForm': case 'QnavoteForm': include_once $dir . '/lib/' . strtolower($cls).'.php'; break; @@ -327,7 +328,7 @@ class QnAPlugin extends MicroAppPlugin $answer = $question->getAnswer($profile); if ($answer) { // User has already answer; show the results. - $form = new QnaansweredForm($answer, $out); + $form = new QnareviseanswerForm($answer, $out); } else { $form = new QnaanswerForm($question, $out); } diff --git a/plugins/QnA/actions/qnanewanswer.php b/plugins/QnA/actions/qnanewanswer.php index d2558380e9..09d111040d 100644 --- a/plugins/QnA/actions/qnanewanswer.php +++ b/plugins/QnA/actions/qnanewanswer.php @@ -158,8 +158,7 @@ class QnanewanswerAction extends Action $this->element('title', null, _m('Answers')); $this->elementEnd('head'); $this->elementStart('body'); - $form = new QnaanswerForm($this->question, $this); - $form->show(); + $this->raw() $this->elementEnd('body'); $this->elementEnd('html'); } else { diff --git a/plugins/QnA/lib/qnaansweredform.php b/plugins/QnA/lib/qnareviseanswerform.php similarity index 93% rename from plugins/QnA/lib/qnaansweredform.php rename to plugins/QnA/lib/qnareviseanswerform.php index b1500140f3..48f47e5e98 100644 --- a/plugins/QnA/lib/qnaansweredform.php +++ b/plugins/QnA/lib/qnareviseanswerform.php @@ -3,7 +3,7 @@ * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2011, StatusNet, Inc. * - * Form for answering a question + * Form for revising a question * * PHP version 5 * @@ -35,7 +35,7 @@ if (!defined('STATUSNET')) { } /** - * Form to add a new answer to a question + * Form to revise a question * * @category QnA * @package StatusNet @@ -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 QnaansweredForm extends Form +class QnareviseanswerForm extends Form { protected $question; protected $answer; @@ -106,7 +106,7 @@ class QnaansweredForm extends Form $id = "question-" . $question->id; $out->element('p', 'Your answer to:', $question->title); - $out->element('input', array('type' => 'text', 'name' => 'answer')); + $out->textarea('answerText', 'You said:', $this->answer->content); } /**