MDL-20636 More progress.
authorTim Hunt <T.J.Hunt@open.ac.uk>
Tue, 21 Dec 2010 17:01:46 +0000 (17:01 +0000)
committerTim Hunt <T.J.Hunt@open.ac.uk>
Thu, 13 Jan 2011 18:35:34 +0000 (18:35 +0000)
question/engine/bank.php
question/engine/datalib.php
question/engine/lib.php
question/engine/renderer.php
question/flags.js
question/preview.js
question/previewlib.php
question/qengine.js
question/question.php
question/todo/diffstat.txt
question/toggleflag.php

index 3bc394a..b4a8c0b 100644 (file)
@@ -77,6 +77,14 @@ abstract class question_bank {
         return self::$questiontypes[$qtypename];
     }
 
+    /**
+     * @param string $qtypename the internal name of a question type. For example multichoice.
+     * @return boolean whether users are allowed to create questions of this type.
+     */
+    public static function qtype_enabled($qtypename) {
+        ;
+    }
+
     /**
      * @param $qtypename the internal name of a question type, for example multichoice.
      * @return string the human_readable name of this question type, from the language pack.
index cd1c997..960867f 100644 (file)
  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class question_engine_data_mapper {
+    /**
+     * @var moodle_database normally points to global $DB, but I prefer not to
+     * use globals if I can help it.
+     */
+    protected $db;
+
+    /**
+     * @param moodle_database $db a database connectoin. Defaults to global $DB.
+     */
+    public function __construct($db = null) {
+        if (is_null($db)) {
+            global $DB;
+            new moodle_database;
+            $this->db = $DB;
+        } else {
+            $this->db = $db;
+        }
+    }
+
     /**
      * Store an entire {@link question_usage_by_activity} in the database,
      * including all the question_attempts that comprise it.
@@ -45,10 +64,7 @@ class question_engine_data_mapper {
         $record->component = addslashes($quba->get_owning_component());
         $record->preferredbehaviour = addslashes($quba->get_preferred_behaviour());
 
-        $newid = insert_record('question_usages', $record);
-        if (!$newid) {
-            throw new Exception('Failed to save questions_usage_by_activity.');
-        }
+        $newid = $this->db->insert_record('question_usages', $record);
         $quba->set_id_from_database($newid);
 
         foreach ($quba->get_attempt_iterator() as $qa) {
@@ -74,10 +90,7 @@ class question_engine_data_mapper {
         $record->rightanswer = addslashes($qa->get_right_answer_summary());
         $record->responsesummary = addslashes($qa->get_response_summary());
         $record->timemodified = time();
-        $record->id = insert_record('question_attempts', $record);
-        if (!$record->id) {
-            throw new Exception('Failed to save question_attempt ' . $qa->get_slot());
-        }
+        $record->id = $this->db->insert_record('question_attempts', $record);
 
         foreach ($qa->get_step_iterator() as $seq => $step) {
             $this->insert_question_attempt_step($step, $record->id, $seq);
@@ -98,18 +111,14 @@ class question_engine_data_mapper {
         $record->timecreated = $step->get_timecreated();
         $record->userid = $step->get_user_id();
 
-        $record->id = insert_record('question_attempt_steps', $record);
-        if (!$record->id) {
-            throw new Exception('Failed to save question_attempt_step' . $seq .
-                    ' for question attempt id ' . $questionattemptid);
-        }
+        $record->id = $this->db->insert_record('question_attempt_steps', $record);
 
         foreach ($step->get_all_data() as $name => $value) {
             $data = new stdClass;
             $data->attemptstepid = $record->id;
             $data->name = addslashes($name);
             $data->value = addslashes($value);
-            insert_record('question_attempt_step_data', $data, false);
+            $this->db->insert_record('question_attempt_step_data', $data, false);
         }
     }
 
@@ -119,8 +128,7 @@ class question_engine_data_mapper {
      * @param question_attempt_step the step that was loaded.
      */
     public function load_question_attempt_step($stepid) {
-        global $CFG;
-        $records = get_records_sql("
+        $records = $this->db->get_records_sql("
 SELECT
     COALESCE(qasd.id, -1 * qas.id) AS id,
     qas.id AS attemptstepid,
@@ -133,12 +141,12 @@ SELECT
     qasd.name,
     qasd.value
 
-FROM {$CFG->prefix}question_attempt_steps qas
-LEFT JOIN {$CFG->prefix}question_attempt_step_data qasd ON qasd.attemptstepid = qas.id
+FROM {question_attempt_steps} qas
+LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 
 WHERE
-    qas.id = $stepid
-        ");
+    qas.id = :stepid
+        ", array('stepid' => $stepid));
 
         if (!$records) {
             throw new Exception('Failed to load question_attempt_step ' . $stepid);
@@ -154,8 +162,7 @@ WHERE
      * @param question_attempt the question attempt that was loaded.
      */
     public function load_question_attempt($questionattemptid) {
-        global $CFG;
-        $records = get_records_sql("
+        $records = $this->db->get_records_sql("
 SELECT
     COALESCE(qasd.id, -1 * qas.id) AS id,
     quba.preferredbehaviour,
@@ -180,17 +187,17 @@ SELECT
     qasd.name,
     qasd.value
 
-FROM {$CFG->prefix}question_attempts qa
-JOIN {$CFG->prefix}question_usages quba ON quba.id = qa.questionusageid
-LEFT JOIN {$CFG->prefix}question_attempt_steps qas ON qas.questionattemptid = qa.id
-LEFT JOIN {$CFG->prefix}question_attempt_step_data qasd ON qasd.attemptstepid = qas.id
+FROM      {question_attempts           qa
+JOIN      {question_usages}            quba ON quba.id               = qa.questionusageid
+LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
+LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 
 WHERE
-    qa.id = $questionattemptid
+    qa.id = :questionattemptid
 
 ORDER BY
     qas.sequencenumber
-        ");
+        ", array('questionattemptid' => $questionattemptid));
 
         if (!$records) {
             throw new Exception('Failed to load question_attempt ' . $questionattemptid);
@@ -208,8 +215,7 @@ ORDER BY
      * @param question_usage_by_activity the usage that was loaded.
      */
     public function load_questions_usage_by_activity($qubaid) {
-        global $CFG;
-        $records = get_records_sql("
+        $records = $this->db->get_records_sql("
 SELECT
     COALESCE(qasd.id, -1 * qas.id) AS id,
     quba.id AS qubaid,
@@ -237,18 +243,18 @@ SELECT
     qasd.name,
     qasd.value
 
-FROM {$CFG->prefix}question_usages quba
-LEFT JOIN {$CFG->prefix}question_attempts qa ON qa.questionusageid = quba.id
-LEFT JOIN {$CFG->prefix}question_attempt_steps qas ON qas.questionattemptid = qa.id
-LEFT JOIN {$CFG->prefix}question_attempt_step_data qasd ON qasd.attemptstepid = qas.id
+FROM      {question_usages}            quba
+LEFT JOIN {question_attempts}          qa   ON qa.questionusageid    = quba.id
+LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
+LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 
 WHERE
-    quba.id = $qubaid
+    quba.id = :qubaid
 
 ORDER BY
     qa.slot,
     qas.sequencenumber
-    ");
+    ", array('qubaid', $qubaid));
 
         if (!$records) {
             throw new Exception('Failed to load questions_usage_by_activity ' . $qubaid);
@@ -266,8 +272,6 @@ ORDER BY
      * @return array of records. See the SQL in this function to see the fields available.
      */
     public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
-        global $CFG;
-
         list($slottest, $params) = get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
 
         $records = get_records_sql("
@@ -293,17 +297,13 @@ SELECT
     qas.userid
 
 FROM {$qubaids->from_question_attempts('qa')}
-JOIN {$CFG->prefix}question_attempt_steps qas ON
+JOIN {question_attempt_steps} qas ON
         qas.id = {$this->latest_step_for_qa_subquery()}
 
 WHERE
     {$qubaids->where()} AND
     qa.slot $slottest
-        ");
-
-        if (!$records) {
-            $records = array();
-        }
+        ", $params + $qubaids->from_where_params());
 
         return $records;
     }
@@ -321,11 +321,9 @@ WHERE
      * $manuallygraded and $all.
      */
     public function load_questions_usages_question_state_summary(qubaid_condition $qubaids, $slots) {
-        global $CFG;
-
         list($slottest, $params) = get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
 
-        $rs = get_recordset_sql("
+        $rs = $this->db->get_recordset_sql("
 SELECT
     qa.slot,
     qa.questionid,
@@ -336,9 +334,9 @@ SELECT
     COUNT(1) AS numattempts
 
 FROM {$qubaids->from_question_attempts('qa')}
-JOIN {$CFG->prefix}question_attempt_steps qas ON
+JOIN {question_attempt_steps} qas ON
         qas.id = {$this->latest_step_for_qa_subquery()}
-JOIN {$CFG->prefix}question q ON q.id = qa.questionid
+JOIN {question} q ON q.id = qa.questionid
 
 WHERE
     {$qubaids->where()} AND
@@ -356,7 +354,7 @@ ORDER BY
     qa.questionid,
     q.name,
     q.id
-        ");
+        ", $params + $qubaids->from_where_params());
 
         if (!$rs) {
             throw new moodle_exception('errorloadingdata');
@@ -694,15 +692,13 @@ ORDER BY
      * @param integer $sessionid the question_attempt id.
      * @param boolean $newstate the new state of the flag. true = flagged.
      */
-    public function update_question_attempt_flag($qubaid, $questionid, $qaid, $newstate) {
-        if (!record_exists('question_attempts', 'id', $qaid, 
-                'questionusageid', $qubaid, 'questionid', $questionid)) {
+    public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
+        if (!$this->db->record_exists('question_attempts', array('id' => $qaid, 
+                'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
             throw new Exception('invalid ids');
         }
 
-        if (!set_field('question_attempts', 'flagged', $newstate, 'id', $qaid)) {
-            throw new Exception('flag update failed');
-        }
+        $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
     }
 
     /**
index f3a9e00..7c556e4 100644 (file)
@@ -504,12 +504,12 @@ abstract class question_flags {
      * @param object $user the user. If null, defaults to $USER.
      * @return string that needs to be sent to question/toggleflag.php for it to work.
      */
-    protected static function get_toggle_checksum($qubaid, $questionid, $qaid, $user = null) {
+    protected static function get_toggle_checksum($qubaid, $questionid, $qaid, $slot, $user = null) {
         if (is_null($user)) {
             global $USER;
             $user = $USER;
         }
-        return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid);
+        return md5($qubaid . "_" . $user->secret . "_" . $questionid . "_" . $qaid . "_" . $slot);
     }
 
     /**
@@ -521,8 +521,9 @@ abstract class question_flags {
         $qaid = $qa->get_database_id();
         $qubaid = $qa->get_usage_id();
         $qid = $qa->get_question()->id;
-        $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid);
-        return "qaid=$qaid&qubaid=$qubaid&qid=$qid&checksum=$checksum&sesskey=" . sesskey();
+        $slot = $qa->get_slot();
+        $checksum = self::get_toggle_checksum($qubaid, $qid, $qaid, $slot);
+        return "qaid=$qaid&qubaid=$qubaid&qid=$qid&slot=$slot&checksum=$checksum&sesskey=" . sesskey();
     }
 
     /**
@@ -535,18 +536,18 @@ abstract class question_flags {
      *      corresponding to the last three arguments.
      * @param boolean $newstate the new state of the flag. true = flagged.
      */
-    public static function update_flag($qubaid, $questionid, $qaid, $checksum, $newstate) {
+    public static function update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate) {
         // Check the checksum - it is very hard to know who a question session belongs
         // to, so we require that checksum parameter is matches an md5 hash of the 
         // three ids and the users username. Since we are only updating a flag, that
         // probably makes it sufficiently difficult for malicious users to toggle
         // other users flags.
-        if ($checksum != question_flags::get_toggle_checksum($qubaid, $questionid, $qaid)) {
+        if ($checksum != question_flags::get_toggle_checksum($qubaid, $questionid, $qaid, $slot)) {
             throw new Exception('checksum failure');
         }
 
         $dm = new question_engine_data_mapper();
-        $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $newstate);
+        $dm->update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate);
     }
 
     public static function initialise_js() {
index 2f45bcb..81d58fd 100644 (file)
@@ -211,9 +211,9 @@ class core_question_renderer extends renderer_base {
                 // of a stupid IE bug: http://www.456bereastreet.com/archive/200802/beware_of_id_and_name_attribute_mixups_when_using_getelementbyid_in_internet_explorer/
                 $flagcontent = '<input type="hidden" name="' . $id . '" value="0" />' .
                         '<input type="checkbox" id="' . $id . 'checkbox" name="' . $id . '" value="1" ' . $checked . ' />' .
+                        '<input type="hidden" value="' . s($postdata) . '" class="questionflagpostdata" />' .
                         '<label id="' . $id . 'label" for="' . $id . '">' . $this->get_flag_html(
                         $qa->is_flagged(), $id . 'img') . '</label>' . "\n" .
-                        print_js_call('question_flag_changer.init_flag', array($id, $postdata, $qa->get_slot()), true);
                 break;
             default:
                 $flagcontent = '';
index 90f9462..10e0b73 100644 (file)
@@ -42,7 +42,6 @@ M.core_question_flags = {
             Y.io(M.core_question_flags.actionurl , {method: 'POST', 'data': postdata});
             M.core_question_flags.fire_listeners(postdata);
         }, document.body, 'input.questionflagimage');
-
     },
 
     update_flag: function(input, image) {
@@ -56,8 +55,8 @@ M.core_question_flags = {
     fire_listeners: function(postdata) {
         for (var i = 0; i < M.core_question_flags.listeners.length; i++) {
             M.core_question_flags.listeners[i](
-                postdata.match(/\baid=(\d+)\b/)[1],
-                postdata.match(/\bqid=(\d+)\b/)[1],
+                postdata.match(/\bqubaid=(\d+)\b/)[1],
+                postdata.match(/\bslot=(\d+)\b/)[1],
                 postdata.match(/\bnewstate=(\d+)\b/)[1]
             );
         }
index c502d8d..56d0d58 100644 (file)
@@ -15,7 +15,7 @@
 
 
 /**
- * This file the Moodle question engine.
+ * JavaScript required by the question preview pop-up.
  *
  * @package moodlecore
  * @subpackage questionengine
  */
 
 
+M.core_question_preview = M.core_question_preview || {};
+
+
 /**
  * Initialise JavaScript-specific parts of the question preview popup.
  */
-function question_preview_init(caption, addto) {
-    // Add a close button to the window.
-    var button = document.createElement('input');
-    button.type = 'button';
-    button.value = caption;
+M.core_question_preview.init(Y) {
+    M.core_question_engine.init_form(Y, '#responseform');
 
-    YAHOO.util.Event.addListener(button, 'click', function() { window.close() });
-
-    var container = document.getElementById(addto);
-    container.appendChild(button);
+    // Add a close button to the window.
+    var closebutton = Y.Node.create('<input type="button" />');
+    button.value = M.str.question.closepreview;
+    Y.one('#previewcontrols').append(closebutton);
+    Y.on('click', function() { window.close() }, closebutton);
 
-    // Make changint the settings disable all submit buttons, like clicking one of the
-    // question buttons does.
-    var form = document.getElementById('mform1');
-    YAHOO.util.Event.addListener(form, 'submit',
-            question_prevent_repeat_submission, document.body);
-}
\ No newline at end of file
+    // Make changing the settings disable all submit buttons, like clicking one
+    // of the question buttons does.
+    Y.on('submit', M.core_question_engine.prevent_repeat_submission, '#mform1', null, Y)
+}
index 4f60b03..f56f84a 100644 (file)
 // You should have received a copy of the GNU General Public License
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
+
 /**
  * Library functions used by question/preview.php.
  *
- * @package    core
+ * @package    moodlecore
  * @subpackage questionengine
  * @copyright  2010 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+
+/**
+ * Settings form for the preview options.
+ *
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class preview_options_form extends moodleform {
+    public function definition() {
+        $mform = $this->_form;
+
+        $hiddenofvisible = array(
+            question_display_options::HIDDEN => get_string('notshown', 'question'),
+            question_display_options::VISIBLE => get_string('shown', 'question'),
+        );
+
+        $mform->addElement('header', 'optionsheader', get_string('changeoptions', 'question'));
+
+        $behaviours = question_engine::get_behaviour_options($this->_customdata->get_preferred_behaviour());
+        $mform->addElement('select', 'behaviour', get_string('howquestionsbehave', 'question'), $behaviours);
+        $mform->setHelpButton('behaviour', array('howquestionsbehave', get_string('howquestionsbehave', 'question'), 'question'));
+
+        $mform->addElement('text', 'maxmark', get_string('markedoutof', 'question'), array('size' => '5'));
+        $mform->setType('maxmark', PARAM_NUMBER);
+
+        $mform->addElement('select', 'correctness', get_string('whethercorrect', 'question'), $hiddenofvisible);
+
+        $marksoptions = array(
+            question_display_options::HIDDEN => get_string('notshown', 'question'),
+            question_display_options::MAX_ONLY => get_string('showmaxmarkonly', 'question'),
+            question_display_options::MARK_AND_MAX => get_string('showmarkandmax', 'question'),
+        );
+        $mform->addElement('select', 'marks', get_string('marks', 'question'), $marksoptions);
+
+        $mform->addElement('select', 'markdp', get_string('decimalplacesingrades', 'question'),
+                question_engine::get_dp_options());
+
+        $mform->addElement('select', 'feedback', get_string('specificfeedback', 'question'), $hiddenofvisible);
+
+        $mform->addElement('select', 'generalfeedback', get_string('generalfeedback', 'question'), $hiddenofvisible);
+
+        $mform->addElement('select', 'rightanswer', get_string('rightanswer', 'question'), $hiddenofvisible);
+
+        $mform->addElement('select', 'history', get_string('responsehistory', 'question'), $hiddenofvisible);
+
+        $mform->addElement('submit', 'submit', get_string('restartwiththeseoptions', 'question'), $hiddenofvisible);
+    }
+}
+
+
+/**
+ * Displays question preview options as default and set the options
+ * Setting default, getting and setting user preferences in question preview options.
+ *
+ * @copyright 2010 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_preview_options extends question_display_options {
+    /** @var string the behaviour to use for this preview. */
+    public $behaviour;
+
+    /** @var number the maximum mark to use for this preview. */
+    public $maxmark;
+
+    /** @var string prefix to append to field names to get user_preference names. */
+    const OPTIONPREFIX = 'question_preview_options_';
+
+    /**
+     * Constructor.
+     */
+    public function __construct($question) {
+        global $CFG;
+        $this->behaviour = 'deferredfeedback';
+        $this->maxmark = $question->defaultmark;
+        $this->correctness = self::VISIBLE;
+        $this->marks = self::MARK_AND_MAX;
+        $this->markdp = $CFG->quiz_decimalpoints;
+        $this->feedback = self::VISIBLE;
+        $this->numpartscorrect = $this->feedback;
+        $this->generalfeedback = self::VISIBLE;
+        $this->rightanswer = self::VISIBLE;
+        $this->history = self::HIDDEN;
+        $this->flags = self::HIDDEN;
+        $this->manualcomment = self::HIDDEN;
+    }
+
+    /**
+     * @return array names of the options we store in the user preferences table.
+     */
+    protected function get_user_pref_fields() {
+        return array('behaviour', 'correctness', 'marks', 'markdp', 'feedback',
+                'generalfeedback', 'rightanswer', 'history');
+    }
+
+    /**
+     * @return array names and param types of the options we read from the request.
+     */
+    protected function get_field_types() {
+        return array(
+            'behaviour' => PARAM_ALPHA,
+            'maxmark' => PARAM_NUMBER,
+            'correctness' => PARAM_BOOL,
+            'marks' => PARAM_INT,
+            'markdp' => PARAM_INT,
+            'feedback' => PARAM_BOOL,
+            'generalfeedback' => PARAM_BOOL,
+            'rightanswer' => PARAM_BOOL,
+            'history' => PARAM_BOOL,
+        );
+    }
+
+    /**
+     * Load the value of the options from the user_preferences table.
+     */
+    public function load_user_defaults() {
+        foreach ($this->get_user_pref_fields() as $field) {
+            $this->$field = get_user_preferences(
+                    self::OPTIONPREFIX . $field, $this->$field);
+        }
+        $this->numpartscorrect = $this->feedback;
+    }
+
+    /**
+     * Save a change to the user's preview options to the database.
+     * @param object $newoptions
+     */
+    public function save_user_preview_options($newoptions) {
+        foreach ($this->get_user_pref_fields() as $field) {
+            if (isset($newoptions->$field)) {
+                set_user_preference(self::OPTIONPREFIX . $field, $newoptions->$field);
+            }
+        }
+    }
+
+    /**
+     * Set the value of any fields included in the request.
+     */
+    public function set_from_request() {
+        foreach ($this->get_field_types() as $field => $type) {
+            $this->$field = optional_param($field, $this->$field, $type);
+        }
+        $this->numpartscorrect = $this->feedback;
+    }
+
+    /**
+     * @return string URL fragment. Parameters needed in the URL when continuing
+     * this preview.
+     */
+    public function get_query_string() {
+        $querystring = array();
+        foreach ($this->get_field_types() as $field => $notused) {
+            if ($field == 'behaviour' || $field == 'maxmark') {
+                continue;
+            }
+            $querystring[] = $field . '=' . $this->$field;
+        }
+        return implode('&', $querystring);
+    }
+}
+
+
 /**
  * Called via pluginfile.php -> question_pluginfile to serve files belonging to
  * a question in a question_attempt when that attempt is a preview.
@@ -89,7 +251,7 @@ function question_preview_question_pluginfile($course, $context, $component,
 
     $options = quiz_get_renderoptions($quiz, $attempt, $context, $state);
     $options->noeditlink = true;
-    // XXX: mulitichoice type needs quiz id to get maxgrade
+    // TODO: mulitichoice type needs quiz id to get maxgrade
     $options->quizid = 0;
 
     if (!question_check_file_access($question, $state, $options, $context->id, $component,
@@ -106,3 +268,31 @@ function question_preview_question_pluginfile($course, $context, $component,
 
     send_stored_file($file, 0, 0, $forcedownload);
 }
+
+/**
+ * The the URL to use for actions relating to this preview.
+ * @param integer $questionid the question being previewed.
+ * @param integer $qubaid the id of the question usage for this preview.
+ * @param question_preview_options $options the options in use.
+ */
+function question_preview_action_url($questionid, $qubaid,
+        question_preview_options $options) {
+    global $CFG;
+    $url = $CFG->wwwroot . '/question/preview.php?id=' . $questionid . '&previewid=' . $qubaid;
+    return $url . '&' . $options->get_query_string();
+}
+
+/**
+ * Delete the current preview, if any, and redirect to start a new preview.
+ * @param integer $previewid
+ * @param integer $questionid
+ * @param object $displayoptions
+ */
+function restart_preview($previewid, $questionid, $displayoptions) {
+    if ($previewid) {
+        begin_sql();
+        question_engine::delete_questions_usage_by_activity($previewid);
+        commit_sql();
+    }
+    redirect(question_preview_url($questionid, $displayoptions->behaviour, $displayoptions->maxmark, $displayoptions));
+}
index 7c5b745..f8f0663 100644 (file)
@@ -1,5 +1,26 @@
 M.core_question_engine = M.core_question_engine || {};
 
+/**
+ * Flag used by M.core_question_engine.prevent_repeat_submission.
+ */
+M.core_question_engine.questionformalreadysubmitted = false;
+
+/**
+ * Initialise a question submit button. This saves the scroll position and
+ * sets the fragment on the form submit URL so the page reloads in the right place.
+ * @param id the id of the button in the HTML.
+ * @param slot the number of the question_attempt within the usage.
+ */
+M.core_question_engine.init_submit_button(Y, button, slot) {
+    Y.on('click', function(e) {
+        var scrollpos = document.getElementById('scrollpos');
+        if (scrollpos) {
+            scrollpos.value = YAHOO.util.Dom.getDocumentScrollTop();
+        }
+        button.form.action = button.form.action + '#q' + slot;
+    }, button);
+}
+
 /**
  * Initialise a form that contains questions printed using print_question.
  * This has the effect of:
@@ -7,17 +28,70 @@ M.core_question_engine = M.core_question_engine || {};
  * 2. Stopping enter from submitting the form (or toggling the next flag) unless
  *    keyboard focus is on the submit button or the flag.
  * 3. Removes any '.questionflagsavebutton's, since we have JavaScript to toggle
- *    the flags using Ajax.
+ *    the flags using ajax.
+ * 4. Scroll to the position indicated by scrollpos= in the URL, if it is there.
+ * 5. Prevent the user from repeatedly submitting the form.
  * @param Y the Yahoo object. Needs to have the DOM and Event modules loaded.
  * @param form something that can be passed to Y.one, to find the form element.
  */
 M.core_question_engine.init_form = function(Y, form) {
     Y.one(form).setAttribute('autocomplete', 'off');
+
+    Y.on('submit', M.core_question_engine.prevent_repeat_submission, form, form, Y);
+
     Y.on('key', function (e) {
         if (!e.target.test('a') && !e.target.test('input[type=submit]') &&
                 !e.target.test('input[type=img]')) {
             e.preventDefault();
         }
     }, form, 'press:13');
+
     Y.one(form).all('.questionflagsavebutton').remove();
+
+    var matches = window.location.href.match(/^.*[?&]scrollpos=(\d*)(?:&|$|#).*$/, '$1');
+    if (matches) {
+        // onDOMReady is the effective one here. I am leaving the immediate call to
+        // window.scrollTo in case it reduces flicker.
+        window.scrollTo(0, matches[1]);
+        Y.on('domready', function() { window.scrollTo(0, matches[1]); });
+
+        // And the following horror is necessary to make it work in IE 8.
+        // Note that the class ie8 on body is only there in Moodle 2.0 and OU Moodle.
+        if (YAHOO.util.Dom.hasClass(document.body, 'ie')) {
+            question_force_ie_to_scroll(matches[1])
+        }
+    }
+}
+
+/**
+ * Event handler to stop the quiz form being submitted more than once.
+ * @param e the form submit event.
+ * @param form the form element.
+ */
+M.core_question_engine.prevent_repeat_submission(e, Y) {
+    if (M.core_question_engine.questionformalreadysubmitted) {
+        e.halt();
+        return;
+    }
+
+    setTimeout(function() {
+        Y.all('input[type=submit]').disabled = true;
+    }, 0);
+    M.core_question_engine.questionformalreadysubmitted = true;
+}
+
+/**
+ * Beat IE into submission.
+ * @param targetpos the target scroll position.
+ */
+M.core_question_engine.force_ie_to_scroll(targetpos) {
+    var hackcount = 25;
+    function do_scroll() {
+        window.scrollTo(0, targetpos);
+        hackcount -= 1;
+        if (hackcount > 0) {
+            setTimeout(do_scroll, 10);
+        }
+    }
+    Y.on('load', do_scroll, window);
 }
index 0041413..2ab21d2 100644 (file)
@@ -105,8 +105,7 @@ if ($id) {
     $question->qtype = $qtype;
 
     // Check that users are allowed to create this question type at the moment.
-    $allowedtypes = question_type_menu();
-    if (!isset($allowedtypes[$qtype])) {
+    if (!question_bank::qtype_enabled($qtype)) {
         print_error('cannotenable', 'question', $returnurl, $qtype);
     }
 
@@ -121,6 +120,8 @@ if ($id) {
     print_error('notenoughdatatoeditaquestion', 'question', $returnurl);
 }
 
+$qtypeobj = question_bank::get_qtype($question->qtype);
+
 // Validate the question category.
 if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) {
     print_error('categorydoesnotexist', 'question', $returnurl);
@@ -164,23 +165,13 @@ if ($id) {
 }
 
 // Validate the question type.
-if (!isset($QTYPES[$question->qtype])) {
-    print_error('unknownquestiontype', 'question', $returnurl, $question->qtype);
-}
 $PAGE->set_pagetype('question-type-' . $question->qtype);
 
 // Create the question editing form.
 if ($wizardnow!=='' && !$movecontext){
-    if (!method_exists($QTYPES[$question->qtype], 'next_wizard_form')){
-        print_error('missingimportantcode', 'question', $returnurl, 'wizard form definition');
-    } else {
-        $mform = $QTYPES[$question->qtype]->next_wizard_form('question.php', $question, $wizardnow, $formeditable);
-    }
+    $mform = $qtypeobj->next_wizard_form('question.php', $question, $wizardnow, $formeditable);
 } else {
-    $mform = $QTYPES[$question->qtype]->create_editing_form('question.php', $question, $category, $contexts, $formeditable);
-}
-if ($mform === null) {
-    print_error('missingimportantcode', 'question', $returnurl, 'question editing form definition for "'.$question->qtype.'"');
+    $mform = $qtypeobj->create_editing_form('question.php', $question, $category, $contexts, $formeditable);
 }
 $toform = fullclone($question); // send the question object and a few more parameters to the form
 $toform->category = "$category->id,$category->contextid";
@@ -202,15 +193,13 @@ $toform->inpopup = $inpopup;
 
 $mform->set_data($toform);
 
-if ($mform->is_cancelled()){
+if ($mform->is_cancelled()) {
     if ($inpopup) {
         close_window();
     } else {
-        if (!empty($question->id)) {
-            $returnurl->param('lastchanged', $question->id);
-        }
         redirect($returnurl->out(false));
     }
+
 } else if ($fromform = $mform->get_data()) {
     /// If we are saving as a copy, break the connection to the old question.
     if (!empty($fromform->makecopy)) {
@@ -248,7 +237,7 @@ if ($mform->is_cancelled()){
 
     } else {
         // We are acutally saving the question.
-        $question = $QTYPES[$question->qtype]->save_question($question, $fromform);
+        $question = $qtypeobj->save_question($question, $fromform);
         if (!empty($CFG->usetags) && isset($fromform->tags)) {
             // A wizardpage from multipe pages questiontype like calculated may not
             // allow editing the question tags, hence the isset($fromform->tags) test.
@@ -257,7 +246,7 @@ if ($mform->is_cancelled()){
         }
     }
 
-    if (($QTYPES[$question->qtype]->finished_edit_wizard($fromform)) || $movecontext) {
+    if (($qtypeobj->finished_edit_wizard($fromform)) || $movecontext) {
         if ($inpopup) {
             echo $OUTPUT->notification(get_string('changessaved'), '');
             close_window(3);
@@ -291,7 +280,7 @@ if ($mform->is_cancelled()){
     }
 
 } else {
-    $streditingquestion = $QTYPES[$question->qtype]->get_heading();
+    $streditingquestion = $qtypeobj->get_heading();
     $PAGE->set_title($streditingquestion);
     $PAGE->set_heading($COURSE->fullname);
     if ($cm !== null) {
@@ -315,7 +304,6 @@ if ($mform->is_cancelled()){
 
     // Display a heading, question editing form and possibly some extra content needed for
     // for this question type.
-    $QTYPES[$question->qtype]->display_question_editing_page($mform, $question, $wizardnow);
+    $qtypeobj->display_question_editing_page($mform, $question, $wizardnow);
     echo $OUTPUT->footer();
 }
-
index 218c331..d35ff86 100644 (file)
@@ -162,11 +162,11 @@ DONE question/file.php                                  |  171 +- | but this fil
 DONE question/move_form.php                             |   32 +-
 DONE question/preview.js                                |   47 +
  question/preview.php                               |  408 ++--
- question/previewlib.php                            |  214 ++
- question/qengine.js                                |  181 ++
- question/question.php                              |    3 +-
+DONE question/previewlib.php                            |  214 ++
+DONE question/qengine.js                                |  181 ++
+DONE question/question.php                              |    3 +-
  question/restorelib.php                            |   88 +-
- question/toggleflag.php                            |   49 +
+DONE question/toggleflag.php                            |   49 +
 
  question/behaviour/behaviourbase.php               |  627 +++++
  question/behaviour/rendererbase.php                |  200 ++
index 0c49db4..24ceef9 100644 (file)
@@ -1,47 +1,49 @@
 <?php
+
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle 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 General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+
 /**
  * Used by ajax calls to toggle the flagged state of a question in an attempt.
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package questionbank
+ *
+ * @package moodlecore
+ * @subpackage questionengine
+ * @copyright 2009 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+
+define('AJAX_SCRIPT', true);
+
 require_once('../config.php');
-require_once($CFG->libdir.'/questionlib.php');
+require_once($CFG->dirroot . '/question/engine/lib.php');
 
 // Parameters
-$sessionid = required_param('qsid', PARAM_INT);
-$attemptid = required_param('aid', PARAM_INT);
+$qaid = required_param('qaid', PARAM_INT);
+$qubaid = required_param('qubaid', PARAM_INT);
 $questionid = required_param('qid', PARAM_INT);
+$slot = required_param('slot', PARAM_INT);
 $newstate = required_param('newstate', PARAM_BOOL);
 $checksum = required_param('checksum', PARAM_ALPHANUM);
 
 // Check user is logged in.
 require_login();
-
-// Check the sesskey.
-if (!confirm_sesskey()) {
-    echo 'sesskey failure';
-}
-
-// Check the checksum - it is very hard to know who a question session belongs
-// to, so we require that checksum parameter is matches an md5 hash of the
-// three ids and the users username. Since we are only updating a flag, that
-// probably makes it sufficiently difficult for malicious users to toggle
-// other users flags.
-if ($checksum != md5($attemptid . "_" . $USER->secret . "_" . $questionid . "_" . $sessionid)) {
-    echo 'checksum failure';
-}
+require_sesskey();
 
 // Check that the requested session really exists
-$questionsession = $DB->get_record('question_sessions', array('id' => $sessionid,
-        'attemptid' => $attemptid, 'questionid' => $questionid));
-if (!$questionsession) {
-    echo 'invalid ids';
-}
-
-// Now change state
-if (!question_update_flag($sessionid, $newstate)) {
-    echo 'update failed';
-}
+question_flags::update_flag($qubaid, $questionid, $qaid, $slot, $checksum, $newstate);
 
 echo 'OK';