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';