diff --git a/question/engine/bank.php b/question/engine/bank.php index 3bc394a2e45c5..b4a8c0b9b41c5 100644 --- a/question/engine/bank.php +++ b/question/engine/bank.php @@ -77,6 +77,14 @@ public static function get_qtype($qtypename, $mustexist = true) { 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. diff --git a/question/engine/datalib.php b/question/engine/datalib.php index cd1c9974a3456..960867f12ebe0 100644 --- a/question/engine/datalib.php +++ b/question/engine/datalib.php @@ -34,6 +34,25 @@ * @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 @@ public function insert_questions_usage_by_activity(question_usage_by_activity $q $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 @@ public function insert_question_attempt(question_attempt $qa) { $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 @@ public function insert_question_attempt_step(question_attempt_step $step, $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 @@ public function insert_question_attempt_step(question_attempt_step $step, * @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 @@ public function load_question_attempt_step($stepid) { 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 @@ public function load_question_attempt_step($stepid) { * @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 @@ public function load_question_attempt($questionattemptid) { 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 @@ public function load_question_attempt($questionattemptid) { * @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 @@ public function load_questions_usage_by_activity($qubaid) { 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 @@ public function load_questions_usage_by_activity($qubaid) { * @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 @@ public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $s 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 @@ public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $s * $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 @@ public function load_questions_usages_question_state_summary(qubaid_condition $q 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 @@ public function load_questions_usages_question_state_summary(qubaid_condition $q qa.questionid, q.name, q.id - "); + ", $params + $qubaids->from_where_params()); if (!$rs) { throw new moodle_exception('errorloadingdata'); @@ -694,15 +692,13 @@ public function delete_previews($questionid) { * @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)); } /** diff --git a/question/engine/lib.php b/question/engine/lib.php index f3a9e005b6b7c..7c556e46ba483 100644 --- a/question/engine/lib.php +++ b/question/engine/lib.php @@ -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 @@ public static function get_postdata(question_attempt $qa) { $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 @@ public static function get_postdata(question_attempt $qa) { * 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() { diff --git a/question/engine/renderer.php b/question/engine/renderer.php index 2f45bcbe2d475..81d58fdea6b52 100644 --- a/question/engine/renderer.php +++ b/question/engine/renderer.php @@ -211,9 +211,9 @@ protected function question_flag(question_attempt $qa, $flagsoption) { // 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 = '' . '' . + '' . '' . "\n" . - print_js_call('question_flag_changer.init_flag', array($id, $postdata, $qa->get_slot()), true); break; default: $flagcontent = ''; diff --git a/question/flags.js b/question/flags.js index 90f9462b87f89..10e0b739948f8 100644 --- a/question/flags.js +++ b/question/flags.js @@ -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] ); } diff --git a/question/preview.js b/question/preview.js index c502d8d653b99..56d0d5880a271 100644 --- a/question/preview.js +++ b/question/preview.js @@ -15,7 +15,7 @@ /** - * This file the Moodle question engine. + * JavaScript required by the question preview pop-up. * * @package moodlecore * @subpackage questionengine @@ -24,23 +24,22 @@ */ +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(''); + 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) +} diff --git a/question/previewlib.php b/question/previewlib.php index 4f60b030e9d21..f56f84a831696 100644 --- a/question/previewlib.php +++ b/question/previewlib.php @@ -15,15 +15,177 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . + /** * 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)); +} diff --git a/question/qengine.js b/question/qengine.js index 7c5b745b9c7b4..f8f06637f88a8 100644 --- a/question/qengine.js +++ b/question/qengine.js @@ -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); } diff --git a/question/question.php b/question/question.php index 00414134b6fed..2ab21d2c8f879 100644 --- a/question/question.php +++ b/question/question.php @@ -105,8 +105,7 @@ $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 @@ 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 @@ } // 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 @@ $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 @@ } 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 (($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 @@ } } 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 @@ // 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(); } - diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt index 218c331f9f2d1..d35ff868d2736 100644 --- a/question/todo/diffstat.txt +++ b/question/todo/diffstat.txt @@ -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 ++ diff --git a/question/toggleflag.php b/question/toggleflag.php index 0c49db4cc70ee..24ceef975b346 100644 --- a/question/toggleflag.php +++ b/question/toggleflag.php @@ -1,47 +1,49 @@ . + + /** * 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';