]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '1.0.x' into qna
authorZach Copley <zach@status.net>
Thu, 24 Mar 2011 21:03:04 +0000 (14:03 -0700)
committerZach Copley <zach@status.net>
Thu, 24 Mar 2011 21:03:04 +0000 (14:03 -0700)
* 1.0.x: (26 commits)
  Fix typo in documentation.
  Fix i18n.
  Change formatting of i18n slightly. Translator hints were not picked up in pot file. May now they are.
  Localisation updates from http://translatewiki.net.
  Translator documentation updated. i18n/L10n updates. Superfluous whitespace removed. Add FIXME for missing class documentation.
  Update translator documentation. Remove superfluous whitespace. L10n/I18n updates. FIXMEs added for missing documentation or headers.
  Update translator documentation. i18n/L10n updates. Superfluous whitespace removed. Add FIXME in files with missing documentation.
  Fallback for RSVP display when Event is deleted
  Enhancement for 'ajax' form class: submit buttons behave more like normal submissions, submitting their name/values through a hidden field.
  Retool group join queue list ajax forms to use two buttons in one form, making it more ajax-submit-friendly. Needs util.js fixes for AJAX submission input buttons...
  Some fixes to make the notice stream class work
  let Inbox class go fingerpokin' in streams
  New NoticeStream class to reify streams of notices
  Refactoring on notification mail generation: common profile & footer chunks pulled out, notifications added for group joins.
  Fix typo in cf45c978
  Mass replacement of #-comments with //-comments
  Add pending members list to group navigation, if group has joins moderated or if it has pending requests open
  Split up some list/form classes, and get the 'approve' and 'cancel' links on group member queue working.
  Pending members queue list -- doesn't yet allow approval.
  Logic to have group joins turn into pending joins automatically when group is set to mod; allow users to cancel their pending group requests.
  ...

14 files changed:
plugins/QnA/QnAPlugin.php [new file with mode: 0644]
plugins/QnA/actions/qnanewanswer.php [new file with mode: 0644]
plugins/QnA/actions/qnanewquestion.php [new file with mode: 0644]
plugins/QnA/actions/qnashowanswer.php [new file with mode: 0644]
plugins/QnA/actions/qnashowquestion.php [new file with mode: 0644]
plugins/QnA/actions/qnavote.php [new file with mode: 0644]
plugins/QnA/classes/QnA_Answer.php [new file with mode: 0644]
plugins/QnA/classes/QnA_Question.php [new file with mode: 0644]
plugins/QnA/classes/QnA_Vote.php [new file with mode: 0644]
plugins/QnA/css/qna.css [new file with mode: 0644]
plugins/QnA/lib/qnaansweredform.php [new file with mode: 0644]
plugins/QnA/lib/qnaanswerform.php [new file with mode: 0644]
plugins/QnA/lib/qnaquestionform.php [new file with mode: 0644]
plugins/QnA/lib/qnavoteform.php [new file with mode: 0644]

diff --git a/plugins/QnA/QnAPlugin.php b/plugins/QnA/QnAPlugin.php
new file mode 100644 (file)
index 0000000..992641b
--- /dev/null
@@ -0,0 +1,405 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Microapp plugin for Questions and Answers
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Question and Answer plugin
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+class QnAPlugin extends MicroAppPlugin
+{
+    /**
+     * Set up our tables (question and answer)
+     *
+     * @see Schema
+     * @see ColumnDef
+     *
+     * @return boolean hook value; true means continue processing, false means stop.
+     */
+    function onCheckSchema()
+    {
+        $schema = Schema::get();
+
+        $schema->ensureTable('qna_question', QnA_Question::schemaDef());
+        $schema->ensureTable('qna_answer', QnA_Answer::schemaDef());
+        $schema->ensureTable('qna_vote', QnA_Vote::schemaDef());
+
+        return true;
+    }
+
+    /**
+     * 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 'QnaansweredForm':
+        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 = 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 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'));
+    }
+
+    /**
+     * 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 (file)
index 0000000..10b1046
--- /dev/null
@@ -0,0 +1,203 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Answer a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QuestonAndAnswer
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Answer a question
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @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 $answerText  = 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
+            );
+        }
+
+        $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')) {
+            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/actions/qnanewquestion.php b/plugins/QnA/actions/qnanewquestion.php
new file mode 100644 (file)
index 0000000..8682f8d
--- /dev/null
@@ -0,0 +1,216 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Add a new Question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Add a new Question
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @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 (file)
index 0000000..9721f22
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2010, StatusNet, Inc.
+ *
+ * Show an answer to a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 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 <zach@status.net>
+ * @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->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->answer->title
+        );
+        $this->elementEnd('h1');
+    }
+}
diff --git a/plugins/QnA/actions/qnashowquestion.php b/plugins/QnA/actions/qnashowquestion.php
new file mode 100644 (file)
index 0000000..6719125
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Show a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Show a question
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+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;
+    }
+
+    /**
+     * 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 (file)
index 0000000..8098cb8
--- /dev/null
@@ -0,0 +1,198 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Vote on a questino or answer
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Vote on a question or answer
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @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 (file)
index 0000000..57c08af
--- /dev/null
@@ -0,0 +1,327 @@
+<?php
+/**
+ * Data class to save answers to questions
+ *
+ * PHP version 5
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * For storing answers
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 $text;        // 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'
+                    ),
+                    '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 = 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;
+    }
+
+    function asHTML()
+    {
+        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 = _("<span class='automatic event-rsvp'><a href='%1s'>%2s</a> is attending <a href='%3s'>%4s</a>.</span>");
+            break;
+        case 'N':
+            $fmt = _("<span class='automatic event-rsvp'><a href='%1s'>%2s</a> is not attending <a href='%3s'>%4s</a>.</span>");
+            break;
+        case '?':
+            $fmt = _("<span class='automatic event-rsvp'><a href='%1s'>%2s</a> might attend <a href='%3s'>%4s</a>.</span>");
+            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
+     *
+     * @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->text        = $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 = '<a href="' . htmlspecialchars($answer->uri) . '">' . htmlspecialchars($question->title) . '</a>';
+        // 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 (file)
index 0000000..5230923
--- /dev/null
@@ -0,0 +1,260 @@
+<?php
+/**
+ * Data class to mark a notice as a question
+ *
+ * PHP version 5
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * For storing a question
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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();
+    }
+
+    /**
+     * 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 countAnswers()
+    {
+        $a              = new QnA_Answer();
+        $a->question_id = $this->id;
+        return $a-count();
+    }
+
+    static function fromNotice($notice)
+    {
+        return QnA_Question::staticGet('uri', $notice->uri);
+    }
+
+    /**
+     * 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 = '<a href="' . htmlspecialchars($q->uri) . '">' . htmlspecialchars($title) . '</a>';
+        // 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 (file)
index 0000000..ad57966
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+/**
+ * Data class to save users votes for 
+ *
+ * PHP version 5
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+/**
+ * For storing votes on question and answers
+ *
+ * @category QnA
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 (file)
index 0000000..4701b5a
--- /dev/null
@@ -0,0 +1 @@
+/* stubb for q&a css */
diff --git a/plugins/QnA/lib/qnaansweredform.php b/plugins/QnA/lib/qnaansweredform.php
new file mode 100644 (file)
index 0000000..a229e7f
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Form for answering a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Form to add a new answer to a question
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+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'));
+    }
+}
diff --git a/plugins/QnA/lib/qnaanswerform.php b/plugins/QnA/lib/qnaanswerform.php
new file mode 100644 (file)
index 0000000..f89f6c7
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Form for answering a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Form to add a new answer to a question
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+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', 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->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'));
+    }
+}
+
diff --git a/plugins/QnA/lib/qnaquestionform.php b/plugins/QnA/lib/qnaquestionform.php
new file mode 100644 (file)
index 0000000..9d0c2aa
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Form for adding a new question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@copley.name>
+ * @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 <zach@copley.name>
+ * @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/qnavoteform.php b/plugins/QnA/lib/qnavoteform.php
new file mode 100644 (file)
index 0000000..f6976c8
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+/**
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * Form for answering a question
+ *
+ * PHP version 5
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+    // This check helps protect against security problems;
+    // your code file can't be executed directly from the web.
+    exit(1);
+}
+
+/**
+ * Form to add a new answer to a question
+ *
+ * @category  QnA
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @copyright 2011 StatusNet, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link      http://status.net/
+ */
+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'));
+    }
+}
+