diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php new file mode 100644 index 0000000000..9a05eeb0b2 --- /dev/null +++ b/plugins/QnA/QnAPlugin.php @@ -0,0 +1,405 @@ +. + * + * @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 'QnareviseanswerAction': + case 'QnavoteAction': + include_once $dir . '/actions/' + . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'QnaquestionForm': + case 'QnaanswerForm': + case 'QnareviseanswerForm': + 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', + array('action' => 'qnanewanswer') + ); + $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 = QnA_Question::saveNew( + $actor, + $questionObj->title, + $questionObj->summary, + $options + ); + break; + 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, $options); + break; + default: + throw new Exception("Unknown object type received by QnA Plugin"); + } + + 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: + return $this->showNoticeQuestion($notice, $out); + case QnA_Answer::OBJECT_TYPE: + 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 + ) + ); + } + } + + function showNoticeQuestion($notice, $out) + { + $user = common_current_user(); + + // @hack we want regular rendering, then just add stuff after that + $nli = new NoticeListItem($notice, $out); + $nli->showNotice(); + + $out->elementStart('div', array('class' => 'entry-content question-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 QnareviseanswerForm($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')); + } + + /** + * 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/qnanewanswer.php b/plugins/QnA/actions/qnanewanswer.php new file mode 100644 index 0000000000..09d111040d --- /dev/null +++ b/plugins/QnA/actions/qnanewanswer.php @@ -0,0 +1,204 @@ +. + * + * @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 QnanewanswerAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $question = null; + protected $content = 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 = 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.'), + 404 + ); + } + + $this->answerText = $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->newAnswer(); + } else { + $this->showPage(); + } + + return; + } + + /** + * Add a new answer + * + * @return void + */ + function newAnswer() + { + try { + $notice = QnA_Answer::saveNew( + $this->user->getProfile(), + $this->question, + $this->answerText + ); + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $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'); + $this->elementStart('head'); + // TRANS: Page title after sending an answer. + $this->element('title', null, _m('Answers')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->raw() + $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 QnaanswerForm($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/QnA/actions/qnanewquestion.php b/plugins/QnA/actions/qnanewquestion.php new file mode 100644 index 0000000000..8682f8dd47 --- /dev/null +++ b/plugins/QnA/actions/qnanewquestion.php @@ -0,0 +1,216 @@ +. + * + * @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); +} + +/** + * 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 QnanewquestionAction extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + protected $title = null; + protected $description = 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(); + } + + $this->title = $this->trimmed('title'); + $this->description = $this->trimmed('description'); + + 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->title)) { + // TRANS: Client exception thrown trying to create a question without a title. + throw new ClientException(_m('Question must have a title.')); + } + + $saved = QnA_Question::saveNew( + $this->user->getProfile(), + $this->title, + $this->description + ); + } 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('Question 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 QuestionForm( + $this, + $this->title, + $this->description + ); + + $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/QnA/actions/qnashowanswer.php b/plugins/QnA/actions/qnashowanswer.php new file mode 100644 index 0000000000..5f3bc2eed9 --- /dev/null +++ b/plugins/QnA/actions/qnashowanswer.php @@ -0,0 +1,142 @@ +. + * + * @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/ + */ + +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 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 QnashowanswerAction 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 = QnA_Answer::staticGet('id', $this->id); + + if (empty($this->answer)) { + 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)) { + // 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() + { + $question = $this->answer->getQuestion(); + + return sprintf( + _('%s\'s answer to "%s"'), + $this->user->nickname, + $question->title + ); + } + + /** + * Overload page title display to show answer link + * + * @return void + */ + + function showPageTitle() + { + $this->elementStart('h1'); + $this->element( + 'a', + array('href' => $this->answer->uri), + $this->question->title + ); + $this->elementEnd('h1'); + } + + function showContent() + { + $this->raw($this->answer->asHTML()); + } +} diff --git a/plugins/QnA/actions/qnashowquestion.php b/plugins/QnA/actions/qnashowquestion.php new file mode 100644 index 0000000000..d128eeec2f --- /dev/null +++ b/plugins/QnA/actions/qnashowquestion.php @@ -0,0 +1,135 @@ +. + * + * @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); +} + +/** + * Show 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 QnashowquestionAction 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 = QnA_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; + } + + function showContent() + { + $this->raw($this->question->asHTML()); + } + + /** + * 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->title); + } + + /** + * @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/QnA/actions/qnavote.php b/plugins/QnA/actions/qnavote.php new file mode 100644 index 0000000000..8098cb87d0 --- /dev/null +++ b/plugins/QnA/actions/qnavote.php @@ -0,0 +1,198 @@ +. + * + * @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); +} + +/** + * Vote on a question or answer + * + * @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 Qnavote extends Action +{ + protected $user = null; + protected $error = null; + protected $complete = null; + + protected $question = 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 = 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); + } + + $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 QnA_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 QnaanswerForm($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/QnA/classes/QnA_Answer.php b/plugins/QnA/classes/QnA_Answer.php new file mode 100644 index 0000000000..06e88354c9 --- /dev/null +++ b/plugins/QnA/classes/QnA_Answer.php @@ -0,0 +1,307 @@ + + * @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 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_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 + 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 $content; // text -> response 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 + * @param mixed $v Value to lookup + * + * @return QnA_Answer object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('QnA_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 QA_Answer object found, or null for no hits + * + */ + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('QnA_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' + ), + 'content' => array('type' => 'text'), // got a better name? + '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( + '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 QnA_Answer found response or null + */ + function getByNotice($notice) + { + $answer = self::staticGet('uri', $notice->uri); + if (empty($answer)) { + throw new Exception("No answer with URI {$this->notice->uri}"); + } + return $answer; + } + + /** + * 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 QnA_Question + */ + function getQuestion() + { + $question = QnA_Question::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; + } + + function asHTML() + { + return self::toHTML( + $this->getProfile(), + $this->getQuestion(), + $this + ); + } + + function asString() + { + return self::toString( + $this->getProfile(), + $this->getQuestion(), + $this + ); + } + + static function toHTML($profile, $question, $answer) + { + $notice = $question->getNotice(); + + $fmt = 'answer by %3s'; + $fmt .= '%4s'; + + return sprintf( + $fmt, + htmlspecialchars($notice->bestUrl()), + htmlspecialchars($profile->profileurl), + htmlspecialchars($profile->getBestName()), + htmlspecialchars($answer->content) + ); + } + + static function toString($profile, $question, $answer) + { + $notice = $question->getNotice(); + + $fmt = _( + '%1s answered the question "%2s": %3s' + ); + + return sprintf( + $fmt, + htmlspecialchars($profile->getBestName()), + htmlspecialchars($question->title), + htmlspecialchars($answer->content) + ); + } + + /** + * 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, $text, $options = null) + { + if (empty($options)) { + $options = array(); + } + + $answer = new QnA_Answer(); + $answer->id = UUID::gen(); + $answer->profile_id = $profile->id; + $answer->question_id = $question->id; + $answer->revisions = 0; + $answer->best = 0; + $answer->content = $text; + $answer->created = common_sql_now(); + $answer->uri = common_local_url( + 'qnashowanswer', + array('id' => $answer->id) + ); + + common_log(LOG_DEBUG, "Saving answer: $answer->id, $answer->uri"); + $answer->insert(); + + $content = sprintf( + _m('answered "%s"'), + $question->title + ); + + $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); + + $tags = array(); + $replies = array(); + + $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 + ); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $answer->uri; + } + + $saved = Notice::saveNew( + $profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options + ); + + return $saved; + } +} diff --git a/plugins/QnA/classes/QnA_Question.php b/plugins/QnA/classes/QnA_Question.php new file mode 100644 index 0000000000..1022f2c3a6 --- /dev/null +++ b/plugins/QnA/classes/QnA_Question.php @@ -0,0 +1,345 @@ + + * @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 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_Question extends Managed_DataObject +{ + 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; + public $profile_id; // int -> profile.id + public $title; // text + public $description; // text + public $closed; // int (boolean) whether a question is closed + 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_Question object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('QnA_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('QnA_Question', $kv); + } + + /** + * The One True Thingy that must be defined and declared. + */ + public static function schemaDef() + { + return array( + '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'), + 'closed' => array('type' => 'int', 'size' => 'tiny'), + '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(); + } + + 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. + * + * @param Profile $profile + * + * @return Answer object or null + */ + function getAnswer(Profile $profile) + { + $a = new QnA_Answer(); + $a->question_id = $this->id; + $a->profile_id = $profile->id; + $a->find(); + if ($a->fetch()) { + return $a; + } else { + return null; + } + } + + 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(); + $a->question_id = $this->id; + return $a-count(); + } + + static function fromNotice($notice) + { + 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 + * + * @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, $title, $description, $options = array()) + { + $q = new QnA_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( + 'qnashowquestion', + 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' => self::OBJECT_TYPE + ), + $options + ); + + if (!array_key_exists('uri', $options)) { + $options['uri'] = $q->uri; + } + + $saved = Notice::saveNew( + $profile->id, + $content, + array_key_exists('source', $options) ? + $options['source'] : 'web', + $options + ); + + return $saved; + } +} diff --git a/plugins/QnA/classes/QnA_Vote.php b/plugins/QnA/classes/QnA_Vote.php new file mode 100644 index 0000000000..ad579666b8 --- /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 = '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 $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/QnA/css/qna.css b/plugins/QnA/css/qna.css new file mode 100644 index 0000000000..4701b5ab03 --- /dev/null +++ b/plugins/QnA/css/qna.css @@ -0,0 +1 @@ +/* stubb for q&a css */ diff --git a/plugins/QnA/lib/qnaanswerform.php b/plugins/QnA/lib/qnaanswerform.php new file mode 100644 index 0000000000..8d78213d7c --- /dev/null +++ b/plugins/QnA/lib/qnaanswerform.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 QnaanswerForm 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('qnanewanswer'); + } + + /** + * Data elements of the form + * + * @return void + */ + function formData() + { + $question = $this->question; + $out = $this->out; + $id = "question-" . $question->id; + + $out->element('p', 'answer', $question->title); + $out->hidden('id', $id); + $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/QnA/lib/qnaquestionform.php b/plugins/QnA/lib/qnaquestionform.php new file mode 100644 index 0000000000..9d0c2aad59 --- /dev/null +++ b/plugins/QnA/lib/qnaquestionform.php @@ -0,0 +1,137 @@ +. + * + * @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 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 QnaquestionForm extends Form +{ + protected $title; + protected $description; + + /** + * Construct a new question form + * + * @param HTMLOutputter $out output channel + * + * @return void + */ + function __construct($out = null, $title = null, $description = null, $options = null) + { + parent::__construct($out); + $this->title = $title; + $this->description = $description; + } + + /** + * 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('qnanewquestion'); + } + + /** + * 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( + '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'); + $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')); + } +} diff --git a/plugins/QnA/lib/qnareviseanswerform.php b/plugins/QnA/lib/qnareviseanswerform.php new file mode 100644 index 0000000000..48f47e5e98 --- /dev/null +++ b/plugins/QnA/lib/qnareviseanswerform.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 revise 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 QnareviseanswerForm 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->textarea('answerText', 'You said:', $this->answer->content); + } + + /** + * 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/QnA/lib/qnavoteform.php b/plugins/QnA/lib/qnavoteform.php new file mode 100644 index 0000000000..f6976c8834 --- /dev/null +++ b/plugins/QnA/lib/qnavoteform.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 QnavoteForm 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('qnavote', 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' => 'vote')); + } + + /** + * Action elements + * + * @return void + */ + function formActions() + { + // TRANS: Button text for submitting a poll response. + $this->out->submit('submit', _m('BUTTON', 'Submit')); + } +} +