diff --git a/admin/qtypes.php b/admin/qtypes.php
index 553654022da..cb9595a3331 100644
--- a/admin/qtypes.php
+++ b/admin/qtypes.php
@@ -14,12 +14,14 @@
admin_externalpage_setup('manageqtypes');
+ $qtypes = question_bank::get_all_qtypes();
+
/// Get some data we will need - question counts and which types are needed.
$counts = $DB->get_records_sql("
SELECT qtype, COUNT(1) as numquestions, SUM(hidden) as numhidden
FROM {question} GROUP BY qtype", array());
$needed = array();
- foreach ($QTYPES as $qtypename => $qtype) {
+ foreach ($qtypes as $qtypename => $qtype) {
if (!isset($counts[$qtypename])) {
$counts[$qtypename] = new stdClass;
$counts[$qtypename]->numquestions = 0;
@@ -29,13 +31,13 @@
$counts[$qtypename]->numquestions -= $counts[$qtypename]->numhidden;
}
$needed['missingtype'] = true; // The system needs the missing question type.
- foreach ($QTYPES as $qtypename => $qtype) {
+ foreach ($qtypes as $qtypename => $qtype) {
foreach ($qtype->requires_qtypes() as $reqtype) {
$needed[$reqtype] = true;
}
}
foreach ($counts as $qtypename => $count) {
- if (!isset($QTYPES[$qtypename])) {
+ if (!isset($qtypes[$qtypename])) {
$counts['missingtype']->numquestions += $count->numquestions - $count->numhidden;
$counts['missingtype']->numhidden += $count->numhidden;
}
@@ -44,7 +46,7 @@
/// Work of the correct sort order.
$config = get_config('question');
$sortedqtypes = array();
- foreach ($QTYPES as $qtypename => $qtype) {
+ foreach ($qtypes as $qtypename => $qtype) {
$sortedqtypes[$qtypename] = $qtype->local_name();
}
$sortedqtypes = question_sort_qtype_array($sortedqtypes, $config);
@@ -53,7 +55,7 @@
// Disable.
if (($disable = optional_param('disable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
- if (!isset($QTYPES[$disable])) {
+ if (!isset($qtypes[$disable])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $disable);
}
@@ -63,11 +65,11 @@
// Enable.
if (($enable = optional_param('enable', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
- if (!isset($QTYPES[$enable])) {
+ if (!isset($qtypes[$enable])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $enable);
}
- if (!$QTYPES[$enable]->menu_name()) {
+ if (!$qtypes[$enable]->menu_name()) {
print_error('cannotenable', 'question', admin_url('qtypes.php'), $enable);
}
@@ -77,7 +79,7 @@
// Move up in order.
if (($up = optional_param('up', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
- if (!isset($QTYPES[$up])) {
+ if (!isset($qtypes[$up])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $up);
}
@@ -88,7 +90,7 @@
// Move down in order.
if (($down = optional_param('down', '', PARAM_SAFEDIR)) && confirm_sesskey()) {
- if (!isset($QTYPES[$down])) {
+ if (!isset($qtypes[$down])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $down);
}
@@ -104,11 +106,11 @@
print_error('cannotdeletemissingqtype', 'admin', admin_url('qtypes.php'));
}
- if (!isset($QTYPES[$delete])) {
+ if (!isset($qtypes[$delete])) {
print_error('unknownquestiontype', 'question', admin_url('qtypes.php'), $delete);
}
- $qtypename = $QTYPES[$delete]->local_name();
+ $qtypename = $qtypes[$delete]->local_name();
if ($counts[$delete]->numquestions + $counts[$delete]->numhidden > 0) {
print_error('cannotdeleteqtypeinuse', 'admin', admin_url('qtypes.php'), $qtypename);
}
@@ -119,7 +121,7 @@
// If not yet confirmed, display a confirmation message.
if (!optional_param('confirm', '', PARAM_BOOL)) {
- $qtypename = $QTYPES[$delete]->local_name();
+ $qtypename = $qtypes[$delete]->local_name();
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('deleteqtypeareyousure', 'admin', $qtypename));
echo $OUTPUT->confirm(get_string('deleteqtypeareyousuremessage', 'admin', $qtypename),
@@ -141,13 +143,13 @@
unset_config($delete . '_sortorder', 'question');
// Then the tables themselves
- drop_plugin_tables($delete, $QTYPES[$delete]->plugin_dir() . '/db/install.xml', false);
+ drop_plugin_tables($delete, $qtypes[$delete]->plugin_dir() . '/db/install.xml', false);
// Remove event handlers and dequeue pending events
events_uninstall('qtype/' . $delete);
$a->qtype = $qtypename;
- $a->directory = $QTYPES[$delete]->plugin_dir();
+ $a->directory = $qtypes[$delete]->plugin_dir();
echo $OUTPUT->box(get_string('qtypedeletefiles', 'admin', $a), 'generalbox', 'notice');
echo $OUTPUT->continue_button(admin_url('qtypes.php'));
echo $OUTPUT->footer();
@@ -174,7 +176,7 @@
/// Add a row for each question type.
$createabletypes = question_type_menu();
foreach ($sortedqtypes as $qtypename => $localname) {
- $qtype = $QTYPES[$qtypename];
+ $qtype = $qtypes[$qtypename];
$row = array();
// Question icon and name.
@@ -213,7 +215,7 @@
$strtypes = array();
if (!empty($requiredtypes)) {
foreach ($requiredtypes as $required) {
- $strtypes[] = $QTYPES[$required]->local_name();
+ $strtypes[] = $qtypes[$required]->local_name();
}
$row[] = implode(', ', $strtypes);
} else {
diff --git a/lib/questionlib.php b/lib/questionlib.php
index 6ae69e4b78a..ddeb74fb79a 100644
--- a/lib/questionlib.php
+++ b/lib/questionlib.php
@@ -26,47 +26,21 @@
* TODO: separate those functions which form part of the API
* from the helper functions.
*
- * Major Contributors
- * - Alex Smith, Julian Sedding and Gustav Delius {@link http://maths.york.ac.uk/serving_maths}
- *
- * @package core
- * @subpackage question
- * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @package moodlecore
+ * @subpackage questionbank
+ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
+require_once($CFG->dirroot . '/question/engine/lib.php');
+require_once($CFG->dirroot . '/question/type/questiontype.php');
+
+
defined('MOODLE_INTERNAL') || die();
/// CONSTANTS ///////////////////////////////////
-/**#@+
- * The different types of events that can create question states
- */
-define('QUESTION_EVENTOPEN', '0'); // The state was created by Moodle
-define('QUESTION_EVENTNAVIGATE', '1'); // The responses were saved because the student navigated to another page (this is not currently used)
-define('QUESTION_EVENTSAVE', '2'); // The student has requested that the responses should be saved but not submitted or validated
-define('QUESTION_EVENTGRADE', '3'); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle.
-define('QUESTION_EVENTDUPLICATE', '4'); // The responses submitted were the same as previously
-define('QUESTION_EVENTVALIDATE', '5'); // The student has requested a validation. This causes the responses to be saved as well, but not graded.
-define('QUESTION_EVENTCLOSEANDGRADE', '6'); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle.
-define('QUESTION_EVENTSUBMIT', '7'); // The student response has been submitted but it has not yet been marked
-define('QUESTION_EVENTCLOSE', '8'); // The response has been submitted and the session has been closed, either because the student requested it or because Moodle did it (e.g. because of a timelimit). The responses have not been graded.
-define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher
-
-define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','.
- QUESTION_EVENTCLOSEANDGRADE.','.
- QUESTION_EVENTMANUALGRADE);
-
-
-define('QUESTION_EVENTS_CLOSED', QUESTION_EVENTCLOSE.','.
- QUESTION_EVENTCLOSEANDGRADE.','.
- QUESTION_EVENTMANUALGRADE);
-
-define('QUESTION_EVENTS_CLOSED_OR_GRADED', QUESTION_EVENTGRADE.','.
- QUESTION_EVENTS_CLOSED);
-
-/**#@-*/
-
/**#@+
* The core question types.
*/
@@ -87,7 +61,7 @@ define("ESSAY", "essay");
* Constant determines the number of answer boxes supplied in the editing
* form for multiple choice and similar question types.
*/
-define("QUESTION_NUMANS", "10");
+define("QUESTION_NUMANS", 10);
/**
* Constant determines the number of answer boxes supplied in the editing
@@ -106,66 +80,21 @@ define("QUESTION_NUMANS_ADD", 3);
/**
* The options used when popping up a question preview window in Javascript.
*/
-define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes&resizable=yes&width=700&height=540');
-
-/**#@+
- * Option flags for ->optionflags
- * The options are read out via bitwise operation using these constants
- */
-/**
- * Whether the questions is to be run in adaptive mode. If this is not set then
- * a question closes immediately after the first submission of responses. This
- * is how question is Moodle always worked before version 1.5
- */
-define('QUESTION_ADAPTIVE', 1);
-/**#@-*/
-
-/**#@+
- * Options for whether flags are shown/editable when rendering questions.
- */
-define('QUESTION_FLAGSHIDDEN', 0);
-define('QUESTION_FLAGSSHOWN', 1);
-define('QUESTION_FLAGSEDITABLE', 2);
-/**#@-*/
+define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes,resizable=yes,width=800,height=600');
/**
- * GLOBAL VARAIBLES
- * @global array $QTYPES
- * @name $QTYPES
+ * @global array holding question type objects
+ * @deprecated
*/
global $QTYPES;
-/**
- * Array holding question type objects. Initialised via calls to
- * question_register_questiontype as the question type classes are included.
- */
-$QTYPES = array();
-
-/**
- * Add a new question type to the various global arrays above.
- *
- * @global object
- * @param object $qtype An instance of the new question type class.
- */
-function question_register_questiontype($qtype) {
- global $QTYPES;
-
- $name = $qtype->name();
- $QTYPES[$name] = $qtype;
+$QTYPES = question_bank::get_all_qtypes();
+function question_register_questiontype() {
+ // TODO kill this.
}
-
-require_once("$CFG->dirroot/question/type/questiontype.php");
-
-// Load the questiontype.php file for each question type
-// These files in turn call question_register_questiontype()
-// with a new instance of each qtype class.
-$qtypenames = get_plugin_list('qtype');
-foreach($qtypenames as $qtypename => $qdir) {
- // Instanciates all plug-in question types
- $qtypefilepath= "$qdir/questiontype.php";
-
- // echo "Loading $qtypename "; // Uncomment for debugging
- if (is_readable($qtypefilepath)) {
- require_once($qtypefilepath);
+// TODO kill this.
+class default_questiontype {
+ function plugin_dir() {
+ return '';
}
}
@@ -177,17 +106,14 @@ foreach($qtypenames as $qtypename => $qdir) {
* The array returned will only hold the names of all the question types that the user should
* be able to create directly. Some internal question types like random questions are excluded.
*
- * @global object
* @return array an array of question type names translated to the user's language.
*/
function question_type_menu() {
- global $QTYPES;
static $menuoptions = null;
if (is_null($menuoptions)) {
$config = get_config('question');
$menuoptions = array();
- foreach ($QTYPES as $name => $qtype) {
- // Get the name if this qtype is enabled.
+ foreach (question_bank::get_all_qtypes() as $name => $qtype) {
$menuname = $qtype->menu_name();
$enabledvar = $name . '_disabled';
if ($menuname && !isset($config->$enabledvar)) {
@@ -282,93 +208,58 @@ function question_save_qtype_order($neworder, $config = null) {
}
}
-/// OTHER CLASSES /////////////////////////////////////////////////////////
-
-/**
- * This holds the options that are set by the course module
- *
- * @package moodlecore
- * @subpackage question
- * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class cmoptions {
- /**
- * Whether a new attempt should be based on the previous one. If true
- * then a new attempt will start in a state where all responses are set
- * to the last responses from the previous attempt.
- */
- var $attemptonlast = false;
-
- /**
- * Various option flags. The flags are accessed via bitwise operations
- * using the constants defined in the CONSTANTS section above.
- */
- var $optionflags = QUESTION_ADAPTIVE;
-
- /**
- * Determines whether in the calculation of the score for a question
- * penalties for earlier wrong responses within the same attempt will
- * be subtracted.
- */
- var $penaltyscheme = true;
-
- /**
- * The maximum time the user is allowed to answer the questions withing
- * an attempt. This is measured in minutes so needs to be multiplied by
- * 60 before compared to timestamps. If set to 0 no timelimit will be applied
- */
- var $timelimit = 0;
-
- /**
- * Timestamp for the closing time. Responses submitted after this time will
- * be saved but no credit will be given for them.
- */
- var $timeclose = 9999999999;
-
- /**
- * The id of the course from withing which the question is currently being used
- */
- var $course = SITEID;
-
- /**
- * Whether the answers in a multiple choice question should be randomly
- * shuffled when a new attempt is started.
- */
- var $shuffleanswers = true;
-
- /**
- * The number of decimals to be shown when scores are printed
- */
- var $decimalpoints = 2;
-}
-
-
/// FUNCTIONS //////////////////////////////////////////////////////
/**
* Returns an array of names of activity modules that use this question
*
- * @global object
- * @global object
+ * @deprecated since Moodle 2.1. Use {@link questions_in_use} instead.
+
* @param object $questionid
* @return array of strings
*/
function question_list_instances($questionid) {
- global $CFG, $DB;
- $instances = array();
- $modules = $DB->get_records('modules');
- foreach ($modules as $module) {
- $fullmod = $CFG->dirroot . '/mod/' . $module->name;
- if (file_exists($fullmod . '/lib.php')) {
- include_once($fullmod . '/lib.php');
- $fn = $module->name.'_question_list_instances';
+ throw new coding_exception('question_list_instances has been deprectated. Please use questions_in_use instead.');
+}
+
+/**
+ * @param array $questionids of question ids.
+ * @return boolean whether any of these questions are being used by any part of Moodle.
+ */
+function questions_in_use($questionids) {
+ global $CFG;
+
+ if (question_engine::questions_in_use($questionids)) {
+ return true;
+ }
+
+ foreach (get_plugin_list('mod') as $module => $path) {
+ $lib = $path . '/lib.php';
+ if (is_readable($lib)) {
+ include_once($lib);
+
+ $fn = $module . '_questions_in_use';
if (function_exists($fn)) {
- $instances = $instances + $fn($questionid);
+ if ($fn($questionids)) {
+ return true;
+ }
+ } else {
+
+ // Fallback for legacy modules.
+ $fn = $module . '_question_list_instances';
+ if (function_exists($fn)) {
+ foreach ($questionids as $questionid) {
+ $instances = $fn($questionid);
+ if (!empty($instances)) {
+ return true;
+ }
+ }
+ }
}
}
}
- return $instances;
+
+ return false;
}
/**
@@ -376,7 +267,6 @@ function question_list_instances($questionid) {
* question categories contain any questions. This will return true even if all the questions are
* hidden.
*
- * @global object
* @param mixed $context either a context object, or a context id.
* @return boolean whether any of the question categories beloning to this context have
* any questions in them.
@@ -435,12 +325,11 @@ function get_grade_options() {
$gradeoptions = array();
foreach ($grades as $grade) {
$percentage = 100 * $grade;
- $neggrade = -$grade;
- $gradeoptions["$grade"] = "$percentage %";
- $gradeoptionsfull["$grade"] = "$percentage %";
- $gradeoptionsfull["$neggrade"] = -$percentage." %";
+ $gradeoptions["$grade"] = $percentage . '%';
+ $gradeoptionsfull["$grade"] = $percentage . '%';
+ $gradeoptionsfull['' . (-$grade)] = (-$percentage) . '%';
}
- $gradeoptionsfull["0"] = $gradeoptions["0"] = get_string("none");
+ $gradeoptionsfull['0'] = $gradeoptions['0'] = get_string('none');
// sort lists
arsort($gradeoptions, SORT_NUMERIC);
@@ -494,32 +383,40 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
}
/**
- * Tests whether a category is in use by any activity module
- *
- * @global object
- * @return boolean
- * @param integer $categoryid
- * @param boolean $recursive Whether to examine category children recursively
+ * @deprecated Since Moodle 2.1. Use {@link question_category_in_use} instead.
+ * @param integer $categoryid a question category id.
+ * @param boolean $recursive whether to check child categories too.
+ * @return boolean whether any question in this category is in use.
*/
function question_category_isused($categoryid, $recursive = false) {
+ throw new coding_exception('question_category_isused has been deprectated. Please use question_category_in_use instead.');
+}
+
+/**
+ * Tests whether any question in a category is used by any part of Moodle.
+ *
+ * @param integer $categoryid a question category id.
+ * @param boolean $recursive whether to check child categories too.
+ * @return boolean whether any question in this category is in use.
+ */
+function question_category_in_use($categoryid, $recursive = false) {
global $DB;
//Look at each question in the category
- if ($questions = $DB->get_records('question', array('category'=>$categoryid), '', 'id,qtype')) {
- foreach ($questions as $question) {
- if (count(question_list_instances($question->id))) {
- return true;
- }
+ if ($questions = $DB->get_records_menu('question', array('category' => $categoryid), '', 'id,1')) {
+ if (questions_in_use(array_keys($questions))) {
+ return true;
}
}
+ if (!$recursive) {
+ return false;
+ }
//Look under child categories recursively
- if ($recursive) {
- if ($children = $DB->get_records('question_categories', array('parent'=>$categoryid))) {
- foreach ($children as $child) {
- if (question_category_isused($child->id, $recursive)) {
- return true;
- }
+ if ($children = $DB->get_records('question_categories', array('parent' => $categoryid), '', 'id,1')) {
+ foreach ($children as $child) {
+ if (question_category_in_use($child->id, $recursive)) {
+ return true;
}
}
}
@@ -527,43 +424,13 @@ function question_category_isused($categoryid, $recursive = false) {
return false;
}
-/**
- * Deletes all data associated to an attempt from the database
- *
- * @global object
- * @global object
- * @param integer $attemptid The id of the attempt being deleted
- */
-function delete_attempt($attemptid) {
- global $QTYPES, $DB;
-
- $states = $DB->get_records('question_states', array('attempt'=>$attemptid));
- if ($states) {
- $stateslist = implode(',', array_keys($states));
-
- // delete question-type specific data
- foreach ($QTYPES as $qtype) {
- $qtype->delete_states($stateslist);
- }
- }
-
- // delete entries from all other question tables
- // It is important that this is done only after calling the questiontype functions
- $DB->delete_records("question_states", array("attempt"=>$attemptid));
- $DB->delete_records("question_sessions", array("attemptid"=>$attemptid));
- $DB->delete_records("question_attempts", array("id"=>$attemptid));
-}
-
/**
* Deletes question and all associated data from the database
*
* It will not delete a question if it is used by an activity module
- *
- * @global object
- * @global object
* @param object $question The question being deleted
*/
-function delete_question($questionid) {
+function question_delete_question($questionid) {
global $QTYPES, $DB;
$question = $DB->get_record_sql('
@@ -579,30 +446,19 @@ function delete_question($questionid) {
}
// Do not delete a question if it is used by an activity module
- if (count(question_list_instances($questionid))) {
+ if (questions_in_use(array($questionid))) {
return;
}
- // delete questiontype-specific data
+ // Check permissions.
question_require_capability_on($question, 'edit');
- if (isset($QTYPES[$question->qtype])) {
- $QTYPES[$question->qtype]->delete_question($questionid, $question->contextid);
- }
- if ($states = $DB->get_records('question_states', array('question'=>$questionid))) {
- $stateslist = implode(',', array_keys($states));
+ $dm = new question_engine_data_mapper();
+ $dm->delete_previews($questionid);
- // delete questiontype-specific data
- foreach ($QTYPES as $qtype) {
- $qtype->delete_states($stateslist);
- }
- }
-
- // Delete entries from all other question tables
- // It is important that this is done only after calling the questiontype functions
- $DB->delete_records('question_answers', array('question' => $questionid));
- $DB->delete_records('question_states', array('question' => $questionid));
- $DB->delete_records('question_sessions', array('questionid' => $questionid));
+ // delete questiontype-specific data
+ question_bank::get_qtype($question->qtype, false)->delete_question(
+ $questionid, $question->contextid);
// Now recursively delete all child questions
if ($children = $DB->get_records('question', array('parent' => $questionid), '', 'id,qtype')) {
@@ -614,13 +470,12 @@ function delete_question($questionid) {
}
// Finally delete the question record itself
- $DB->delete_records('question', array('id'=>$questionid));
+ $DB->delete_records('question', array('id' => $questionid));
}
/**
* All question categories and their questions are deleted for this course.
*
- * @global object
* @param object $mod an object representing the activity
* @param boolean $feedback to specify if the process must output a summary of its work
* @return boolean
@@ -674,7 +529,6 @@ function question_delete_course($course, $feedback=true) {
* 1/ All question categories and their questions are deleted for this course category.
* 2/ All questions are moved to new category
*
- * @global object
* @param object $category course category object
* @param object $newcategory empty means everything deleted, otherwise id of category where content moved
* @param boolean $feedback to specify if the process must output a summary of its work
@@ -752,7 +606,6 @@ function question_delete_course_category($category, $newcategory, $feedback=true
/**
* Enter description here...
*
- * @global object
* @param string $questionids list of questionids
* @param object $newcontext the context to create the saved category in.
* @param string $oldplace a textual description of the think being deleted, e.g. from get_context_name
@@ -784,7 +637,6 @@ function question_save_from_deletion($questionids, $newcontextid, $oldplace, $ne
/**
* All question categories and their questions are deleted for this activity.
*
- * @global object
* @param object $cm the course module object representing the activity
* @param boolean $feedback to specify if the process must output a summary of its work
* @return boolean
@@ -836,7 +688,6 @@ function question_delete_activity($cm, $feedback=true) {
* acutally moving questions and associated data. However, callers of this function also have to
* do other work, which is why you should not call this method directly from outside the questionbank.
*
- * @global object
* @param string $questionids a comma-separated list of question ids.
* @param integer $newcategoryid the id of the category to move to.
*/
@@ -894,6 +745,28 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte
}
}
+/**
+ * Generate the URL for starting a new preview of a given question with the given options.
+ * @param integer $questionid the question to preview.
+ * @param string $preferredbehaviour the behaviour to use for the preview.
+ * @param float $maxmark the maximum to mark the question out of.
+ * @param question_display_options $displayoptions the display options to use.
+ * @return string the URL.
+ */
+function question_preview_url($questionid, $preferredbehaviour, $maxmark, $displayoptions) {
+ return new moodle_url('/question/preview.php', array(
+ 'id' => $questionid,
+ 'behaviour' => $preferredbehaviour,
+ 'maxmark' => $maxmark,
+ 'correctness' => $displayoptions->correctness,
+ 'marks' => $displayoptions->marks,
+ 'markdp' => $displayoptions->markdp,
+ 'feedback' => (bool) $displayoptions->feedback,
+ 'generalfeedback' => (bool) $displayoptions->generalfeedback,
+ 'rightanswer' => (bool) $displayoptions->rightanswer,
+ 'history' => (bool) $displayoptions->history));
+}
+
/**
* Given a list of ids, load the basic information about a set of questions from the questions table.
* The $join and $extrafields arguments can be used together to pull in extra data.
@@ -912,7 +785,7 @@ function question_move_category_to_context($categoryid, $oldcontextid, $newconte
* on them before they can be properly used.
*/
function question_preload_questions($questionids, $extrafields = '', $join = '', $extraparams = array()) {
- global $CFG, $DB;
+ global $DB;
if (empty($questionids)) {
return array();
}
@@ -929,7 +802,7 @@ function question_preload_questions($questionids, $extrafields = '', $join = '',
// Load the questions
if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) {
- return 'Could not load questions.';
+ return array();
}
foreach ($questions as $question) {
@@ -972,8 +845,6 @@ function question_load_questions($questionids, $extrafields = '', $join = '') {
/**
* Private function to factor common code out of get_question_options().
*
- * @global object
- * @global object
* @param object $question the question to tidy.
* @param boolean $loadtags load the question tags from the tags table. Optional, default false.
* @return boolean true if successful, else false.
@@ -984,7 +855,6 @@ function _tidy_question(&$question, $loadtags = false) {
$question->qtype = 'missingtype';
$question->questiontext = '
' . get_string('warningmissingtype', 'quiz') . '
' . $question->questiontext;
}
- $question->name_prefix = question_make_name_prefix($question->id);
if ($success = $QTYPES[$question->qtype]->get_question_options($question)) {
if (isset($question->_partiallyloaded)) {
unset($question->_partiallyloaded);
@@ -1022,1022 +892,24 @@ function get_question_options(&$questions, $loadtags = false) {
}
}
-/**
- * Load the basic state information for
- *
- * @global object
- * @param integer $attemptid the attempt id to load the states for.
- * @return array an array of state data from the database, you will subsequently
- * need to call question_load_states to get fully loaded states that can be
- * used by the question types. The states here should be sufficient for
- * basic tasks like rendering navigation.
- */
-function question_preload_states($attemptid) {
- global $DB;
- // Note, changes here probably also need to be reflected in
- // regrade_question_in_attempt and question_load_specific_state.
-
- // The questionid field must be listed first so that it is used as the
- // array index in the array returned by $DB->get_records_sql
- $statefields = 'n.questionid as question, s.id, s.attempt, ' .
- 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
- 's.penalty, n.sumpenalty, n.manualcomment, n.manualcommentformat, ' .
- 'n.flagged, n.id as questionsessionid';
-
- // Load the newest states for the questions
- $sql = "SELECT $statefields
- FROM {question_states} s, {question_sessions} n
- WHERE s.id = n.newest AND n.attemptid = ?";
- $states = $DB->get_records_sql($sql, array($attemptid));
- if (!$states) {
- return false;
- }
-
- // Load the newest graded states for the questions
- $sql = "SELECT $statefields
- FROM {question_states} s, {question_sessions} n
- WHERE s.id = n.newgraded AND n.attemptid = ?";
- $gradedstates = $DB->get_records_sql($sql, array($attemptid));
-
- // Hook the two together.
- foreach ($states as $questionid => $state) {
- $states[$questionid]->_partiallyloaded = true;
- if ($gradedstates[$questionid]) {
- $states[$questionid]->last_graded = $gradedstates[$questionid];
- $states[$questionid]->last_graded->_partiallyloaded = true;
- }
- }
-
- return $states;
-}
-
-/**
- * Finish loading the question states that were extracted from the database with
- * question_preload_states, creating new states for any question where there
- * is not a state in the database.
- *
- * @global object
- * @global object
- * @param array $questions the questions to load state for.
- * @param array $states the partially loaded states this array is updated.
- * @param object $cmoptions options from the module we are loading the states for. E.g. $quiz.
- * @param object $attempt The attempt for which the question sessions are
- * to be restored or created.
- * @param mixed either the id of a previous attempt, if this attmpt is
- * building on a previous one, or false for a clean attempt.
- * @return true or false for success or failure.
- */
-function question_load_states(&$questions, &$states, $cmoptions, $attempt, $lastattemptid = false) {
- global $QTYPES, $DB;
-
- // loop through all questions and set the last_graded states
- foreach (array_keys($questions) as $qid) {
- if (isset($states[$qid])) {
- restore_question_state($questions[$qid], $states[$qid]);
- if (isset($states[$qid]->_partiallyloaded)) {
- unset($states[$qid]->_partiallyloaded);
- }
- if (isset($states[$qid]->last_graded)) {
- restore_question_state($questions[$qid], $states[$qid]->last_graded);
- if (isset($states[$qid]->last_graded->_partiallyloaded)) {
- unset($states[$qid]->last_graded->_partiallyloaded);
- }
- } else {
- $states[$qid]->last_graded = clone($states[$qid]);
- }
- } else {
-
- if ($lastattemptid) {
- // If the new attempt is to be based on this previous attempt.
- // Find the responses from the previous attempt and save them to the new session
-
- // Load the last graded state for the question. Note, $statefields is
- // the same as above, except that we don't want n.manualcomment.
- $statefields = 'n.questionid as question, s.id, s.attempt, ' .
- 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
- 's.penalty, n.sumpenalty';
- $sql = "SELECT $statefields
- FROM {question_states} s, {question_sessions} n
- WHERE s.id = n.newest
- AND n.attemptid = ?
- AND n.questionid = ?";
- if (!$laststate = $DB->get_record_sql($sql, array($lastattemptid, $qid))) {
- // Only restore previous responses that have been graded
- continue;
- }
- // Restore the state so that the responses will be restored
- restore_question_state($questions[$qid], $laststate);
- $states[$qid] = clone($laststate);
- unset($states[$qid]->id);
- } else {
- // create a new empty state
- $states[$qid] = new stdClass();
- $states[$qid]->question = $qid;
- $states[$qid]->responses = array('' => '');
- $states[$qid]->raw_grade = 0;
- }
-
- // now fill/overide initial values
- $states[$qid]->attempt = $attempt->uniqueid;
- $states[$qid]->seq_number = 0;
- $states[$qid]->timestamp = $attempt->timestart;
- $states[$qid]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN;
- $states[$qid]->grade = 0;
- $states[$qid]->penalty = 0;
- $states[$qid]->sumpenalty = 0;
- $states[$qid]->manualcomment = '';
- $states[$qid]->manualcommentformat = FORMAT_HTML;
- $states[$qid]->flagged = 0;
-
- // Prevent further changes to the session from incrementing the
- // sequence number
- $states[$qid]->changed = true;
-
- if ($lastattemptid) {
- // prepare the previous responses for new processing
- $action = new stdClass;
- $action->responses = $laststate->responses;
- $action->timestamp = $laststate->timestamp;
- $action->event = QUESTION_EVENTSAVE; //emulate save of questions from all pages MDL-7631
-
- // Process these responses ...
- question_process_responses($questions[$qid], $states[$qid], $action, $cmoptions, $attempt);
-
- // Fix for Bug #5506: When each attempt is built on the last one,
- // preserve the options from any previous attempt.
- if ( isset($laststate->options) ) {
- $states[$qid]->options = $laststate->options;
- }
- } else {
- // Create the empty question type specific information
- if (!$QTYPES[$questions[$qid]->qtype]->create_session_and_responses(
- $questions[$qid], $states[$qid], $cmoptions, $attempt)) {
- return false;
- }
- }
- $states[$qid]->last_graded = clone($states[$qid]);
- }
- }
- return true;
-}
-
-/**
-* Loads the most recent state of each question session from the database
-* or create new one.
-*
-* For each question the most recent session state for the current attempt
-* is loaded from the question_states table and the question type specific data and
-* responses are added by calling {@link restore_question_state()} which in turn
-* calls {@link restore_session_and_responses()} for each question.
-* If no states exist for the question instance an empty state object is
-* created representing the start of a session and empty question
-* type specific information and responses are created by calling
-* {@link create_session_and_responses()}.
-*
-* @return array An array of state objects representing the most recent
-* states of the question sessions.
-* @param array $questions The questions for which sessions are to be restored or
-* created.
-* @param object $cmoptions
-* @param object $attempt The attempt for which the question sessions are
-* to be restored or created.
-* @param mixed either the id of a previous attempt, if this attmpt is
-* building on a previous one, or false for a clean attempt.
-*/
-function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid = false) {
- // Preload the states.
- $states = question_preload_states($attempt->uniqueid);
- if (!$states) {
- $states = array();
- }
-
- // Then finish the job.
- if (!question_load_states($questions, $states, $cmoptions, $attempt, $lastattemptid)) {
- return false;
- }
-
- return $states;
-}
-
-/**
- * Load a particular previous state of a question.
- *
- * @global object
- * @param array $question The question to load the state for.
- * @param object $cmoptions Options from the specifica activity module, e.g. $quiz.
- * @param object $attempt The attempt for which the question sessions are to be loaded.
- * @param integer $stateid The id of a specific state of this question.
- * @return object the requested state. False on error.
- */
-function question_load_specific_state($question, $cmoptions, $attempt, $stateid) {
- global $DB;
- // Load specified states for the question.
- // sess.sumpenalty is probably wrong here shoul really be a sum of penalties from before the one we are asking for.
- $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.manualcommentformat,
- sess.flagged, sess.id as questionsessionid
- FROM {question_states} st, {question_sessions} sess
- WHERE st.id = ?
- AND st.attempt = ?
- AND sess.attemptid = st.attempt
- AND st.question = ?
- AND sess.questionid = st.question';
- $state = $DB->get_record_sql($sql, array($stateid, $attempt->id, $question->id));
- if (!$state) {
- return false;
- }
- restore_question_state($question, $state);
-
- // Load the most recent graded states for the questions before the specified one.
- $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.manualcommentformat,
- sess.flagged, sess.id as questionsessionid
- FROM {question_states} st, {question_sessions} sess
- WHERE st.seq_number <= ?
- AND st.attempt = ?
- AND sess.attemptid = st.attempt
- AND st.question = ?
- AND sess.questionid = st.question
- AND st.event IN ('.QUESTION_EVENTS_GRADED.') '.
- 'ORDER BY st.seq_number DESC';
- $gradedstates = $DB->get_records_sql($sql, array($state->seq_number, $attempt->id, $question->id), 0, 1);
- if (empty($gradedstates)) {
- $state->last_graded = clone($state);
- } else {
- $gradedstate = reset($gradedstates);
- restore_question_state($question, $gradedstate);
- $state->last_graded = $gradedstate;
- }
- return $state;
-}
-
-/**
-* Creates the run-time fields for the states
-*
-* Extends the state objects for a question by calling
-* {@link restore_session_and_responses()}
- *
- * @global object
-* @param object $question The question for which the state is needed
-* @param object $state The state as loaded from the database
-* @return boolean Represents success or failure
-*/
-function restore_question_state(&$question, &$state) {
- global $QTYPES;
-
- // initialise response to the value in the answer field
- $state->responses = array('' => $state->answer);
-
- // Set the changed field to false; any code which changes the
- // question session must set this to true and must increment
- // ->seq_number. The save_question_session
- // function will save the new state object to the database if the field is
- // set to true.
- $state->changed = false;
-
- // Load the question type specific data
- return $QTYPES[$question->qtype]->restore_session_and_responses($question, $state);
-
-}
-
-/**
-* Saves the current state of the question session to the database
-*
-* The state object representing the current state of the session for the
-* question is saved to the question_states table with ->responses[''] saved
-* to the answer field of the database table. The information in the
-* question_sessions table is updated.
-* The question type specific data is then saved.
- *
- * @global array
- * @global object
-* @return mixed The id of the saved or updated state or false
-* @param object $question The question for which session is to be saved.
-* @param object $state The state information to be saved. In particular the
-* most recent responses are in ->responses. The object
-* is updated to hold the new ->id.
-*/
-function save_question_session($question, $state) {
- global $QTYPES, $DB;
-
- // Check if the state has changed
- if (!$state->changed && isset($state->id)) {
- if (isset($state->newflaggedstate) && $state->flagged != $state->newflaggedstate) {
- // If this fails, don't worry too much, it is not critical data.
- question_update_flag($state->questionsessionid, $state->newflaggedstate);
- }
- return $state->id;
- }
- // Set the legacy answer field
- $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
-
- // Save the state
- if (!empty($state->update)) { // this forces the old state record to be overwritten
- $DB->update_record('question_states', $state);
- } else {
- $state->id = $DB->insert_record('question_states', $state);
- }
-
- // create or update the session
- if (!$session = $DB->get_record('question_sessions', array('attemptid' => $state->attempt, 'questionid' => $question->id))) {
- $session = new stdClass;
- $session->attemptid = $state->attempt;
- $session->questionid = $question->id;
- $session->newest = $state->id;
- // The following may seem weird, but the newgraded field needs to be set
- // already even if there is no graded state yet.
- $session->newgraded = $state->id;
- $session->sumpenalty = $state->sumpenalty;
- $session->manualcomment = $state->manualcomment;
- $session->manualcommentformat = $state->manualcommentformat;
- $session->flagged = !empty($state->newflaggedstate);
- $DB->insert_record('question_sessions', $session);
- } else {
- $session->newest = $state->id;
- if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) {
- // this state is graded or newly opened, so it goes into the lastgraded field as well
- $session->newgraded = $state->id;
- $session->sumpenalty = $state->sumpenalty;
- $session->manualcomment = $state->manualcomment;
- $session->manualcommentformat = $state->manualcommentformat;
- }
- $session->flagged = !empty($state->newflaggedstate);
- $DB->update_record('question_sessions', $session);
- }
-
- unset($state->answer);
-
- // Save the question type specific state information and responses
- if (!$QTYPES[$question->qtype]->save_session_and_responses($question, $state)) {
- return false;
- }
-
- // Reset the changed flag
- $state->changed = false;
- return $state->id;
-}
-
-/**
-* Determines whether a state has been graded by looking at the event field
-*
-* @return boolean true if the state has been graded
-* @param object $state
-*/
-function question_state_is_graded($state) {
- static $question_events_graded = array();
- if (!$question_events_graded){
- $question_events_graded = explode(',', QUESTION_EVENTS_GRADED);
- }
- return (in_array($state->event, $question_events_graded));
-}
-
-/**
-* Determines whether a state has been closed by looking at the event field
-*
-* @return boolean true if the state has been closed
-* @param object $state
-*/
-function question_state_is_closed($state) {
- static $question_events_closed = array();
- if (!$question_events_closed){
- $question_events_closed = explode(',', QUESTION_EVENTS_CLOSED);
- }
- return (in_array($state->event, $question_events_closed));
-}
-
-
-/**
- * Extracts responses from submitted form
- *
- * This can extract the responses given to one or several questions present on a page
- * It returns an array with one entry for each question, indexed by question id
- * Each entry is an object with the properties
- * ->event The event that has triggered the submission. This is determined by which button
- * the user has pressed.
- * ->responses An array holding the responses to an individual question, indexed by the
- * name of the corresponding form element.
- * ->timestamp A unix timestamp
- * @return array array of action objects, indexed by question ids.
- * @param array $questions an array containing at least all questions that are used on the form
- * @param array $formdata the data submitted by the form on the question page
- * @param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted
- */
-function question_extract_responses($questions, $formdata, $defaultevent=QUESTION_EVENTSAVE) {
-
- $time = time();
- $actions = array();
- foreach ($formdata as $key => $response) {
- // Get the question id from the response name
- if (false !== ($quid = question_get_id_from_name_prefix($key))) {
- // check if this is a valid id
- if (!isset($questions[$quid])) {
- print_error('formquestionnotinids', 'question');
- }
-
- // Remove the name prefix from the name
- //decrypt trying
- $key = substr($key, strlen($questions[$quid]->name_prefix));
- if (false === $key) {
- $key = '';
- }
- // Check for question validate and mark buttons & set events
- if ($key === 'validate') {
- $actions[$quid]->event = QUESTION_EVENTVALIDATE;
- } else if ($key === 'submit') {
- $actions[$quid]->event = QUESTION_EVENTSUBMIT;
- } else {
- $actions[$quid]->event = $defaultevent;
- }
- // Update the state with the new response
- $actions[$quid]->responses[$key] = $response;
-
- // Set the timestamp
- $actions[$quid]->timestamp = $time;
- }
- }
- foreach ($actions as $quid => $notused) {
- ksort($actions[$quid]->responses);
- }
- return $actions;
-}
-
-
-/**
- * Returns the html for question feedback image.
- *
- * @global object
- * @param float $fraction value representing the correctness of the user's
- * response to a question.
- * @param boolean $selected whether or not the answer is the one that the
- * user picked.
- * @return string
- */
-function question_get_feedback_image($fraction, $selected=true) {
- global $CFG, $OUTPUT;
- static $icons = array('correct' => 'tick_green', 'partiallycorrect' => 'tick_amber',
- 'incorrect' => 'cross_red');
-
- if ($selected) {
- $size = 'big';
- } else {
- $size = 'small';
- }
- $class = question_get_feedback_class($fraction);
- return '';
-}
-
-/**
- * Returns the class name for question feedback.
- * @param float $fraction value representing the correctness of the user's
- * response to a question.
- * @return string
- */
-function question_get_feedback_class($fraction) {
- if ($fraction >= 1/1.01) {
- return 'correct';
- } else if ($fraction > 0.0) {
- return 'partiallycorrect';
- } else {
- return 'incorrect';
- }
-}
-
-
-/**
-* For a given question in an attempt we walk the complete history of states
-* and recalculate the grades as we go along.
-*
-* This is used when a question is changed and old student
-* responses need to be marked with the new version of a question.
-*
-* @todo Make sure this is not quiz-specific
-*
- * @global object
-* @return boolean Indicates whether the grade has changed
-* @param object $question A question object
-* @param object $attempt The attempt, in which the question needs to be regraded.
-* @param object $cmoptions
-* @param boolean $verbose Optional. Whether to print progress information or not.
-* @param boolean $dryrun Optional. Whether to make changes to grades records
-* or record that changes need to be made for a later regrade.
-*/
-function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false, $dryrun=false) {
- global $DB, $OUTPUT;
-
- // load all states for this question in this attempt, ordered in sequence
- if ($states = $DB->get_records('question_states',
- array('attempt'=>$attempt->uniqueid, 'question'=>$question->id),
- 'seq_number ASC')) {
- $states = array_values($states);
-
- // Subtract the grade for the latest state from $attempt->sumgrades to get the
- // sumgrades for the attempt without this question.
- $attempt->sumgrades -= $states[count($states)-1]->grade;
-
- // Initialise the replaystate
- $replaystate = question_load_specific_state($question, $cmoptions, $attempt, $states[0]->id);
- $replaystate->sumpenalty = 0;
- $replaystate->last_graded->sumpenalty = 0;
-
- $changed = false;
- for($j = 1; $j < count($states); $j++) {
- restore_question_state($question, $states[$j]);
- $action = new stdClass;
- $action->responses = $states[$j]->responses;
- $action->timestamp = $states[$j]->timestamp;
-
- // Change event to submit so that it will be reprocessed
- if (in_array($states[$j]->event, array(QUESTION_EVENTCLOSE,
- QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE))) {
- $action->event = QUESTION_EVENTSUBMIT;
-
- // By default take the event that was saved in the database
- } else {
- $action->event = $states[$j]->event;
- }
-
- if ($action->event == QUESTION_EVENTMANUALGRADE) {
- // Ensure that the grade is in range - in the past this was not checked,
- // but now it is (MDL-14835) - so we need to ensure the data is valid before
- // proceeding.
- if ($states[$j]->grade < 0) {
- $states[$j]->grade = 0;
- $changed = true;
- } else if ($states[$j]->grade > $question->maxgrade) {
- $states[$j]->grade = $question->maxgrade;
- $changed = true;
-
- }
- if (!$dryrun){
- $error = question_process_comment($question, $replaystate, $attempt,
- $replaystate->manualcomment, $replaystate->manualcommentformat, $states[$j]->grade);
- if (is_string($error)) {
- echo $OUTPUT->notification($error);
- }
- } else {
- $replaystate->grade = $states[$j]->grade;
- }
- } else {
- // Reprocess (regrade) responses
- if (!question_process_responses($question, $replaystate,
- $action, $cmoptions, $attempt) && $verbose) {
- $a = new stdClass;
- $a->qid = $question->id;
- $a->stateid = $states[$j]->id;
- echo $OUTPUT->notification(get_string('errorduringregrade', 'question', $a));
- }
- // We need rounding here because grades in the DB get truncated
- // e.g. 0.33333 != 0.3333333, but we want them to be equal here
- if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5))
- or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5))
- or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) {
- $changed = true;
- }
- // If this was previously a closed state, and it has been knoced back to
- // graded, then fix up the state again.
- if ($replaystate->event == QUESTION_EVENTGRADE &&
- ($states[$j]->event == QUESTION_EVENTCLOSE ||
- $states[$j]->event == QUESTION_EVENTCLOSEANDGRADE)) {
- $replaystate->event = $states[$j]->event;
- }
- }
-
- $replaystate->id = $states[$j]->id;
- $replaystate->changed = true;
- $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created
- if (!$dryrun){
- save_question_session($question, $replaystate);
- }
- }
- if ($changed) {
- if (!$dryrun){
- // TODO, call a method in quiz to do this, where 'quiz' comes from
- // the question_attempts table.
- $DB->update_record('quiz_attempts', $attempt);
- }
- }
- if ($changed){
- $toinsert = new stdClass();
- $toinsert->oldgrade = round((float)$states[count($states)-1]->grade, 5);
- $toinsert->newgrade = round((float)$replaystate->grade, 5);
- $toinsert->attemptid = $attempt->uniqueid;
- $toinsert->questionid = $question->id;
- //the grade saved is the old grade if the new grade is saved
- //it is the new grade if this is a dry run.
- $toinsert->regraded = $dryrun?0:1;
- $toinsert->timemodified = time();
- $DB->insert_record('quiz_question_regrade', $toinsert);
- return true;
- } else {
- return false;
- }
- }
- return false;
-}
-
-/**
-* Processes an array of student responses, grading and saving them as appropriate
-*
- * @global array
-* @param object $question Full question object, passed by reference
-* @param object $state Full state object, passed by reference
-* @param object $action object with the fields ->responses which
-* is an array holding the student responses,
-* ->action which specifies the action, e.g., QUESTION_EVENTGRADE,
-* and ->timestamp which is a timestamp from when the responses
-* were submitted by the student.
-* @param object $cmoptions
-* @param object $attempt The attempt is passed by reference so that
-* during grading its ->sumgrades field can be updated
-* @return boolean Indicates success/failure
-*/
-function question_process_responses($question, &$state, $action, $cmoptions, &$attempt) {
- global $QTYPES;
-
- // if no responses are set initialise to empty response
- if (!isset($action->responses)) {
- $action->responses = array('' => '');
- }
-
- $state->newflaggedstate = !empty($action->responses['_flagged']);
-
- // make sure these are gone!
- unset($action->responses['submit'], $action->responses['validate'], $action->responses['_flagged']);
-
- // Check the question session is still open
- if (question_state_is_closed($state)) {
- return true;
- }
-
- // If $action->event is not set that implies saving
- if (! isset($action->event)) {
- debugging('Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER);
- $action->event = QUESTION_EVENTSAVE;
- }
- // If submitted then compare against last graded
- // responses, not last given responses in this case
- if (question_isgradingevent($action->event)) {
- $state->responses = $state->last_graded->responses;
- }
-
- // Check for unchanged responses (exactly unchanged, not equivalent).
- // We also have to catch questions that the student has not yet attempted
- $sameresponses = $QTYPES[$question->qtype]->compare_responses($question, $action, $state);
- if (!empty($state->last_graded) && $state->last_graded->event == QUESTION_EVENTOPEN &&
- question_isgradingevent($action->event)) {
- $sameresponses = false;
- }
-
- // If the response has not been changed then we do not have to process it again
- // unless the attempt is closing or validation is requested
- if ($sameresponses and QUESTION_EVENTCLOSE != $action->event
- and QUESTION_EVENTVALIDATE != $action->event) {
- return true;
- }
-
- // Roll back grading information to last graded state and set the new
- // responses
- $newstate = clone($state->last_graded);
- $newstate->responses = $action->responses;
- $newstate->seq_number = $state->seq_number + 1;
- $newstate->changed = true; // will assure that it gets saved to the database
- $newstate->last_graded = clone($state->last_graded);
- $newstate->timestamp = $action->timestamp;
- $newstate->newflaggedstate = $state->newflaggedstate;
- $newstate->flagged = $state->flagged;
- $newstate->questionsessionid = $state->questionsessionid;
- $state = $newstate;
-
- // Set the event to the action we will perform. The question type specific
- // grading code may override this by setting it to QUESTION_EVENTCLOSE if the
- // attempt at the question causes the session to close
- $state->event = $action->event;
-
- if (!question_isgradingevent($action->event)) {
- // Grade the response but don't update the overall grade
- if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
- return false;
- }
-
- // Temporary hack because question types are not given enough control over what is going
- // on. Used by Opaque questions.
- // TODO fix this code properly.
- if (!empty($state->believeevent)) {
- // If the state was graded we need to ...
- if (question_state_is_graded($state)) {
- question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
-
- // update the attempt grade
- $attempt->sumgrades -= (float)$state->last_graded->grade;
- $attempt->sumgrades += (float)$state->grade;
-
- // and update the last_graded field.
- unset($state->last_graded);
- $state->last_graded = clone($state);
- unset($state->last_graded->changed);
- }
- } else {
- // Don't allow the processing to change the event type
- $state->event = $action->event;
- }
-
- } else { // grading event
-
- // Unless the attempt is closing, we want to work out if the current responses
- // (or equivalent responses) were already given in the last graded attempt.
- if(QUESTION_EVENTCLOSE != $action->event && QUESTION_EVENTOPEN != $state->last_graded->event &&
- $QTYPES[$question->qtype]->compare_responses($question, $state, $state->last_graded)) {
- $state->event = QUESTION_EVENTDUPLICATE;
- }
-
- // If we did not find a duplicate or if the attempt is closing, perform grading
- if ((!$sameresponses and QUESTION_EVENTDUPLICATE != $state->event) or
- QUESTION_EVENTCLOSE == $action->event) {
- if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
- return false;
- }
-
- // Calculate overall grade using correct penalty method
- question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
- }
-
- // If the state was graded we need to ...
- if (question_state_is_graded($state)) {
- // update the attempt grade
- $attempt->sumgrades -= (float)$state->last_graded->grade;
- $attempt->sumgrades += (float)$state->grade;
-
- // and update the last_graded field.
- unset($state->last_graded);
- $state->last_graded = clone($state);
- unset($state->last_graded->changed);
- }
- }
- $attempt->timemodified = $action->timestamp;
-
- return true;
-}
-
-/**
-* Determine if event requires grading
-*/
-function question_isgradingevent($event) {
- return (QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event);
-}
-
-/**
-* Applies the penalty from the previous graded responses to the raw grade
-* for the current responses
-*
-* The grade for the question in the current state is computed by subtracting the
-* penalty accumulated over the previous graded responses at the question from the
-* raw grade. If the timestamp is more than 1 minute beyond the end of the attempt
-* the grade is set to zero. The ->grade field of the state object is modified to
-* reflect the new grade but is never allowed to decrease.
-* @param object $question The question for which the penalty is to be applied.
-* @param object $state The state for which the grade is to be set from the
-* raw grade and the cumulative penalty from the last
-* graded state. The ->grade field is updated by applying
-* the penalty scheme determined in $cmoptions to the ->raw_grade and
-* ->last_graded->penalty fields.
-* @param object $cmoptions The options set by the course module.
-* The ->penaltyscheme field determines whether penalties
-* for incorrect earlier responses are subtracted.
-*/
-function question_apply_penalty_and_timelimit(&$question, &$state, $attempt, $cmoptions) {
- // TODO. Quiz dependancy. The fact that the attempt that is passed in here
- // is from quiz_attempts, and we use things like $cmoptions->timelimit.
-
- // deal with penalty
- if ($cmoptions->penaltyscheme) {
- $state->grade = $state->raw_grade - $state->sumpenalty;
- $state->sumpenalty += (float) $state->penalty;
- } else {
- $state->grade = $state->raw_grade;
- }
-
- // deal with timelimit
- if ($cmoptions->timelimit) {
- // We allow for 5% uncertainty in the following test
- if ($state->timestamp - $attempt->timestart > $cmoptions->timelimit * 1.05) {
- $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
- if (!has_capability('mod/quiz:ignoretimelimits', get_context_instance(CONTEXT_MODULE, $cm->id),
- $attempt->userid, false)) {
- $state->grade = 0;
- }
- }
- }
-
- // deal with closing time
- if ($cmoptions->timeclose and $state->timestamp > ($cmoptions->timeclose + 60) // allowing 1 minute lateness
- and !$attempt->preview) { // ignore closing time for previews
- $state->grade = 0;
- }
-
- // Ensure that the grade does not go down
- $state->grade = max($state->grade, $state->last_graded->grade);
-}
-
/**
* Print the icon for the question type
*
- * @global array
- * @global object
-* @param object $question The question object for which the icon is required
-* only $question->qtype is used.
-* @param boolean $return If true the functions returns the link as a string
+* @param object $question The question object for which the icon is required.
+* Only $question->qtype is used.
+* @return string the HTML for the img tag.
*/
-function print_question_icon($question, $return = false) {
- global $QTYPES, $CFG, $OUTPUT;
+function print_question_icon($question) {
+ global $OUTPUT;
- if (array_key_exists($question->qtype, $QTYPES)) {
- $namestr = $QTYPES[$question->qtype]->local_name();
- } else {
- $namestr = 'missingtype';
- }
- $html = 'pix_url('icon', $qtype->plugin_name()) . '" alt="' .
$namestr . '" title="' . $namestr . '" />';
- if ($return) {
- return $html;
- } else {
- echo $html;
- }
-}
-/**
- * @param $question
- * @param $state
- * @param $prefix
- * @param $cmoptions
- * @param $caption
- */
-function question_print_comment_fields($question, $state, $prefix, $cmoptions, $caption = '') {
- global $QTYPES;
- $idprefix = preg_replace('/[^-_a-zA-Z0-9]/', '', $prefix);
- $otherquestionsinuse = '';
- if (!empty($cmoptions->questions)) {
- $otherquestionsinuse = $cmoptions->questions;
- }
- if (!question_state_is_graded($state) && $QTYPES[$question->qtype]->is_question_manual_graded($question, $otherquestionsinuse)) {
- $grade = '';
- } else {
- $grade = question_format_grade($cmoptions, $state->last_graded->grade);
- }
- $maxgrade = question_format_grade($cmoptions, $question->maxgrade);
- $fieldsize = strlen($maxgrade) - 1;
- if (empty($caption)) {
- $caption = format_string($question->name);
- }
- ?>
-
- $question->maxgrade) {
- $a = new stdClass;
- $a->grade = $grade;
- $a->maxgrade = $question->maxgrade;
- $a->name = $question->name;
- return get_string('errormanualgradeoutofrange', 'question', $a);
- }
-
- // Update the comment and save it in the database
- $comment = trim($comment);
- $state->manualcomment = $comment;
- $state->manualcommentformat = $commentformat;
- $state->newflaggedstate = $state->flagged;
- $DB->set_field('question_sessions', 'manualcomment', $comment, array('attemptid'=>$attempt->uniqueid, 'questionid'=>$question->id));
-
- // Update the attempt if the score has changed.
- if ($grade !== '' && (abs($state->last_graded->grade - $grade) > 0.002 || $state->last_graded->event != QUESTION_EVENTMANUALGRADE)) {
- $attempt->sumgrades = $attempt->sumgrades - $state->last_graded->grade + $grade;
- $attempt->timemodified = time();
- $DB->update_record('quiz_attempts', $attempt);
-
- // We want to update existing state (rather than creating new one) if it
- // was itself created by a manual grading event.
- $state->update = $state->event == QUESTION_EVENTMANUALGRADE;
-
- // Update the other parts of the state object.
- $state->raw_grade = $grade;
- $state->grade = $grade;
- $state->penalty = 0;
- $state->timestamp = time();
- $state->seq_number++;
- $state->event = QUESTION_EVENTMANUALGRADE;
-
- // Update the last graded state (don't simplify!)
- unset($state->last_graded);
- $state->last_graded = clone($state);
-
- // We need to indicate that the state has changed in order for it to be saved.
- $state->changed = 1;
- }
-
- return true;
-}
-
-/**
-* Construct name prefixes for question form element names
-*
-* Construct the name prefix that should be used for example in the
-* names of form elements created by questions.
-* This is called by {@link get_question_options()}
-* to set $question->name_prefix.
-* This name prefix includes the question id which can be
-* extracted from it with {@link question_get_id_from_name_prefix()}.
-*
-* @return string
-* @param integer $id The question id
-*/
-function question_make_name_prefix($id) {
- return 'resp' . $id . '_';
-}
-
-/**
- * Extract question id from the prefix of form element names
- *
- * @return integer The question id
- * @param string $name The name that contains a prefix that was
- * constructed with {@link question_make_name_prefix()}
- */
-function question_get_id_from_name_prefix($name) {
- if (!preg_match('/^resp([0-9]+)_/', $name, $matches)) {
- return false;
- }
- return (integer) $matches[1];
-}
-
-/**
- * Extract question id from the prefix of form element names
- *
- * @return integer The question id
- * @param string $name The name that contains a prefix that was
- * constructed with {@link question_make_name_prefix()}
- */
-function question_id_and_key_from_post_name($name) {
- if (!preg_match('/^resp([0-9]+)_(.*)$/', $name, $matches)) {
- return array(false, false);
- }
- return array((integer) $matches[1], $matches[2]);
-}
-
-/**
- * Returns the unique id for a new attempt
- *
- * Every module can keep their own attempts table with their own sequential ids but
- * the question code needs to also have a unique id by which to identify all these
- * attempts. Hence a module, when creating a new attempt, calls this function and
- * stores the return value in the 'uniqueid' field of its attempts table.
- *
- * @global object
- */
-function question_new_attempt_uniqueid($modulename='quiz') {
- global $DB;
-
- $attempt = new stdClass;
- $attempt->modulename = $modulename;
- $id = $DB->insert_record('question_attempts', $attempt);
- return $id;
+ return $html;
}
/**
@@ -2053,124 +925,22 @@ function question_hash($question) {
return make_unique_id_code();
}
-/**
- * Round a grade to to the correct number of decimal places, and format it for display.
- * If $cmoptions->questiondecimalpoints is set, that is used, otherwise
- * else if $cmoptions->decimalpoints is used,
- * otherwise a default of 2 is used, but this should not be relied upon, and generated a developer debug warning.
- * However, if $cmoptions->questiondecimalpoints is -1, the means use $cmoptions->decimalpoints.
- *
- * @param object $cmoptions The modules settings.
- * @param float $grade The grade to round.
- */
-function question_format_grade($cmoptions, $grade) {
- if (isset($cmoptions->questiondecimalpoints) && $cmoptions->questiondecimalpoints != -1) {
- $decimalplaces = $cmoptions->questiondecimalpoints;
- } else if (isset($cmoptions->decimalpoints)) {
- $decimalplaces = $cmoptions->decimalpoints;
- } else {
- $decimalplaces = 2;
- debugging('Code that leads to question_format_grade being called should set ' .
- '$cmoptions->questiondecimalpoints or $cmoptions->decimalpoints', DEBUG_DEVELOPER);
- }
- return format_float($grade, $decimalplaces);
-}
-
-/**
- * @return string An inline script that creates a JavaScript object storing
- * various strings and bits of configuration that the scripts in qengine.js need
- * to get from PHP.
- */
-function question_init_qengine_js() {
- global $CFG, $PAGE, $OUTPUT;
- static $done = false;
- if ($done) {
- return;
- }
- $module = array(
- 'name' => 'core_question_flags',
- 'fullpath' => '/question/flags.js',
- 'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
- );
- $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
- $flagattributes = array(
- 0 => array(
- 'src' => $OUTPUT->pix_url('i/unflagged') . '',
- 'title' => get_string('clicktoflag', 'question'),
- 'alt' => get_string('notflagged', 'question'),
- ),
- 1 => array(
- 'src' => $OUTPUT->pix_url('i/flagged') . '',
- 'title' => get_string('clicktounflag', 'question'),
- 'alt' => get_string('flagged', 'question'),
- ),
- );
- $PAGE->requires->js_init_call('M.core_question_flags.init',
- array($actionurl, $flagattributes), false, $module);
- $done = true;
-}
-
/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
/**
- * Give the questions in $questionlist a chance to request the CSS or JavaScript
- * they need, before the header is printed.
- *
- * If your code is going to call the print_question function, it must call this
- * funciton before print_header.
- *
- * @param array $questionlist a list of questionids of the questions what will appear on this page.
- * @param array $questions an array of question objects, whose keys are question ids.
- * Must contain all the questions in $questionlist
- * @param array $states an array of question state objects, whose keys are question ids.
- * Must contain the state of all the questions in $questionlist
- */
-function question_get_html_head_contributions($questionlist, &$questions, &$states) {
- global $CFG, $PAGE, $QTYPES;
-
- // The question engine's own JavaScript.
- question_init_qengine_js();
-
- // Anything that questions on this page need.
- foreach ($questionlist as $questionid) {
- $question = $questions[$questionid];
- $QTYPES[$question->qtype]->get_html_head_contributions($question, $states[$questionid]);
- }
-}
-
-/**
- * Like {@link get_html_head_contributions()} but for the editing page
- * question/question.php.
+ * Get anything that needs to be included in the head of the question editing page
+ * for a particular question type. This function is called by question/question.php.
*
* @param $question A question object. Only $question->qtype is used.
* @return string Deprecated. Some HTML code that can go inside the head tag.
*/
function question_get_editing_head_contributions($question) {
- global $QTYPES;
- $QTYPES[$question->qtype]->get_editing_head_contributions();
+ question_bank::get_qtype($question->qtype, false)->get_editing_head_contributions();
}
-/**
- * Prints a question
- *
- * Simply calls the question type specific print_question() method.
- *
- * @global array
- * @param object $question The question to be rendered.
- * @param object $state The state to render the question in.
- * @param integer $number The number for this question.
- * @param object $cmoptions The options specified by the course module
- * @param object $options An object specifying the rendering options.
- */
-function print_question(&$question, &$state, $number, $cmoptions, $options=null, $context=null) {
- global $QTYPES;
- $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options, $context);
-}
/**
* Saves question options
*
* Simply calls the question type specific save_question_options() method.
- *
- * @global array
*/
function save_question_options($question) {
global $QTYPES;
@@ -2178,66 +948,12 @@ function save_question_options($question) {
$QTYPES[$question->qtype]->save_question_options($question);
}
-/**
-* Gets all teacher stored answers for a given question
-*
-* Simply calls the question type specific get_all_responses() method.
- *
- * @global array
-*/
-// ULPGC ecastro
-function get_question_responses($question, $state) {
- global $QTYPES;
- $r = $QTYPES[$question->qtype]->get_all_responses($question, $state);
- return $r;
-}
-
-/**
-* Gets the response given by the user in a particular state
-*
-* Simply calls the question type specific get_actual_response() method.
- *
- * @global array
-*/
-// ULPGC ecastro
-function get_question_actual_response($question, $state) {
- global $QTYPES;
-
- $r = $QTYPES[$question->qtype]->get_actual_response($question, $state);
- return $r;
-}
-
-/**
-* TODO: document this
- *
- * @global array
-*/
-// ULPGc ecastro
-function get_question_fraction_grade($question, $state) {
- global $QTYPES;
-
- $r = $QTYPES[$question->qtype]->get_fractional_grade($question, $state);
- return $r;
-}
-/**
- * @global array
-* @return integer grade out of 1 that a random guess by a student might score.
-*/
-// ULPGc ecastro
-function question_get_random_guess_score($question) {
- global $QTYPES;
-
- $r = $QTYPES[$question->qtype]->get_random_guess_score($question);
- return $r;
-}
/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
/**
* returns the categories with their names ordered following parent-child relationships
* finally it tries to return pending categories (those being orphaned, whose parent is
* incorrect) to avoid missing any category from original array.
- *
- * @global object
*/
function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
global $DB;
@@ -2364,7 +1080,6 @@ function question_category_select_menu($contexts, $top = false, $currentcat = 0,
}
/**
- * @global object
* @param integer $contextid a context id.
* @return object the default question category for that context, or false if none.
*/
@@ -2378,42 +1093,10 @@ function question_get_default_category($contextid) {
}
}
-/**
- * @global object
- * @global object
- * @param object $context a context
- * @return string A URL for editing questions in this context.
- */
-function question_edit_url($context) {
- global $CFG, $SITE;
- if (!has_any_capability(question_get_question_capabilities(), $context)) {
- return false;
- }
- $baseurl = $CFG->wwwroot . '/question/edit.php?';
- $defaultcategory = question_get_default_category($context->id);
- if ($defaultcategory) {
- $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&';
- }
- switch ($context->contextlevel) {
- case CONTEXT_SYSTEM:
- return $baseurl . 'courseid=' . $SITE->id;
- case CONTEXT_COURSECAT:
- // This is nasty, becuase we can only edit questions in a course
- // context at the moment, so for now we just return false.
- return false;
- case CONTEXT_COURSE:
- return $baseurl . 'courseid=' . $context->instanceid;
- case CONTEXT_MODULE:
- return $baseurl . 'cmid=' . $context->instanceid;
- }
-
-}
-
/**
* Gets the default category in the most specific context.
* If no categories exist yet then default ones are created in all contexts.
*
- * @global object
* @param array $contexts The context objects for this context and all parent contexts.
* @return object The default category - the category in the course context
*/
@@ -2461,7 +1144,6 @@ function question_make_default_categories($contexts) {
* Get all the category objects, including a count of the number of questions in that category,
* for all the categories in the lists $contexts.
*
- * @global object
* @param mixed $contexts either a single contextid, or a comma-separated list of context ids.
* @param string $sortorder used as the ORDER BY clause in the select statement.
* @return array of category objects.
@@ -2478,7 +1160,6 @@ function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder,
/**
* Output an array of question categories.
- * @global object
*/
function question_category_options($contexts, $top = false, $currentcat = 0, $popupform = false, $nochildrenof = -1) {
global $CFG;
@@ -2536,6 +1217,7 @@ function question_add_context_in_key($categories){
}
return $newcatarray;
}
+
function question_add_tops($categories, $pcontexts){
$topcats = array();
foreach ($pcontexts as $context){
@@ -2552,14 +1234,13 @@ function question_add_tops($categories, $pcontexts){
/**
* Returns a comma separated list of ids of the category and all subcategories
- * @global object
*/
function question_categorylist($categoryid) {
global $DB;
// returns a comma separated list of ids of the category and all subcategories
$categorylist = $categoryid;
- if ($subcategories = $DB->get_records('question_categories', array('parent'=>$categoryid), 'sortorder ASC', 'id, 1')) {
+ if ($subcategories = $DB->get_records('question_categories', array('parent' => $categoryid), 'sortorder ASC', 'id, 1')) {
foreach ($subcategories as $subcategory) {
$categorylist .= ','. question_categorylist($subcategory->id);
}
@@ -2567,52 +1248,40 @@ function question_categorylist($categoryid) {
return $categorylist;
}
-
-
-
//===========================
// Import/Export Functions
//===========================
/**
* Get list of available import or export formats
- *
- * @global object
* @param string $type 'import' if import list, otherwise export list assumed
* @return array sorted list of import/export formats available
*/
-function get_import_export_formats( $type ) {
+function get_import_export_formats($type) {
global $CFG;
- $fileformats = get_plugin_list("qformat");
+ $fileformats = get_plugin_list('qformat');
$fileformatname=array();
require_once( "{$CFG->dirroot}/question/format.php" );
- foreach ($fileformats as $fileformat=>$fdir) {
- $format_file = "$fdir/format.php";
- if (file_exists($format_file) ) {
- require_once($format_file);
- }
- else {
+ foreach ($fileformats as $fileformat => $fdir) {
+ $formatfile = $fdir . '/format.php';
+ if (is_readable($formatfile)) {
+ include_once($formatfile);
+ } else {
continue;
}
+
$classname = "qformat_$fileformat";
- $format_class = new $classname();
- if ($type=='import') {
+ $formatclass = new $classname();
+ if ($type == 'import') {
$provided = $format_class->provide_import();
- }
- else {
+ } else {
$provided = $format_class->provide_export();
}
+
if ($provided) {
- $formatname = get_string($fileformat, 'quiz');
- if ($formatname == "[[$fileformat]]") {
- $formatname = get_string($fileformat, 'qformat_'.$fileformat);
- if ($formatname == "[[$fileformat]]") {
- $formatname = $fileformat; // Just use the raw folder name
- }
- }
- $fileformatnames[$fileformat] = $formatname;
+ $fileformatnames[$fileformat] = get_string($fileformat, 'qformat_'.$fileformat);
}
}
natcasesort($fileformatnames);
@@ -2650,8 +1319,9 @@ function question_default_export_filename($course, $category) {
}
/**
- * @package moodlecore
- * @subpackage question
+ * Converts contextlevels to strings and back to help with reading/writing contexts
+ * to/from import/export files.
+ *
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -2659,26 +1329,26 @@ class context_to_string_translator{
/**
* @var array used to translate between contextids and strings for this context.
*/
- var $contexttostringarray = array();
+ protected $contexttostringarray = array();
- function context_to_string_translator($contexts){
+ public function __construct($contexts) {
$this->generate_context_to_string_array($contexts);
}
- function context_to_string($contextid){
+ public function context_to_string($contextid) {
return $this->contexttostringarray[$contextid];
}
- function string_to_context($contextname){
+ public function string_to_context($contextname) {
$contextid = array_search($contextname, $this->contexttostringarray);
return $contextid;
}
- function generate_context_to_string_array($contexts){
+ protected function generate_context_to_string_array($contexts) {
if (!$this->contexttostringarray){
$catno = 1;
foreach ($contexts as $context){
- switch ($context->contextlevel){
+ switch ($context->contextlevel){
case CONTEXT_MODULE :
$contextstring = 'module';
break;
@@ -2700,38 +1370,9 @@ class context_to_string_translator{
}
-/**
- * @return array all the capabilities that relate to accessing particular questions.
- */
-function question_get_question_capabilities() {
- return array(
- 'moodle/question:add',
- 'moodle/question:editmine',
- 'moodle/question:editall',
- 'moodle/question:viewmine',
- 'moodle/question:viewall',
- 'moodle/question:usemine',
- 'moodle/question:useall',
- 'moodle/question:movemine',
- 'moodle/question:moveall',
- );
-}
-
-/**
- * @return array all the question bank capabilities.
- */
-function question_get_all_capabilities() {
- $caps = question_get_question_capabilities();
- $caps[] = 'moodle/question:managecategory';
- $caps[] = 'moodle/question:flag';
- return $caps;
-}
-
/**
* Check capability on category
*
- * @global object
- * @global object
* @param mixed $question object or id
* @param string $cap 'add', 'edit', 'view', 'use', 'move'
* @param integer $cachecat useful to cache all question records in a category
@@ -2740,11 +1381,6 @@ function question_get_all_capabilities() {
function question_has_capability_on($question, $cap, $cachecat = -1){
global $USER, $DB;
- // nicolasconnault@gmail.com In some cases I get $question === false. Since no such object exists, it can't be deleted, we can safely return true
- if ($question === false) {
- return true;
- }
-
// these are capabilities on existing questions capabilties are
//set per category. Each of these has a mine and all version. Append 'mine' and 'all'
$question_questioncaps = array('edit', 'view', 'use', 'move');
@@ -2819,62 +1455,32 @@ function question_get_real_state($state) {
}
/**
- * Update the flagged state of a particular question session.
- *
- * @global object
- * @param integer $sessionid question_session id.
- * @param boolean $newstate the new state for the flag.
- * @return boolean success or failure.
+ * @param object $context a context
+ * @return string A URL for editing questions in this context.
*/
-function question_update_flag($sessionid, $newstate) {
- global $DB;
- return $DB->set_field('question_sessions', 'flagged', $newstate, array('id' => $sessionid));
-}
+function question_edit_url($context) {
+ global $CFG, $SITE;
+ if (!has_any_capability(question_get_question_capabilities(), $context)) {
+ return false;
+ }
+ $baseurl = $CFG->wwwroot . '/question/edit.php?';
+ $defaultcategory = question_get_default_category($context->id);
+ if ($defaultcategory) {
+ $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&';
+ }
+ switch ($context->contextlevel) {
+ case CONTEXT_SYSTEM:
+ return $baseurl . 'courseid=' . $SITE->id;
+ case CONTEXT_COURSECAT:
+ // This is nasty, becuase we can only edit questions in a course
+ // context at the moment, so for now we just return false.
+ return false;
+ case CONTEXT_COURSE:
+ return $baseurl . 'courseid=' . $context->instanceid;
+ case CONTEXT_MODULE:
+ return $baseurl . 'cmid=' . $context->instanceid;
+ }
-/**
- * Update the flagged state of all the questions in an attempt, where a new .
- *
- * @global object
- * @param integer $sessionid question_session id.
- * @param boolean $newstate the new state for the flag.
- * @return boolean success or failure.
- */
-function question_save_flags($formdata, $attemptid, $questionids) {
- global $DB;
- $donequestionids = array();
- foreach ($formdata as $postvariable => $value) {
- list($qid, $key) = question_id_and_key_from_post_name($postvariable);
- if ($qid !== false && in_array($qid, $questionids)) {
- if ($key == '_flagged') {
- $DB->set_field('question_sessions', 'flagged', !empty($value),
- array('attemptid' => $attemptid, 'questionid' => $qid));
- $donequestionids[$qid] = 1;
- }
- }
- }
- foreach ($questionids as $qid) {
- if (!isset($donequestionids[$qid])) {
- $DB->set_field('question_sessions', 'flagged', 0,
- array('attemptid' => $attemptid, 'questionid' => $qid));
- }
- }
-}
-
-/**
- *
- * @global object
- * @param integer $attemptid the question_attempt id.
- * @param integer $questionid the question id.
- * @param integer $sessionid the question_session id.
- * @param object $user a user, or null to use $USER.
- * @return string that needs to be sent to question/toggleflag.php for it to work.
- */
-function question_get_toggleflag_checksum($attemptid, $questionid, $sessionid, $user = null) {
- if (is_null($user)) {
- global $USER;
- $user = $USER;
- }
- return md5($attemptid . "_" . $user->secret . "_" . $questionid . "_" . $sessionid);
}
/**
@@ -2914,6 +1520,33 @@ function question_extend_settings_navigation(navigation_node $navigationnode, $c
return $questionnode;
}
+/**
+ * @return array all the capabilities that relate to accessing particular questions.
+ */
+function question_get_question_capabilities() {
+ return array(
+ 'moodle/question:add',
+ 'moodle/question:editmine',
+ 'moodle/question:editall',
+ 'moodle/question:viewmine',
+ 'moodle/question:viewall',
+ 'moodle/question:usemine',
+ 'moodle/question:useall',
+ 'moodle/question:movemine',
+ 'moodle/question:moveall',
+ );
+}
+
+/**
+ * @return array all the question bank capabilities.
+ */
+function question_get_all_capabilities() {
+ $caps = question_get_question_capabilities();
+ $caps[] = 'moodle/question:managecategory';
+ $caps[] = 'moodle/question:flag';
+ return $caps;
+}
+
class question_edit_contexts {
public static $CAPS = array(
@@ -2942,7 +1575,7 @@ class question_edit_contexts {
/**
* @param current context
*/
- public function question_edit_contexts($thiscontext){
+ public function question_edit_contexts($thiscontext) {
$pcontextids = get_parent_contexts($thiscontext);
$contexts = array($thiscontext);
foreach ($pcontextids as $pcontextid){
@@ -2953,20 +1586,20 @@ class question_edit_contexts {
/**
* @return array all parent contexts
*/
- public function all(){
+ public function all() {
return $this->allcontexts;
}
/**
* @return object lowest context which must be either the module or course context
*/
- public function lowest(){
+ public function lowest() {
return $this->allcontexts[0];
}
/**
* @param string $cap capability
* @return array parent contexts having capability, zero based index
*/
- public function having_cap($cap){
+ public function having_cap($cap) {
$contextswithcap = array();
foreach ($this->allcontexts as $context){
if (has_capability($cap, $context)){
@@ -2979,7 +1612,7 @@ class question_edit_contexts {
* @param array $caps capabilities
* @return array parent contexts having at least one of $caps, zero based index
*/
- public function having_one_cap($caps){
+ public function having_one_cap($caps) {
$contextswithacap = array();
foreach ($this->allcontexts as $context){
foreach ($caps as $cap){
@@ -2995,7 +1628,7 @@ class question_edit_contexts {
* @param string $tabname edit tab name
* @return array parent contexts having at least one of $caps, zero based index
*/
- public function having_one_edit_tab_cap($tabname){
+ public function having_one_edit_tab_cap($tabname) {
return $this->having_one_cap(self::$CAPS[$tabname]);
}
/**
@@ -3004,7 +1637,7 @@ class question_edit_contexts {
* @param string $cap capability
* @return boolean
*/
- public function have_cap($cap){
+ public function have_cap($cap) {
return (count($this->having_cap($cap)));
}
@@ -3014,7 +1647,7 @@ class question_edit_contexts {
* @param array $caps capability
* @return boolean
*/
- public function have_one_cap($caps){
+ public function have_one_cap($caps) {
foreach ($caps as $cap) {
if ($this->have_cap($cap)) {
return true;
@@ -3022,6 +1655,7 @@ class question_edit_contexts {
}
return false;
}
+
/**
* Has at least one parent context got one of the caps for actions on $tabname
*
@@ -3031,6 +1665,7 @@ class question_edit_contexts {
public function have_one_edit_tab_cap($tabname){
return $this->have_one_cap(self::$CAPS[$tabname]);
}
+
/**
* Throw error if at least one parent context hasn't got the cap $cap
*
@@ -3041,6 +1676,7 @@ class question_edit_contexts {
print_error('nopermissions', '', '', $cap);
}
}
+
/**
* Throw error if at least one parent context hasn't got one of the caps $caps
*
diff --git a/question/engine/bank.php b/question/engine/bank.php
index b4a8c0b9b41..8fed6afb2ba 100644
--- a/question/engine/bank.php
+++ b/question/engine/bank.php
@@ -63,8 +63,9 @@ abstract class question_bank {
if (isset(self::$questiontypes[$qtypename])) {
return self::$questiontypes[$qtypename];
}
- $file = $CFG->dirroot . '/question/type/' . $qtypename . '/questiontype.php';
+ $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
if (!is_readable($file)) {
+ echo 'problem';
if ($mustexist || $qtypename == 'missingtype') {
throw new Exception('Unknown question type ' . $qtypename);
} else {
@@ -73,6 +74,9 @@ abstract class question_bank {
}
include_once($file);
$class = 'qtype_' . $qtypename;
+ if (!class_exists($class)) {
+ throw new coding_exception("Class $class must be defined in $file");
+ }
self::$questiontypes[$qtypename] = new $class();
return self::$questiontypes[$qtypename];
}
@@ -82,7 +86,7 @@ abstract class question_bank {
* @return boolean whether users are allowed to create questions of this type.
*/
public static function qtype_enabled($qtypename) {
- ;
+ return true; // TODO
}
/**
@@ -98,9 +102,12 @@ abstract class question_bank {
*/
public static function get_all_qtypes() {
$qtypes = array();
- $plugins = get_list_of_plugins('question/type', 'datasetdependent');
- foreach ($plugins as $plugin) {
- $qtypes[$plugin] = self::get_qtype($plugin);
+ foreach (get_plugin_list('qtype') as $plugin => $notused) {
+ try {
+ $qtypes[$plugin] = self::get_qtype($plugin);
+ } catch (Exception $e) {
+ // TODO ingore, but reivew this later.
+ }
}
return $qtypes;
}
diff --git a/question/engine/lib.php b/question/engine/lib.php
index 7c556e46ba4..e7d49b88ed8 100644
--- a/question/engine/lib.php
+++ b/question/engine/lib.php
@@ -551,21 +551,32 @@ abstract class question_flags {
}
public static function initialise_js() {
- global $CFG;
-
- require_js(array('yui_yahoo','yui_dom','yui_event','yui_connection'));
- require_js($CFG->wwwroot . '/question/qengine.js');
-
- $config = array(
- 'actionurl' => $CFG->wwwroot . '/question/toggleflag.php',
- 'flagicon' => $CFG->pixpath . '/i/flagged.png',
- 'unflagicon' => $CFG->pixpath . '/i/unflagged.png',
- 'flagtooltip' => get_string('clicktoflag', 'question'),
- 'unflagtooltip' => get_string('clicktounflag', 'question'),
- 'flaggedalt' => get_string('flagged', 'question'),
- 'unflaggedalt' => get_string('notflagged', 'question'),
+ global $CFG, $PAGE, $OUTPUT;
+ static $done = false;
+ if ($done) {
+ return;
+ }
+ $module = array(
+ 'name' => 'core_question_flags',
+ 'fullpath' => '/question/flags.js',
+ 'requires' => array('base', 'dom', 'event-delegate', 'io-base'),
);
- return print_js_config($config, 'qengine_config', true);
+ $actionurl = $CFG->wwwroot . '/question/toggleflag.php';
+ $flagattributes = array(
+ 0 => array(
+ 'src' => $OUTPUT->pix_url('i/unflagged') . '',
+ 'title' => get_string('clicktoflag', 'question'),
+ 'alt' => get_string('notflagged', 'question'),
+ ),
+ 1 => array(
+ 'src' => $OUTPUT->pix_url('i/flagged') . '',
+ 'title' => get_string('clicktounflag', 'question'),
+ 'alt' => get_string('flagged', 'question'),
+ ),
+ );
+ $PAGE->requires->js_init_call('M.core_question_flags.init',
+ array($actionurl, $flagattributes), false, $module);
+ $done = true;
}
}
diff --git a/question/engine/renderer.php b/question/engine/renderer.php
index 81d58fdea6b..71dd3ce0a2c 100644
--- a/question/engine/renderer.php
+++ b/question/engine/renderer.php
@@ -213,7 +213,7 @@ class core_question_renderer extends renderer_base {
'' .
'' .
'' . "\n" .
+ $qa->is_flagged(), $id . 'img') . '' . "\n";
break;
default:
$flagcontent = '';
diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt
index d35ff868d27..b5ab9b08b9b 100644
--- a/question/todo/diffstat.txt
+++ b/question/todo/diffstat.txt
@@ -52,7 +52,7 @@ Internal changes
lang/en_utf8/quiz_regrade.php | 7 -
lang/en_utf8/quiz_responses.php | 11 -
- lib/questionlib.php | 1434 ++--------
+DONE lib/questionlib.php | 1434 ++--------
mod/quiz/accessrules.php | 828 ++++++
mod/quiz/attempt.php | 742 ++----
@@ -158,7 +158,7 @@ DONE question/comment.html | 25 -
DONE question/editlib.php | 36 +-
GONE question/exportfile.php | 52 +-
DONE question/file.php | 171 +- | but this file is probably obsolete.
-!!!TODO question/import_form.php | 19 + | the change is to add validation that a file has been uploaded.
+ question/import_form.php | 19 + | the change is to add validation that a file has been uploaded.
DONE question/move_form.php | 32 +-
DONE question/preview.js | 47 +
question/preview.php | 408 ++--
@@ -168,6 +168,14 @@ DONE question/question.php | 3 +-
question/restorelib.php | 88 +-
DONE question/toggleflag.php | 49 +
+ question/type/edit_question_form.php | 264 ++-
+ question/type/question.html | 46 -
+ question/type/questionbase.php | 787 ++++++
+DONE question/type/questiontype.php | 1302 ++-------
+ question/type/rendererbase.php | 265 ++ -- TODO diff questointype to get necessary changes.
+ question/type/simpletest/testquestionbase.php | 117 +
+DONE question/type/simpletest/testquestiontype.php | 91 +-
+
question/behaviour/behaviourbase.php | 627 +++++
question/behaviour/rendererbase.php | 200 ++
@@ -423,14 +431,6 @@ DONE question/toggleflag.php | 49 +
question/type/truefalse/simpletest/testquestiontype.php | 73 +
question/type/truefalse/version.php | 4 +-
- question/type/edit_question_form.php | 264 ++-
- question/type/question.html | 46 -
- question/type/questionbase.php | 787 ++++++
- question/type/questiontype.php | 1302 ++-------
- question/type/rendererbase.php | 265 ++
- question/type/simpletest/testquestionbase.php | 117 +
- question/type/simpletest/testquestiontype.php | 91 +-
-
theme/standard/styles_color.css | 72 +-
theme/standard/styles_fonts.css | 20 +-
theme/standard/styles_layout.css | 265 ++-
diff --git a/question/todo/questionlib_2.0.diff.txt b/question/todo/questionlib_2.0.diff.txt
new file mode 100644
index 00000000000..b889602310a
--- /dev/null
+++ b/question/todo/questionlib_2.0.diff.txt
@@ -0,0 +1,807 @@
+
+ /**
+ * Prints a question
+ *
+ * Simply calls the question type specific print_question() method.
++ *
++ * @global array
+ * @param object $question The question to be rendered.
+ * @param object $state The state to render the question in.
+ * @param integer $number The number for this question.
+ * @param object $cmoptions The options specified by the course module
+ * @param object $options An object specifying the rendering options.
+ */
+-function print_question(&$question, &$state, $number, $cmoptions, $options=null) {
++function print_question(&$question, &$state, $number, $cmoptions, $options=null, $context=null) {
+ global $QTYPES;
+- $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options);
++ $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options, $context);
+ }
+ /**
+ * Saves question options
+ *
+ * Simply calls the question type specific save_question_options() method.
++ *
++ * @global array
+ */
+ function save_question_options($question) {
+ global $QTYPES;
+@@ -2075,8 +2255,9 @@ function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
+ //If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
+ if ($level == 1) {
+ foreach ($keys as $key) {
+- //If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
+- if (!isset($categories[$key]->processed) && !$DB->record_exists('question_categories', array('course'=>$categories[$key]->course, 'id'=>$categories[$key]->parent))) {
++ // If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
++ if (!isset($categories[$key]->processed) && !$DB->record_exists(
++ 'question_categories', array('contextid'=>$categories[$key]->contextid, 'id'=>$categories[$key]->parent))) {
+ $children[$key] = $categories[$key];
+ $categories[$key]->processed = true;
+ $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
+@@ -2167,16 +2348,23 @@ function add_indented_names($categories, $nochildrenof = -1) {
+ * @param integer $selected optionally, the id of a category to be selected by default in the dropdown.
+ */
+ function question_category_select_menu($contexts, $top = false, $currentcat = 0, $selected = "", $nochildrenof = -1) {
++ global $OUTPUT;
+ $categoriesarray = question_category_options($contexts, $top, $currentcat, false, $nochildrenof);
+ if ($selected) {
+- $nothing = '';
++ $choose = '';
+ } else {
+- $nothing = 'choose';
++ $choose = 'choosedots';
++ }
++ $options = array();
++ foreach($categoriesarray as $group=>$opts) {
++ $options[] = array($group=>$opts);
+ }
+- choose_from_menu_nested($categoriesarray, 'category', $selected, $nothing);
++
++ echo html_writer::select($options, 'category', $selected, $choose);
+ }
+@@ -2216,23 +2406,31 @@ function question_edit_url($context) {
+ /**
+ * Gets the default category in the most specific context.
+ * If no categories exist yet then default ones are created in all contexts.
+ *
++ * @global object
+ * @param array $contexts The context objects for this context and all parent contexts.
+ * @return object The default category - the category in the course context
+ */
+ function question_make_default_categories($contexts) {
+ global $DB;
++ static $preferredlevels = array(
++ CONTEXT_COURSE => 4,
++ CONTEXT_MODULE => 3,
++ CONTEXT_COURSECAT => 2,
++ CONTEXT_SYSTEM => 1,
++ );
+
+ $toreturn = null;
++ $preferredness = 0;
+ // If it already exists, just return it.
+ foreach ($contexts as $key => $context) {
+- if (!$exists = $DB->record_exists("question_categories", array('contextid'=>$context->id))){
++ if (!$exists = $DB->record_exists("question_categories", array('contextid'=>$context->id))) {
+ // Otherwise, we need to make one
+ $category = new stdClass;
+ $contextname = print_context_name($context, false, true);
+@@ -2242,19 +2440,20 @@ function question_make_default_categories($contexts) {
+ $category->parent = 0;
+ $category->sortorder = 999; // By default, all categories get this number, and are sorted alphabetically.
+ $category->stamp = make_unique_id_code();
+- if (!$category->id = $DB->insert_record('question_categories', $category)) {
+- print_error('cannotcreatedefaultcat', '', '', print_context_name($context));
+- }
++ $category->id = $DB->insert_record('question_categories', $category);
+ } else {
+ $category = question_get_default_category($context->id);
+ }
+-
+- if ($context->contextlevel == CONTEXT_COURSE){
+- $toreturn = clone($category);
++ if ($preferredlevels[$context->contextlevel] > $preferredness &&
++ has_any_capability(array('moodle/question:usemine', 'moodle/question:useall'), $context)) {
++ $toreturn = $category;
++ $preferredness = $preferredlevels[$context->contextlevel];
+ }
+ }
+
+-
++ if (!is_null($toreturn)) {
++ $toreturn = clone($toreturn);
++ }
+ return $toreturn;
+ }
+
+@@ -2313,9 +2514,12 @@ function question_category_options($contexts, $top = false, $currentcat = 0, $po
+ if ($popupform){
+ $popupcats = array();
+ foreach ($categoriesarray as $contextstring => $optgroup){
+- $popupcats[] = '--'.$contextstring;
+- $popupcats = array_merge($popupcats, $optgroup);
+- $popupcats[] = '--';
++ $group = array();
++ foreach ($optgroup as $key=>$value) {
++ $key = str_replace($CFG->wwwroot, '', $key);
++ $group[$key] = $value;
++ }
++ $popupcats[] = array($contextstring=>$group);
+ }
+ return $popupcats;
+ } else {
+@@ -2335,7 +2539,7 @@ function question_add_context_in_key($categories){
+ function question_add_tops($categories, $pcontexts){
+ $topcats = array();
+ foreach ($pcontexts as $context){
+- $newcat = new object();
++ $newcat = new stdClass();
+ $newcat->id = "0,$context";
+ $newcat->name = get_string('top');
+ $newcat->parent = -1;
+ function get_import_export_formats( $type ) {
+
+ global $CFG;
+- $fileformats = get_list_of_plugins("question/format");
++ $fileformats = get_plugin_list("qformat");
+
+ $fileformatname=array();
+ require_once( "{$CFG->dirroot}/question/format.php" );
+- foreach ($fileformats as $key => $fileformat) {
+- $format_file = $CFG->dirroot . "/question/format/$fileformat/format.php";
+- if (file_exists( $format_file ) ) {
+- require_once( $format_file );
++ foreach ($fileformats as $fileformat=>$fdir) {
++ $format_file = "$fdir/format.php";
++ if (file_exists($format_file) ) {
++ require_once($format_file);
+ }
+ else {
+ continue;
+@@ -2400,7 +2607,10 @@ function get_import_export_formats( $type ) {
+ if ($provided) {
+ $formatname = get_string($fileformat, 'quiz');
+ if ($formatname == "[[$fileformat]]") {
+- $formatname = $fileformat; // Just use the raw folder name
++ $formatname = get_string($fileformat, 'qformat_'.$fileformat);
++ if ($formatname == "[[$fileformat]]") {
++ $formatname = $fileformat; // Just use the raw folder name
++ }
+ }
+ $fileformatnames[$fileformat] = $formatname;
+ }
+@@ -2412,50 +2622,39 @@ function get_import_export_formats( $type ) {
+
+
+ /**
+-* Create default export filename
+-*
+-* @return string default export filename
+-* @param object $course
+-* @param object $category
++* Create a reasonable default file name for exporting questions from a particular
++* category.
++* @param object $course the course the questions are in.
++* @param object $category the question category.
++* @return string the filename.
+ */
+-function default_export_filename($course,$category) {
+- //Take off some characters in the filename !!
+- $takeoff = array(" ", ":", "/", "\\", "|");
+- $export_word = str_replace($takeoff,"_",moodle_strtolower(get_string("exportfilename","quiz")));
+- //If non-translated, use "export"
+- if (substr($export_word,0,1) == "[") {
+- $export_word= "export";
+- }
+-
+- //Calculate the date format string
+- $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz"));
+- //If non-translated, use "%Y%m%d-%H%M"
+- if (substr($export_date_format,0,1) == "[") {
+- $export_date_format = "%%Y%%m%%d-%%H%%M";
+- }
+-
+- //Calculate the shortname
+- $export_shortname = clean_filename($course->shortname);
+- if (empty($export_shortname) or $export_shortname == '_' ) {
+- $export_shortname = $course->id;
+- }
+-
+- //Calculate the category name
+- $export_categoryname = clean_filename($category->name);
+-
+- //Calculate the final export filename
+- //The export word
+- $export_name = $export_word."-";
+- //The shortname
+- $export_name .= moodle_strtolower($export_shortname)."-";
+- //The category name
+- $export_name .= moodle_strtolower($export_categoryname)."-";
+- //The date format
+- $export_name .= userdate(time(),$export_date_format,99,false);
+- //Extension is supplied by format later.
++function question_default_export_filename($course, $category) {
++ // We build a string that is an appropriate name (questions) from the lang pack,
++ // then the corse shortname, then the question category name, then a timestamp.
++
++ $base = clean_filename(get_string('exportfilename', 'question'));
++
++ $dateformat = str_replace(' ', '_', get_string('exportnameformat', 'question'));
++ $timestamp = clean_filename(userdate(time(), $dateformat, 99, false));
++
++ $shortname = clean_filename($course->shortname);
++ if ($shortname == '' || $shortname == '_' ) {
++ $shortname = $course->id;
++ }
++
++ $categoryname = clean_filename(format_string($category->name));
++
++ return "{$base}-{$shortname}-{$categoryname}-{$timestamp}";
+
+ return $export_name;
+ }
++
++/**
++ * @package moodlecore
++ * @subpackage question
++ * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
++ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
++ */
+ class context_to_string_translator{
+ /**
+ * @var array used to translate between contextids and strings for this context.
+@@ -2549,13 +2751,13 @@ function question_has_capability_on($question, $cap, $cachecat = -1){
+ static $questions = array();
+ static $categories = array();
+ static $cachedcat = array();
+- if ($cachecat != -1 && (array_search($cachecat, $cachedcat)===FALSE)){
+- $questions += $DB->get_records('question', array('category'=>$cachecat));
++ if ($cachecat != -1 && array_search($cachecat, $cachedcat) === false) {
++ $questions += $DB->get_records('question', array('category' => $cachecat));
+ $cachedcat[] = $cachecat;
+ }
+ if (!is_object($question)){
+ if (!isset($questions[$question])){
+- if (!$questions[$question] = $DB->get_record('question', array('id'=>$question), 'id,category,createdby')) {
++ if (!$questions[$question] = $DB->get_record('question', array('id' => $question), 'id,category,createdby')) {
+ print_error('questiondoesnotexist', 'question');
+ }
+ }
+@@ -2567,11 +2769,12 @@ function question_has_capability_on($question, $cap, $cachecat = -1){
+ }
+ }
+ $category = $categories[$question->category];
++ $context = get_context_instance_by_id($category->contextid);
+
+ if (array_search($cap, $question_questioncaps)!== FALSE){
+- if (!has_capability('moodle/question:'.$cap.'all', get_context_instance_by_id($category->contextid))){
++ if (!has_capability('moodle/question:'.$cap.'all', $context)){
+ if ($question->createdby == $USER->id){
+- return has_capability('moodle/question:'.$cap.'mine', get_context_instance_by_id($category->contextid));
++ return has_capability('moodle/question:'.$cap.'mine', $context);
+ } else {
+ return false;
+ }
+@@ -2579,7 +2782,7 @@ function question_has_capability_on($question, $cap, $cachecat = -1){
+ return true;
+ }
+ } else {
+- return has_capability('moodle/question:'.$cap, get_context_instance_by_id($category->contextid));
++ return has_capability('moodle/question:'.$cap, $context);
+ }
+
+ }
+@@ -2594,107 +2797,6 @@ function question_require_capability_on($question, $cap){
+ return true;
+ }
+
+-function question_file_links_base_url($courseid){
+- global $CFG;
+- $baseurl = preg_quote("$CFG->wwwroot/file.php", '!');
+- $baseurl .= '('.preg_quote('?file=', '!').')?';//may or may not
+- //be using slasharguments, accept either
+- $baseurl .= "/$courseid/";//course directory
+- return $baseurl;
+-}
+-
+-/*
+- * Find all course / site files linked to in a piece of html.
+- * @param string html the html to search
+- * @param int course search for files for courseid course or set to siteid for
+- * finding site files.
+- * @return array files with keys being files.
+- */
+-function question_find_file_links_from_html($html, $courseid){
+- global $CFG;
+- $baseurl = question_file_links_base_url($courseid);
+- $searchfor = '!'.
+- '(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$baseurl.'([^"]*)"'.
+- '|'.
+- '(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$baseurl.'([^\']*)\''.
+- '!i';
+- $matches = array();
+- $no = preg_match_all($searchfor, $html, $matches);
+- if ($no){
+- $rawurls = array_filter(array_merge($matches[5], $matches[10]));//array_filter removes empty elements
+- //remove any links that point somewhere they shouldn't
+- foreach (array_keys($rawurls) as $rawurlkey){
+- if (!$cleanedurl = question_url_check($rawurls[$rawurlkey])){
+- unset($rawurls[$rawurlkey]);
+- } else {
+- $rawurls[$rawurlkey] = $cleanedurl;
+- }
+-
+- }
+- $urls = array_flip($rawurls);// array_flip removes duplicate files
+- // and when we merge arrays will continue to automatically remove duplicates
+- } else {
+- $urls = array();
+- }
+- return $urls;
+-}
+-
+-/**
+- * Check that url doesn't point anywhere it shouldn't
+- *
+- * @param $url string relative url within course files directory
+- * @return mixed boolean false if not OK or cleaned URL as string if OK
+- */
+-function question_url_check($url){
+- global $CFG;
+- if ((substr(strtolower($url), 0, strlen($CFG->moddata)) == strtolower($CFG->moddata)) ||
+- (substr(strtolower($url), 0, 10) == 'backupdata')){
+- return false;
+- } else {
+- return clean_param($url, PARAM_PATH);
+- }
+-}
+-
+-/**
+- * Find all course / site files linked to in a piece of html.
+- * @param string html the html to search
+- * @param int course search for files for courseid course or set to siteid for
+- * finding site files.
+- * @return array files with keys being files.
+- */
+-function question_replace_file_links_in_html($html, $fromcourseid, $tocourseid, $url, $destination, &$changed){
+- global $CFG;
+- require_once($CFG->libdir .'/filelib.php');
+- $tourl = get_file_url("$tocourseid/$destination");
+- $fromurl = question_file_links_base_url($fromcourseid).preg_quote($url, '!');
+- $searchfor = array('!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$fromurl.'(")!i',
+- '!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$fromurl.'(\')!i');
+- $newhtml = preg_replace($searchfor, '\\1'.$tourl.'\\5', $html);
+- if ($newhtml != $html){
+- $changed = true;
+- }
+- return $newhtml;
+-}
+-
+-function get_filesdir_from_context($context){
+- global $DB;
+-
+- switch ($context->contextlevel){
+- case CONTEXT_COURSE :
+- $courseid = $context->instanceid;
+- break;
+- case CONTEXT_MODULE :
+- $courseid = $DB->get_field('course_modules', 'course', array('id'=>$context->instanceid));
+- break;
+- case CONTEXT_COURSECAT :
+- case CONTEXT_SYSTEM :
+- $courseid = SITEID;
+- break;
+- default :
+- print_error('invalidcontext');
+- }
+- return $courseid;
+-}
+ /**
+ * Get the real state - the correct question id and answer - for a random
+ * question.
+@@ -2702,11 +2804,12 @@ function get_filesdir_from_context($context){
+ * @return mixed return integer real question id or false if there was an
+ * error..
+ */
+-function question_get_real_state($state){
++function question_get_real_state($state) {
++ global $OUTPUT;
+ $realstate = clone($state);
+ $matches = array();
+ if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)){
+- notify(get_string('errorrandom', 'quiz_statistics'));
++ echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics'));
+ return false;
+ } else {
+ $realstate->question = $matches[1];
+@@ -2770,4 +2877,389 @@ function question_get_toggleflag_checksum($attemptid, $questionid, $sessionid, $
+ return md5($attemptid . "_" . $user->secret . "_" . $questionid . "_" . $sessionid);
+ }
+
+-?>
++/**
++ * Adds question bank setting links to the given navigation node if caps are met.
++ *
++ * @param navigation_node $navigationnode The navigation node to add the question branch to
++ * @param stdClass $context
++ * @return navigation_node Returns the question branch that was added
++ */
++function question_extend_settings_navigation(navigation_node $navigationnode, $context) {
++ global $PAGE;
++
++ if ($context->contextlevel == CONTEXT_COURSE) {
++ $params = array('courseid'=>$context->instanceid);
++ } else if ($context->contextlevel == CONTEXT_MODULE) {
++ $params = array('cmid'=>$context->instanceid);
++ } else {
++ return;
++ }
++
++ $questionnode = $navigationnode->add(get_string('questionbank','question'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER);
++
++ $contexts = new question_edit_contexts($context);
++ if ($contexts->have_one_edit_tab_cap('questions')) {
++ $questionnode->add(get_string('questions', 'quiz'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_SETTING);
++ }
++ if ($contexts->have_one_edit_tab_cap('categories')) {
++ $questionnode->add(get_string('categories', 'quiz'), new moodle_url('/question/category.php', $params), navigation_node::TYPE_SETTING);
++ }
++ if ($contexts->have_one_edit_tab_cap('import')) {
++ $questionnode->add(get_string('import', 'quiz'), new moodle_url('/question/import.php', $params), navigation_node::TYPE_SETTING);
++ }
++ if ($contexts->have_one_edit_tab_cap('export')) {
++ $questionnode->add(get_string('export', 'quiz'), new moodle_url('/question/export.php', $params), navigation_node::TYPE_SETTING);
++ }
++
++ return $questionnode;
++}
++
++class question_edit_contexts {
++
++ public static $CAPS = array(
++ 'editq' => array('moodle/question:add',
++ 'moodle/question:editmine',
++ 'moodle/question:editall',
++ 'moodle/question:viewmine',
++ 'moodle/question:viewall',
++ 'moodle/question:usemine',
++ 'moodle/question:useall',
++ 'moodle/question:movemine',
++ 'moodle/question:moveall'),
++ 'questions'=>array('moodle/question:add',
++ 'moodle/question:editmine',
++ 'moodle/question:editall',
++ 'moodle/question:viewmine',
++ 'moodle/question:viewall',
++ 'moodle/question:movemine',
++ 'moodle/question:moveall'),
++ 'categories'=>array('moodle/question:managecategory'),
++ 'import'=>array('moodle/question:add'),
++ 'export'=>array('moodle/question:viewall', 'moodle/question:viewmine'));
++
++ protected $allcontexts;
++
++ /**
++ * @param current context
++ */
++ public function question_edit_contexts($thiscontext){
++ $pcontextids = get_parent_contexts($thiscontext);
++ $contexts = array($thiscontext);
++ foreach ($pcontextids as $pcontextid){
++ $contexts[] = get_context_instance_by_id($pcontextid);
++ }
++ $this->allcontexts = $contexts;
++ }
++ /**
++ * @return array all parent contexts
++ */
++ public function all(){
++ return $this->allcontexts;
++ }
++ /**
++ * @return object lowest context which must be either the module or course context
++ */
++ public function lowest(){
++ return $this->allcontexts[0];
++ }
++ /**
++ * @param string $cap capability
++ * @return array parent contexts having capability, zero based index
++ */
++ public function having_cap($cap){
++ $contextswithcap = array();
++ foreach ($this->allcontexts as $context){
++ if (has_capability($cap, $context)){
++ $contextswithcap[] = $context;
++ }
++ }
++ return $contextswithcap;
++ }
++ /**
++ * @param array $caps capabilities
++ * @return array parent contexts having at least one of $caps, zero based index
++ */
++ public function having_one_cap($caps){
++ $contextswithacap = array();
++ foreach ($this->allcontexts as $context){
++ foreach ($caps as $cap){
++ if (has_capability($cap, $context)){
++ $contextswithacap[] = $context;
++ break; //done with caps loop
++ }
++ }
++ }
++ return $contextswithacap;
++ }
++ /**
++ * @param string $tabname edit tab name
++ * @return array parent contexts having at least one of $caps, zero based index
++ */
++ public function having_one_edit_tab_cap($tabname){
++ return $this->having_one_cap(self::$CAPS[$tabname]);
++ }
++ /**
++ * Has at least one parent context got the cap $cap?
++ *
++ * @param string $cap capability
++ * @return boolean
++ */
++ public function have_cap($cap){
++ return (count($this->having_cap($cap)));
++ }
++
++ /**
++ * Has at least one parent context got one of the caps $caps?
++ *
++ * @param array $caps capability
++ * @return boolean
++ */
++ public function have_one_cap($caps){
++ foreach ($caps as $cap) {
++ if ($this->have_cap($cap)) {
++ return true;
++ }
++ }
++ return false;
++ }
++ /**
++ * Has at least one parent context got one of the caps for actions on $tabname
++ *
++ * @param string $tabname edit tab name
++ * @return boolean
++ */
++ public function have_one_edit_tab_cap($tabname){
++ return $this->have_one_cap(self::$CAPS[$tabname]);
++ }
++ /**
++ * Throw error if at least one parent context hasn't got the cap $cap
++ *
++ * @param string $cap capability
++ */
++ public function require_cap($cap){
++ if (!$this->have_cap($cap)){
++ print_error('nopermissions', '', '', $cap);
++ }
++ }
++ /**
++ * Throw error if at least one parent context hasn't got one of the caps $caps
++ *
++ * @param array $cap capabilities
++ */
++ public function require_one_cap($caps) {
++ if (!$this->have_one_cap($caps)) {
++ $capsstring = join($caps, ', ');
++ print_error('nopermissions', '', '', $capsstring);
++ }
++ }
++
++ /**
++ * Throw error if at least one parent context hasn't got one of the caps $caps
++ *
++ * @param string $tabname edit tab name
++ */
++ public function require_one_edit_tab_cap($tabname){
++ if (!$this->have_one_edit_tab_cap($tabname)) {
++ print_error('nopermissions', '', '', 'access question edit tab '.$tabname);
++ }
++ }
++}
++
++/**
++ * Rewrite question url, file_rewrite_pluginfile_urls always build url by
++ * $file/$contextid/$component/$filearea/$itemid/$pathname_in_text, so we cannot add
++ * extra questionid and attempted in url by it, so we create quiz_rewrite_question_urls
++ * to build url here
++ *
++ * @param string $text text being processed
++ * @param string $file the php script used to serve files
++ * @param int $contextid
++ * @param string $component component
++ * @param string $filearea filearea
++ * @param array $ids other IDs will be used to check file permission
++ * @param int $itemid
++ * @param array $options
++ * @return string
++ */
++function quiz_rewrite_question_urls($text, $file, $contextid, $component, $filearea, array $ids, $itemid, array $options=null) {
++ global $CFG;
++
++ $options = (array)$options;
++ if (!isset($options['forcehttps'])) {
++ $options['forcehttps'] = false;
++ }
++
++ if (!$CFG->slasharguments) {
++ $file = $file . '?file=';
++ }
++
++ $baseurl = "$CFG->wwwroot/$file/$contextid/$component/$filearea/";
++
++ if (!empty($ids)) {
++ $baseurl .= (implode('/', $ids) . '/');
++ }
++
++ if ($itemid !== null) {
++ $baseurl .= "$itemid/";
++ }
++
++ if ($options['forcehttps']) {
++ $baseurl = str_replace('http://', 'https://', $baseurl);
++ }
++
++ return str_replace('@@PLUGINFILE@@/', $baseurl, $text);
++}
++
++/**
++ * Called by pluginfile.php to serve files related to the 'question' core
++ * component and for files belonging to qtypes.
++ *
++ * For files that relate to questions in a question_attempt, then we delegate to
++ * a function in the component that owns the attempt (for example in the quiz,
++ * or in core question preview) to get necessary inforation.
++ *
++ * (Note that, at the moment, all question file areas relate to questions in
++ * attempts, so the If at the start of the last paragraph is always true.)
++ *
++ * Does not return, either calls send_file_not_found(); or serves the file.
++ *
++ * @param object $course course settings object
++ * @param object $context context object
++ * @param string $component the name of the component we are serving files for.
++ * @param string $filearea the name of the file area.
++ * @param array $args the remaining bits of the file path.
++ * @param bool $forcedownload whether the user must be forced to download the file.
++ */
++function question_pluginfile($course, $context, $component, $filearea, $args, $forcedownload) {
++ global $DB, $CFG;
++
++ list($context, $course, $cm) = get_context_info_array($context->id);
++ require_login($course, false, $cm);
++
++ if ($filearea === 'export') {
++ require_once($CFG->dirroot . '/question/editlib.php');
++ $contexts = new question_edit_contexts($context);
++ // check export capability
++ $contexts->require_one_edit_tab_cap('export');
++ $category_id = (int)array_shift($args);
++ $format = array_shift($args);
++ $cattofile = array_shift($args);
++ $contexttofile = array_shift($args);
++ $filename = array_shift($args);
++
++ // load parent class for import/export
++ require_once($CFG->dirroot . '/question/format.php');
++ require_once($CFG->dirroot . '/question/editlib.php');
++ require_once($CFG->dirroot . '/question/format/' . $format . '/format.php');
++
++ $classname = 'qformat_' . $format;
++ if (!class_exists($classname)) {
++ send_file_not_found();
++ }
++
++ $qformat = new $classname();
++
++ if (!$category = $DB->get_record('question_categories', array('id' => $category_id))) {
++ send_file_not_found();
++ }
++
++ $qformat->setCategory($category);
++ $qformat->setContexts($contexts->having_one_edit_tab_cap('export'));
++ $qformat->setCourse($course);
++
++ if ($cattofile == 'withcategories') {
++ $qformat->setCattofile(true);
++ } else {
++ $qformat->setCattofile(false);
++ }
++
++ if ($contexttofile == 'withcontexts') {
++ $qformat->setContexttofile(true);
++ } else {
++ $qformat->setContexttofile(false);
++ }
++
++ if (!$qformat->exportpreprocess()) {
++ send_file_not_found();
++ print_error('exporterror', 'question', $thispageurl->out());
++ }
++
++ // export data to moodle file pool
++ if (!$content = $qformat->exportprocess(true)) {
++ send_file_not_found();
++ }
++
++ //DEBUG
++ //echo '';
++ //die;
++ send_file($content, $filename, 0, 0, true, true, $qformat->mime_type());
++ }
++
++ $attemptid = (int)array_shift($args);
++ $questionid = (int)array_shift($args);
++
++
++ if ($attemptid === 0) {
++ // preview
++ require_once($CFG->dirroot . '/question/previewlib.php');
++ return question_preview_question_pluginfile($course, $context,
++ $component, $filearea, $attemptid, $questionid, $args, $forcedownload);
++
++ } else {
++ $module = $DB->get_field('question_attempts', 'modulename',
++ array('id' => $attemptid));
++
++ $dir = get_component_directory($module);
++ if (!file_exists("$dir/lib.php")) {
++ send_file_not_found();
++ }
++ include_once("$dir/lib.php");
++
++ $filefunction = $module . '_question_pluginfile';
++ if (!function_exists($filefunction)) {
++ send_file_not_found();
++ }
++
++ $filefunction($course, $context, $component, $filearea, $attemptid, $questionid,
++ $args, $forcedownload);
++
++ send_file_not_found();
++ }
++}
++
++/**
++ * Final test for whether a studnet should be allowed to see a particular file.
++ * This delegates the decision to the question type plugin.
++ *
++ * @param object $question The question to be rendered.
++ * @param object $state The state to render the question in.
++ * @param object $options An object specifying the rendering options.
++ * @param string $component the name of the component we are serving files for.
++ * @param string $filearea the name of the file area.
++ * @param array $args the remaining bits of the file path.
++ * @param bool $forcedownload whether the user must be forced to download the file.
++ */
++function question_check_file_access($question, $state, $options, $contextid, $component,
++ $filearea, $args, $forcedownload) {
++ global $QTYPES;
++ return $QTYPES[$question->qtype]->check_file_access($question, $state, $options, $contextid, $component,
++ $filearea, $args, $forcedownload);
++}
++
++/**
++ * Create url for question export
++ *
++ * @param int $contextid, current context
++ * @param int $categoryid, categoryid
++ * @param string $format
++ * @param string $withcategories
++ * @param string $ithcontexts
++ * @param moodle_url export file url
++ */
++function question_make_export_url($contextid, $categoryid, $format, $withcategories, $withcontexts, $filename) {
++ global $CFG;
++ $urlbase = "$CFG->httpswwwroot/pluginfile.php";
++ return moodle_url::make_file_url($urlbase, "/$contextid/question/export/{$categoryid}/{$format}/{$withcategories}/{$withcontexts}/{$filename}", true);
++}
diff --git a/question/todo/questionlib_qe.diff.txt b/question/todo/questionlib_qe.diff.txt
new file mode 100644
index 00000000000..31ce5d9ebd9
--- /dev/null
+++ b/question/todo/questionlib_qe.diff.txt
@@ -0,0 +1,1720 @@
+diff --git a/lib/questionlib.php b/lib/questionlib.php
+index 04752b2..3211e92 100644
+--- a/lib/questionlib.php
++++ b/lib/questionlib.php
+@@ -1,4 +1,20 @@
+-.
++
+ /**
+ * Code for handling and processing questions
+ *
+@@ -10,34 +26,18 @@
+ * TODO: separate those functions which form part of the API
+ * from the helper functions.
+ *
+- * @author Martin Dougiamas and many others. This has recently been completely
+- * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
+- * the Serving Mathematics project
+- * {@link http://maths.york.ac.uk/serving_maths}
+- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+- * @package question
++ * @package moodlecore
++ * @subpackage questionbank
++ * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
++ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+-/// CONSTANTS ///////////////////////////////////
+
+-/**#@+
+- * The different types of events that can create question states
+- */
+-define('QUESTION_EVENTOPEN', '0'); // The state was created by Moodle
+-define('QUESTION_EVENTNAVIGATE', '1'); // The responses were saved because the student navigated to another page (this is not currently used)
+-define('QUESTION_EVENTSAVE', '2'); // The student has requested that the responses should be saved but not submitted or validated
+-define('QUESTION_EVENTGRADE', '3'); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle.
+-define('QUESTION_EVENTDUPLICATE', '4'); // The responses submitted were the same as previously
+-define('QUESTION_EVENTVALIDATE', '5'); // The student has requested a validation. This causes the responses to be saved as well, but not graded.
+-define('QUESTION_EVENTCLOSEANDGRADE', '6'); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle.
+-define('QUESTION_EVENTSUBMIT', '7'); // The student response has been submitted but it has not yet been marked
+-define('QUESTION_EVENTCLOSE', '8'); // The response has been submitted and the session has been closed, either because the student requested it or because Moodle did it (e.g. because of a timelimit). The responses have not been graded.
+-define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher
+-
+-define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','.
+- QUESTION_EVENTCLOSEANDGRADE.','.
+- QUESTION_EVENTMANUALGRADE);
+-/**#@-*/
++require_once($CFG->dirroot . '/question/engine/lib.php');
++require_once($CFG->dirroot . '/question/type/questiontype.php');
++
++
++/// CONSTANTS ///////////////////////////////////
+
+ /**#@+
+ * The core question types.
+@@ -59,7 +59,7 @@ define("ESSAY", "essay");
+ * Constant determines the number of answer boxes supplied in the editing
+ * form for multiple choice and similar question types.
+ */
+-define("QUESTION_NUMANS", "10");
++define("QUESTION_NUMANS", 10);
+
+ /**
+ * Constant determines the number of answer boxes supplied in the editing
+@@ -78,22 +78,10 @@ define("QUESTION_NUMANS_ADD", 3);
+ /**
+ * The options used when popping up a question preview window in Javascript.
+ */
+-define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes,resizable=yes,width=700,height=540');
++define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes,resizable=yes,width=800,height=600');
+
+ /**#@+
+- * Option flags for ->optionflags
+- * The options are read out via bitwise operation using these constants
+- */
+-/**
+- * Whether the questions is to be run in adaptive mode. If this is not set then
+- * a question closes immediately after the first submission of responses. This
+- * is how question is Moodle always worked before version 1.5
+- */
+-define('QUESTION_ADAPTIVE', 1);
+-
+-/**
+ * options used in forms that move files.
+- *
+ */
+ define('QUESTION_FILENOTHINGSELECTED', 0);
+ define('QUESTION_FILEDONOTHING', 1);
+@@ -103,64 +91,13 @@ define('QUESTION_FILEMOVELINKSONLY', 4);
+
+ /**#@-*/
+
+-/// QTYPES INITIATION //////////////////
+-// These variables get initialised via calls to question_register_questiontype
+-// as the question type classes are included.
+-global $QTYPES, $QTYPE_MANUAL, $QTYPE_EXCLUDE_FROM_RANDOM;
+-/**
+- * Array holding question type objects
+- */
+-$QTYPES = array();
+ /**
+- * String in the format "'type1','type2'" that can be used in SQL clauses like
+- * "WHERE q.type IN ($QTYPE_MANUAL)".
++ * @global array holding question type objects
++ * @deprecated
+ */
+-$QTYPE_MANUAL = '';
+-/**
+- * String in the format "'type1','type2'" that can be used in SQL clauses like
+- * "WHERE q.type NOT IN ($QTYPE_EXCLUDE_FROM_RANDOM)".
+- */
+-$QTYPE_EXCLUDE_FROM_RANDOM = '';
+-
+-/**
+- * Add a new question type to the various global arrays above.
+- *
+- * @param object $qtype An instance of the new question type class.
+- */
+-function question_register_questiontype($qtype) {
+- global $QTYPES, $QTYPE_MANUAL, $QTYPE_EXCLUDE_FROM_RANDOM;
+-
+- $name = $qtype->name();
+- $QTYPES[$name] = $qtype;
+- if ($qtype->is_manual_graded()) {
+- if ($QTYPE_MANUAL) {
+- $QTYPE_MANUAL .= ',';
+- }
+- $QTYPE_MANUAL .= "'$name'";
+- }
+- if (!$qtype->is_usable_by_random()) {
+- if ($QTYPE_EXCLUDE_FROM_RANDOM) {
+- $QTYPE_EXCLUDE_FROM_RANDOM .= ',';
+- }
+- $QTYPE_EXCLUDE_FROM_RANDOM .= "'$name'";
+- }
+-}
+-
+-require_once("$CFG->dirroot/question/type/questiontype.php");
++global $QTYPES;
++$QTYPES = question_bank::get_all_qtypes();
+
+-// Load the questiontype.php file for each question type
+-// These files in turn call question_register_questiontype()
+-// with a new instance of each qtype class.
+-$qtypenames= get_list_of_plugins('question/type');
+-foreach($qtypenames as $qtypename) {
+- // Instanciates all plug-in question types
+- $qtypefilepath= "$CFG->dirroot/question/type/$qtypename/questiontype.php";
+-
+- // echo "Loading $qtypename "; // Uncomment for debugging
+- if (is_readable($qtypefilepath)) {
+- require_once($qtypefilepath);
+- }
+-}
+
+ /**
+ * An array of question type names translated to the user's language, suitable for use when
+@@ -173,11 +110,10 @@ foreach($qtypenames as $qtypename) {
+ * @return array an array of question type names translated to the user's language.
+ */
+ function question_type_menu() {
+- global $QTYPES;
+ static $menu_options = null;
+ if (is_null($menu_options)) {
+ $menu_options = array();
+- foreach ($QTYPES as $name => $qtype) {
++ foreach (question_bank::get_all_qtypes() as $name => $qtype) {
+ $menuname = $qtype->menu_name();
+ if ($menuname) {
+ $menu_options[$name] = $menuname;
+@@ -187,86 +123,58 @@ function question_type_menu() {
+ return $menu_options;
+ }
+
+-/// OTHER CLASSES /////////////////////////////////////////////////////////
+-
+-/**
+- * This holds the options that are set by the course module
+- */
+-class cmoptions {
+- /**
+- * Whether a new attempt should be based on the previous one. If true
+- * then a new attempt will start in a state where all responses are set
+- * to the last responses from the previous attempt.
+- */
+- var $attemptonlast = false;
+-
+- /**
+- * Various option flags. The flags are accessed via bitwise operations
+- * using the constants defined in the CONSTANTS section above.
+- */
+- var $optionflags = QUESTION_ADAPTIVE;
+-
+- /**
+- * Determines whether in the calculation of the score for a question
+- * penalties for earlier wrong responses within the same attempt will
+- * be subtracted.
+- */
+- var $penaltyscheme = true;
+-
+- /**
+- * The maximum time the user is allowed to answer the questions withing
+- * an attempt. This is measured in minutes so needs to be multiplied by
+- * 60 before compared to timestamps. If set to 0 no timelimit will be applied
+- */
+- var $timelimit = 0;
+-
+- /**
+- * Timestamp for the closing time. Responses submitted after this time will
+- * be saved but no credit will be given for them.
+- */
+- var $timeclose = 9999999999;
+-
+- /**
+- * The id of the course from withing which the question is currently being used
+- */
+- var $course = SITEID;
+-
+- /**
+- * Whether the answers in a multiple choice question should be randomly
+- * shuffled when a new attempt is started.
+- */
+- var $shuffleanswers = true;
+-
+- /**
+- * The number of decimals to be shown when scores are printed
+- */
+- var $decimalpoints = 2;
+-}
+-
+-
+ /// FUNCTIONS //////////////////////////////////////////////////////
+
+ /**
+ * Returns an array of names of activity modules that use this question
+ *
++ * @deprecated since Moodle 2.1. Use {@link questions_in_use} instead.
++
+ * @param object $questionid
+ * @return array of strings
+ */
+ function question_list_instances($questionid) {
++ throw new coding_exception('question_list_instances has been deprectated. Please use questions_in_use instead.');
++}
++
++/**
++ * @param array $questionids of question ids.
++ * @return boolean whether any of these questions are being used by any part of Moodle.
++ */
++function questions_in_use($questionids) {
+ global $CFG;
+- $instances = array();
+- $modules = get_records('modules');
+- foreach ($modules as $module) {
+- $fullmod = $CFG->dirroot . '/mod/' . $module->name;
+- if (file_exists($fullmod . '/lib.php')) {
+- include_once($fullmod . '/lib.php');
+- $fn = $module->name.'_question_list_instances';
++
++ if (question_engine::questions_in_use($questionids)) {
++ return true;
++ }
++
++ foreach (get_records('modules') as $module) {
++ $lib = $CFG->dirroot . '/mod/' . $module->name . '/lib.php';
++ if (file_exists($lib)) {
++ include_once($lib);
++
++ $fn = $module->name . '_questions_in_use';
+ if (function_exists($fn)) {
+- $instances = $instances + $fn($questionid);
++ if ($fn($questionids)) {
++ return true;
++ }
++ } else {
++
++ // Fallback for legacy modules.
++ $fn = $module->name.'_question_list_instances';
++ foreach ($questionids as $questionid) {
++ if (function_exists($fn)) {
++ $instances = $fn($questionid);
++ if (!empty($instances)) {
++ return true;
++ }
++ }
++ }
+ }
+ }
+ }
+- return $instances;
++
++ return false;
+ }
+
+ /**
+@@ -298,39 +206,42 @@ function question_context_has_any_questions($context) {
+ * @return object ->gradeoptionsfull full array ->gradeoptions +ve only
+ */
+ function get_grade_options() {
+- // define basic array of grades
++ // define basic array of grades. This list comprises all fractions of the form:
++ // a. p/q for q <= 6, 0 <= p <= q
++ // b. p/10 for 0 <= p <= 10
++ // c. 1/q for 1 <= q <= 10
++ // d. 1/20
+ $grades = array(
+- 1.00,
+- 0.90,
+- 0.83333,
+- 0.80,
+- 0.75,
+- 0.70,
+- 0.66666,
+- 0.60,
+- 0.50,
+- 0.40,
+- 0.33333,
+- 0.30,
+- 0.25,
+- 0.20,
+- 0.16666,
+- 0.142857,
+- 0.125,
+- 0.11111,
+- 0.10,
+- 0.05,
+- 0);
++ 1.0000000,
++ 0.9000000,
++ 0.8333333,
++ 0.8000000,
++ 0.7500000,
++ 0.7000000,
++ 0.6666667,
++ 0.6000000,
++ 0.5000000,
++ 0.4000000,
++ 0.3333333,
++ 0.3000000,
++ 0.2500000,
++ 0.2000000,
++ 0.1666667,
++ 0.1428571,
++ 0.1250000,
++ 0.1111111,
++ 0.1000000,
++ 0.0500000,
++ 0.0000000);
+
+ // iterate through grades generating full range of options
+ $gradeoptionsfull = array();
+ $gradeoptions = array();
+ foreach ($grades as $grade) {
+ $percentage = 100 * $grade;
+- $neggrade = -$grade;
+- $gradeoptions["$grade"] = "$percentage %";
+- $gradeoptionsfull["$grade"] = "$percentage %";
+- $gradeoptionsfull["$neggrade"] = -$percentage." %";
++ $gradeoptions["$grade"] = $percentage . '%';
++ $gradeoptionsfull["$grade"] = $percentage . '%';
++ $gradeoptionsfull['' . (-$grade)] = (-$percentage) . '%';
+ }
+ $gradeoptionsfull["0"] = $gradeoptions["0"] = get_string("none");
+
+@@ -386,60 +297,43 @@ function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
+ }
+
+ /**
+- * Tests whether a category is in use by any activity module
+- *
+- * @return boolean
+- * @param integer $categoryid
+- * @param boolean $recursive Whether to examine category children recursively
++ * @deprecated Since Moodle 2.1. Use {@link question_category_in_use} instead.
++ * @param integer $categoryid a question category id.
++ * @param boolean $recursive whether to check child categories too.
++ * @return boolean whether any question in this category is in use.
+ */
+ function question_category_isused($categoryid, $recursive = false) {
+-
+- //Look at each question in the category
+- if ($questions = get_records('question', 'category', $categoryid)) {
+- foreach ($questions as $question) {
+- if (count(question_list_instances($question->id))) {
+- return true;
+- }
+- }
+- }
+-
+- //Look under child categories recursively
+- if ($recursive) {
+- if ($children = get_records('question_categories', 'parent', $categoryid)) {
+- foreach ($children as $child) {
+- if (question_category_isused($child->id, $recursive)) {
+- return true;
+- }
+- }
+- }
+- }
+-
+- return false;
++ throw new coding_exception('question_category_isused has been deprectated. Please use question_category_in_use instead.');
+ }
+
+ /**
+- * Deletes all data associated to an attempt from the database
++ * Tests whether any question in a category is used by any part of Moodle.
+ *
+- * @param integer $attemptid The id of the attempt being deleted
++ * @param integer $categoryid a question category id.
++ * @param boolean $recursive whether to check child categories too.
++ * @return boolean whether any question in this category is in use.
+ */
+-function delete_attempt($attemptid) {
+- global $QTYPES;
++function question_category_in_use($categoryid, $recursive = false) {
+
+- $states = get_records('question_states', 'attempt', $attemptid);
+- if ($states) {
+- $stateslist = implode(',', array_keys($states));
+-
+- // delete question-type specific data
+- foreach ($QTYPES as $qtype) {
+- $qtype->delete_states($stateslist);
++ // Look at each question in the category
++ if ($questions = get_records('question', 'category', $categoryid)) {
++ if (questions_in_use($questions)) {
++ return true;
+ }
+ }
++ if (!$recursive) {
++ return false;
++ }
+
+- // delete entries from all other question tables
+- // It is important that this is done only after calling the questiontype functions
+- delete_records("question_states", "attempt", $attemptid);
+- delete_records("question_sessions", "attemptid", $attemptid);
+- delete_records("question_attempts", "id", $attemptid);
++ // Look under child categories recursively
++ if ($children = get_records('question_categories', 'parent', $categoryid)) {
++ foreach ($children as $child) {
++ if (question_category_in_use($child->id, $recursive)) {
++ return true;
++ }
++ }
++ }
++ return false;
+ }
+
+ /**
+@@ -448,7 +342,7 @@ function delete_attempt($attemptid) {
+ * It will not delete a question if it is used by an activity module
+ * @param object $question The question being deleted
+ */
+-function delete_question($questionid) {
++function question_delete_question($questionid) {
+ global $QTYPES;
+
+ if (!$question = get_record('question', 'id', $questionid)) {
+@@ -459,12 +353,17 @@ function delete_question($questionid) {
+ }
+
+ // Do not delete a question if it is used by an activity module
+- if (count(question_list_instances($questionid))) {
++ if (questions_in_use(array($questionid))) {
+ return;
+ }
+
+- // delete questiontype-specific data
++ // Check permissions.
+ question_require_capability_on($question, 'edit');
++
++ $dm = new question_engine_data_mapper();
++ $dm->delete_previews($questionid);
++
++ // Delete questiontype-specific data
+ if ($question) {
+ if (isset($QTYPES[$question->qtype])) {
+ $QTYPES[$question->qtype]->delete_question($questionid);
+@@ -473,26 +372,11 @@ function delete_question($questionid) {
+ echo "Question with id $questionid does not exist. ";
+ }
+
+- if ($states = get_records('question_states', 'question', $questionid)) {
+- $stateslist = implode(',', array_keys($states));
+-
+- // delete questiontype-specific data
+- foreach ($QTYPES as $qtype) {
+- $qtype->delete_states($stateslist);
+- }
+- }
+-
+- // delete entries from all other question tables
+- // It is important that this is done only after calling the questiontype functions
+- delete_records("question_answers", "question", $questionid);
+- delete_records("question_states", "question", $questionid);
+- delete_records("question_sessions", "questionid", $questionid);
+-
+- // Now recursively delete all child questions
++ // Recursively delete all child questions
+ if ($children = get_records('question', 'parent', $questionid)) {
+ foreach ($children as $child) {
+ if ($child->id != $questionid) {
+- delete_question($child->id);
++ question_delete_question($child->id);
+ }
+ }
+ }
+@@ -517,7 +401,7 @@ function question_delete_course($course, $feedback=true) {
+ //Cache some strings
+ $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
+ $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
+- $categoriescourse = get_records('question_categories', 'contextid', $coursecontext->id, 'parent', 'id, parent, name');
++ $categoriescourse = get_records('question_categories', 'contextid', $coursecontext->id, 'parent', 'id, parent, name, contextid');
+
+ if ($categoriescourse) {
+
+@@ -531,7 +415,7 @@ function question_delete_course($course, $feedback=true) {
+ //deleting questions
+ if ($questions = get_records("question", "category", $category->id)) {
+ foreach ($questions as $question) {
+- delete_question($question->id);
++ question_delete_question($question->id);
+ }
+ delete_records("question", "category", $category->id);
+ }
+@@ -578,7 +462,7 @@ function question_delete_course_category($category, $newcategory, $feedback=true
+
+ // Try to delete each question.
+ foreach ($questions as $question) {
+- delete_question($question->id);
++ question_delete_question($question->id);
+ }
+
+ // Check to see if there were any questions that were kept because they are
+@@ -676,7 +560,7 @@ function question_delete_activity($cm, $feedback=true) {
+ //Cache some strings
+ $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
+ $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
+- if ($categoriesmods = get_records('question_categories', 'contextid', $modcontext->id, 'parent', 'id, parent, name')){
++ if ($categoriesmods = get_records('question_categories', 'contextid', $modcontext->id, 'parent', 'id, parent, name, contextid')){
+ //Sort categories following their tree (parent-child) relationships
+ //this will make the feedback more readable
+ $categoriesmods = sort_categories_by_tree($categoriesmods);
+@@ -687,7 +571,7 @@ function question_delete_activity($cm, $feedback=true) {
+ //deleting questions
+ if ($questions = get_records("question", "category", $category->id)) {
+ foreach ($questions as $question) {
+- delete_question($question->id);
++ question_delete_question($question->id);
+ }
+ delete_records("question", "category", $category->id);
+ }
+@@ -751,725 +635,148 @@ function questionbank_navigation_tabs(&$row, $contexts, $querystring) {
+ }
+
+ /**
+- * Private function to factor common code out of get_question_options().
+- *
+- * @param object $question the question to tidy.
+- * @return boolean true if successful, else false.
++ * Generate the URL for starting a new preview of a given question with the given options.
++ * @param integer $questionid the question to preview.
++ * @param string $preferredbehaviour the behaviour to use for the preview.
++ * @param float $maxmark the maximum to mark the question out of.
++ * @param question_display_options $displayoptions the display options to use.
++ * @return string the URL.
+ */
+-function _tidy_question(&$question) {
+- global $QTYPES;
+- if (!array_key_exists($question->qtype, $QTYPES)) {
+- $question->qtype = 'missingtype';
+- $question->questiontext = '
' . get_string('warningmissingtype', 'quiz') . '
' . $question->questiontext;
+- }
+- $question->name_prefix = question_make_name_prefix($question->id);
+- return $QTYPES[$question->qtype]->get_question_options($question);
+-}
+-
+-/**
+- * Updates the question objects with question type specific
+- * information by calling {@link get_question_options()}
++function question_preview_url($questionid, $preferredbehaviour, $maxmark, $displayoptions) {
++ global $CFG;
++ return $CFG->wwwroot . '/question/preview.php?id=' . $questionid .
++ '&behaviour=' . $preferredbehaviour .
++ '&maxmark=' . $maxmark .
++ '&correctness=' . $displayoptions->correctness .
++ '&marks=' . $displayoptions->marks .
++ '&markdp=' . $displayoptions->markdp .
++ '&feedback=' . (bool) $displayoptions->feedback .
++ '&generalfeedback=' . (bool) $displayoptions->generalfeedback .
++ '&rightanswer=' . (bool) $displayoptions->rightanswer .
++ '&history=' . (bool) $displayoptions->history;
++}
++
++/**
++ * Given a list of ids, load the basic information about a set of questions from the questions table.
++ * The $join and $extrafields arguments can be used together to pull in extra data.
++ * See, for example, the usage in mod/quiz/attemptlib.php, and
++ * read the code below to see how the SQL is assembled. Throws exceptions on error.
+ *
+- * Can be called either with an array of question objects or with a single
+- * question object.
++ * @global object
++ * @global object
++ * @param array $questionids array of question ids.
++ * @param string $extrafields extra SQL code to be added to the query.
++ * @param string $join extra SQL code to be added to the query.
++ * @param array $extraparams values for any placeholders in $join.
++ * You are strongly recommended to use named placeholder.
+ *
+- * @param mixed $questions Either an array of question objects to be updated
+- * or just a single question object
+- * @return bool Indicates success or failure.
++ * @return array partially complete question objects. You need to call get_question_options
++ * on them before they can be properly used.
+ */
+-function get_question_options(&$questions) {
+- if (is_array($questions)) { // deal with an array of questions
+- foreach ($questions as $i => $notused) {
+- if (!_tidy_question($questions[$i])) {
+- return false;
+- }
+- }
+- return true;
+- } else { // deal with single question
+- return _tidy_question($questions);
+- }
+-}
+-
+-/**
+-* Loads the most recent state of each question session from the database
+-* or create new one.
+-*
+-* For each question the most recent session state for the current attempt
+-* is loaded from the question_states table and the question type specific data and
+-* responses are added by calling {@link restore_question_state()} which in turn
+-* calls {@link restore_session_and_responses()} for each question.
+-* If no states exist for the question instance an empty state object is
+-* created representing the start of a session and empty question
+-* type specific information and responses are created by calling
+-* {@link create_session_and_responses()}.
+-*
+-* @return array An array of state objects representing the most recent
+-* states of the question sessions.
+-* @param array $questions The questions for which sessions are to be restored or
+-* created.
+-* @param object $cmoptions
+-* @param object $attempt The attempt for which the question sessions are
+-* to be restored or created.
+-* @param mixed either the id of a previous attempt, if this attmpt is
+-* building on a previous one, or false for a clean attempt.
+-*/
+-function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid = false) {
+- global $CFG, $QTYPES;
+-
+- // get the question ids
+- $ids = array_keys($questions);
+- $questionlist = implode(',', $ids);
+-
+- // The question field must be listed first so that it is used as the
+- // array index in the array returned by get_records_sql
+- $statefields = 'n.questionid as question, s.id, s.attempt, s.originalquestion, ' .
+- 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
+- 's.penalty, n.sumpenalty, n.manualcomment';
+- // Load the newest states for the questions
+- $sql = "SELECT $statefields".
+- " FROM {$CFG->prefix}question_states s,".
+- " {$CFG->prefix}question_sessions n".
+- " WHERE s.id = n.newest".
+- " AND n.attemptid = '$attempt->uniqueid'".
+- " AND n.questionid IN ($questionlist)";
+- $states = get_records_sql($sql);
+-
+- // Load the newest graded states for the questions
+- $sql = "SELECT $statefields".
+- " FROM {$CFG->prefix}question_states s,".
+- " {$CFG->prefix}question_sessions n".
+- " WHERE s.id = n.newgraded".
+- " AND n.attemptid = '$attempt->uniqueid'".
+- " AND n.questionid IN ($questionlist)";
+- $gradedstates = get_records_sql($sql);
+-
+- // loop through all questions and set the last_graded states
+- foreach ($ids as $i) {
+- if (isset($states[$i])) {
+- restore_question_state($questions[$i], $states[$i]);
+- if (isset($gradedstates[$i])) {
+- restore_question_state($questions[$i], $gradedstates[$i]);
+- $states[$i]->last_graded = $gradedstates[$i];
+- } else {
+- $states[$i]->last_graded = clone($states[$i]);
+- }
+- } else {
+- if ($lastattemptid) {
+- // If the new attempt is to be based on this previous attempt.
+- // Find the responses from the previous attempt and save them to the new session
+-
+- // Load the last graded state for the question. Note, $statefields is
+- // the same as above, except that we don't want n.manualcomment.
+- $statefields = 'n.questionid as question, s.id, s.attempt, s.originalquestion, ' .
+- 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
+- 's.penalty, n.sumpenalty';
+- $sql = "SELECT $statefields".
+- " FROM {$CFG->prefix}question_states s,".
+- " {$CFG->prefix}question_sessions n".
+- " WHERE s.id = n.newest".
+- " AND n.attemptid = '$lastattemptid'".
+- " AND n.questionid = '$i'";
+- if (!$laststate = get_record_sql($sql)) {
+- // Only restore previous responses that have been graded
+- continue;
+- }
+- // Restore the state so that the responses will be restored
+- restore_question_state($questions[$i], $laststate);
+- $states[$i] = clone($laststate);
+- unset($states[$i]->id);
+- } else {
+- // create a new empty state
+- $states[$i] = new object;
+- $states[$i]->question = $i;
+- $states[$i]->responses = array('' => '');
+- $states[$i]->raw_grade = 0;
+- }
+-
+- // now fill/overide initial values
+- $states[$i]->attempt = $attempt->uniqueid;
+- $states[$i]->seq_number = 0;
+- $states[$i]->timestamp = $attempt->timestart;
+- $states[$i]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN;
+- $states[$i]->grade = 0;
+- $states[$i]->penalty = 0;
+- $states[$i]->sumpenalty = 0;
+- $states[$i]->manualcomment = '';
+-
+- // Prevent further changes to the session from incrementing the
+- // sequence number
+- $states[$i]->changed = true;
+-
+- if ($lastattemptid) {
+- // prepare the previous responses for new processing
+- $action = new stdClass;
+- $action->responses = $laststate->responses;
+- $action->timestamp = $laststate->timestamp;
+- $action->event = QUESTION_EVENTSAVE; //emulate save of questions from all pages MDL-7631
+-
+- // Process these responses ...
+- question_process_responses($questions[$i], $states[$i], $action, $cmoptions, $attempt);
+-
+- // Fix for Bug #5506: When each attempt is built on the last one,
+- // preserve the options from any previous attempt.
+- if ( isset($laststate->options) ) {
+- $states[$i]->options = $laststate->options;
+- }
+- } else {
+- // Create the empty question type specific information
+- if (!$QTYPES[$questions[$i]->qtype]->create_session_and_responses(
+- $questions[$i], $states[$i], $cmoptions, $attempt)) {
+- return false;
+- }
+- }
+- $states[$i]->last_graded = clone($states[$i]);
+- }
++function question_preload_questions($questionids, $extrafields = '', $join = '') {
++ global $CFG;
++ if (empty($questionids)) {
++ return array();
+ }
+- return $states;
+-}
+-
+-
+-/**
+-* Creates the run-time fields for the states
+-*
+-* Extends the state objects for a question by calling
+-* {@link restore_session_and_responses()}
+-* @param object $question The question for which the state is needed
+-* @param object $state The state as loaded from the database
+-* @return boolean Represents success or failure
+-*/
+-function restore_question_state(&$question, &$state) {
+- global $QTYPES;
+-
+- // initialise response to the value in the answer field
+- $state->responses = array('' => addslashes($state->answer));
+- unset($state->answer);
+- $state->manualcomment = isset($state->manualcomment) ? addslashes($state->manualcomment) : '';
+-
+- // Set the changed field to false; any code which changes the
+- // question session must set this to true and must increment
+- // ->seq_number. The save_question_session
+- // function will save the new state object to the database if the field is
+- // set to true.
+- $state->changed = false;
+-
+- // Load the question type specific data
+- return $QTYPES[$question->qtype]
+- ->restore_session_and_responses($question, $state);
+-
+-}
+-
+-/**
+-* Saves the current state of the question session to the database
+-*
+-* The state object representing the current state of the session for the
+-* question is saved to the question_states table with ->responses[''] saved
+-* to the answer field of the database table. The information in the
+-* question_sessions table is updated.
+-* The question type specific data is then saved.
+-* @return mixed The id of the saved or updated state or false
+-* @param object $question The question for which session is to be saved.
+-* @param object $state The state information to be saved. In particular the
+-* most recent responses are in ->responses. The object
+-* is updated to hold the new ->id.
+-*/
+-function save_question_session(&$question, &$state) {
+- global $QTYPES;
+- // Check if the state has changed
+- if (!$state->changed && isset($state->id)) {
+- return $state->id;
++ if ($join) {
++ $join = ' JOIN '.$join;
+ }
+- // Set the legacy answer field
+- $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
+-
+- // Save the state
+- if (!empty($state->update)) { // this forces the old state record to be overwritten
+- update_record('question_states', $state);
+- } else {
+- if (!$state->id = insert_record('question_states', $state)) {
+- unset($state->id);
+- unset($state->answer);
+- return false;
+- }
++ if ($extrafields) {
++ $extrafields = ', ' . $extrafields;
+ }
++ $sql = 'SELECT q.*' . $extrafields . " FROM {$CFG->prefix}question q" . $join .
++ ' WHERE q.id IN (' . implode(',', $questionids) . ')';
+
+- // create or update the session
+- if (!$session = get_record('question_sessions', 'attemptid',
+- $state->attempt, 'questionid', $question->id)) {
+- $session->attemptid = $state->attempt;
+- $session->questionid = $question->id;
+- $session->newest = $state->id;
+- // The following may seem weird, but the newgraded field needs to be set
+- // already even if there is no graded state yet.
+- $session->newgraded = $state->id;
+- $session->sumpenalty = $state->sumpenalty;
+- $session->manualcomment = $state->manualcomment;
+- if (!insert_record('question_sessions', $session)) {
+- error('Could not insert entry in question_sessions');
+- }
+- } else {
+- $session->newest = $state->id;
+- if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) {
+- // this state is graded or newly opened, so it goes into the lastgraded field as well
+- $session->newgraded = $state->id;
+- $session->sumpenalty = $state->sumpenalty;
+- $session->manualcomment = $state->manualcomment;
+- } else {
+- $session->manualcomment = addslashes($session->manualcomment);
+- }
+- update_record('question_sessions', $session);
++ // Load the questions
++ if (!$questions = get_records_sql($sql)) {
++ return array();
+ }
+
+- unset($state->answer);
+-
+- // Save the question type specific state information and responses
+- if (!$QTYPES[$question->qtype]->save_session_and_responses(
+- $question, $state)) {
+- return false;
++ foreach ($questions as $question) {
++ $question->_partiallyloaded = true;
+ }
+- // Reset the changed flag
+- $state->changed = false;
+- return $state->id;
+-}
+
+-/**
+-* Determines whether a state has been graded by looking at the event field
+-*
+-* @return boolean true if the state has been graded
+-* @param object $state
+-*/
+-function question_state_is_graded($state) {
+- $gradedevents = explode(',', QUESTION_EVENTS_GRADED);
+- return (in_array($state->event, $gradedevents));
+-}
++ // Note, a possible optimisation here would be to not load the TEXT fields
++ // (that is, questiontext and generalfeedback) here, and instead load them in
++ // question_load_questions. That would add one DB query, but reduce the amount
++ // of data transferred most of the time. I am not going to do this optimisation
++ // until it is shown to be worthwhile.
+
+-/**
+-* Determines whether a state has been closed by looking at the event field
+-*
+-* @return boolean true if the state has been closed
+-* @param object $state
+-*/
+-function question_state_is_closed($state) {
+- return ($state->event == QUESTION_EVENTCLOSE
+- or $state->event == QUESTION_EVENTCLOSEANDGRADE
+- or $state->event == QUESTION_EVENTMANUALGRADE);
++ return $questions;
+ }
+
+-
+ /**
+- * Extracts responses from submitted form
++ * Load a set of questions, given a list of ids. The $join and $extrafields arguments can be used
++ * together to pull in extra data. See, for example, the usage in mod/quiz/attempt.php, and
++ * read the code below to see how the SQL is assembled. Throws exceptions on error.
+ *
+- * This can extract the responses given to one or several questions present on a page
+- * It returns an array with one entry for each question, indexed by question id
+- * Each entry is an object with the properties
+- * ->event The event that has triggered the submission. This is determined by which button
+- * the user has pressed.
+- * ->responses An array holding the responses to an individual question, indexed by the
+- * name of the corresponding form element.
+- * ->timestamp A unix timestamp
+- * @return array array of action objects, indexed by question ids.
+- * @param array $questions an array containing at least all questions that are used on the form
+- * @param array $formdata the data submitted by the form on the question page
+- * @param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted
++ * @param array $questionids array of question ids.
++ * @param string $extrafields extra SQL code to be added to the query.
++ * @param string $join extra SQL code to be added to the query.
++ * @param array $extraparams values for any placeholders in $join.
++ * You are strongly recommended to use named placeholder.
++ *
++ * @return array question objects.
+ */
+-function question_extract_responses($questions, $formdata, $defaultevent=QUESTION_EVENTSAVE) {
+-
+- $time = time();
+- $actions = array();
+- foreach ($formdata as $key => $response) {
+- // Get the question id from the response name
+- if (false !== ($quid = question_get_id_from_name_prefix($key))) {
+- // check if this is a valid id
+- if (!isset($questions[$quid])) {
+- error('Form contained question that is not in questionids');
+- }
+-
+- // Remove the name prefix from the name
+- //decrypt trying
+- $key = substr($key, strlen($questions[$quid]->name_prefix));
+- if (false === $key) {
+- $key = '';
+- }
+- // Check for question validate and mark buttons & set events
+- if ($key === 'validate') {
+- $actions[$quid]->event = QUESTION_EVENTVALIDATE;
+- } else if ($key === 'submit') {
+- $actions[$quid]->event = QUESTION_EVENTSUBMIT;
+- } else {
+- $actions[$quid]->event = $defaultevent;
+- }
+-
+- // Update the state with the new response
+- $actions[$quid]->responses[$key] = $response;
++function question_load_questions($questionids, $extrafields = '', $join = '') {
++ $questions = question_preload_questions($questionids, $extrafields, $join);
+
+- // Set the timestamp
+- $actions[$quid]->timestamp = $time;
+- }
+- }
+- foreach ($actions as $quid => $notused) {
+- ksort($actions[$quid]->responses);
++ // Load the question type specific information
++ if (!get_question_options($questions)) {
++ return 'Could not load the question options';
+ }
+- return $actions;
+-}
+-
+-
+-/**
+- * Returns the html for question feedback image.
+- * @param float $fraction value representing the correctness of the user's
+- * response to a question.
+- * @param boolean $selected whether or not the answer is the one that the
+- * user picked.
+- * @return string
+- */
+-function question_get_feedback_image($fraction, $selected=true) {
+-
+- global $CFG;
+
+- if ($fraction >= 1.0) {
+- if ($selected) {
+- $feedbackimg = '';
+- } else {
+- $feedbackimg = '';
+- }
+- } else if ($fraction > 0.0 && $fraction < 1.0) {
+- if ($selected) {
+- $feedbackimg = '';
+- } else {
+- $feedbackimg = '';
+- }
+- } else {
+- if ($selected) {
+- $feedbackimg = '';
+- } else {
+- $feedbackimg = '';
+- }
+- }
+- return $feedbackimg;
++ return $questions;
+ }
+
+-
+ /**
+- * Returns the class name for question feedback.
+- * @param float $fraction value representing the correctness of the user's
+- * response to a question.
+- * @return string
++ * Private function to factor common code out of get_question_options().
++ *
++ * @param object $question the question to tidy.
++ * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
++ * @return boolean true if successful, else false.
+ */
+-function question_get_feedback_class($fraction) {
+-
+- global $CFG;
+-
+- if ($fraction >= 1.0) {
+- $class = 'correct';
+- } else if ($fraction > 0.0 && $fraction < 1.0) {
+- $class = 'partiallycorrect';
+- } else {
+- $class = 'incorrect';
++function _tidy_question(&$question, $loadtags = false) {
++ global $CFG, $QTYPES;
++ if (!array_key_exists($question->qtype, $QTYPES)) {
++ $question->qtype = 'missingtype';
++ $question->questiontext = '
' . get_string('warningmissingtype', 'quiz') . '
' . $question->questiontext;
+ }
+- return $class;
+-}
+-
+-
+-/**
+-* For a given question in an attempt we walk the complete history of states
+-* and recalculate the grades as we go along.
+-*
+-* This is used when a question is changed and old student
+-* responses need to be marked with the new version of a question.
+-*
+-* TODO: Make sure this is not quiz-specific
+-*
+-* @return boolean Indicates whether the grade has changed
+-* @param object $question A question object
+-* @param object $attempt The attempt, in which the question needs to be regraded.
+-* @param object $cmoptions
+-* @param boolean $verbose Optional. Whether to print progress information or not.
+-*/
+-function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false) {
+-
+- // load all states for this question in this attempt, ordered in sequence
+- if ($states = get_records_select('question_states',
+- "attempt = '{$attempt->uniqueid}' AND question = '{$question->id}'",
+- 'seq_number ASC')) {
+- $states = array_values($states);
+-
+- // Subtract the grade for the latest state from $attempt->sumgrades to get the
+- // sumgrades for the attempt without this question.
+- $attempt->sumgrades -= $states[count($states)-1]->grade;
+-
+- // Initialise the replaystate
+- $state = clone($states[0]);
+- $state->manualcomment = get_field('question_sessions', 'manualcomment', 'attemptid',
+- $attempt->uniqueid, 'questionid', $question->id);
+- restore_question_state($question, $state);
+- $state->sumpenalty = 0.0;
+- $replaystate = clone($state);
+- $replaystate->last_graded = $state;
+-
+- $changed = false;
+- for($j = 1; $j < count($states); $j++) {
+- restore_question_state($question, $states[$j]);
+- $action = new stdClass;
+- $action->responses = $states[$j]->responses;
+- $action->timestamp = $states[$j]->timestamp;
+-
+- // Change event to submit so that it will be reprocessed
+- if (QUESTION_EVENTCLOSE == $states[$j]->event
+- or QUESTION_EVENTGRADE == $states[$j]->event
+- or QUESTION_EVENTCLOSEANDGRADE == $states[$j]->event) {
+- $action->event = QUESTION_EVENTSUBMIT;
+-
+- // By default take the event that was saved in the database
+- } else {
+- $action->event = $states[$j]->event;
+- }
+-
+- if ($action->event == QUESTION_EVENTMANUALGRADE) {
+- // Ensure that the grade is in range - in the past this was not checked,
+- // but now it is (MDL-14835) - so we need to ensure the data is valid before
+- // proceeding.
+- if ($states[$j]->grade < 0) {
+- $states[$j]->grade = 0;
+- } else if ($states[$j]->grade > $question->maxgrade) {
+- $states[$j]->grade = $question->maxgrade;
+- }
+- $error = question_process_comment($question, $replaystate, $attempt,
+- $replaystate->manualcomment, $states[$j]->grade);
+- if (is_string($error)) {
+- notify($error);
+- }
+- } else {
+-
+- // Reprocess (regrade) responses
+- if (!question_process_responses($question, $replaystate,
+- $action, $cmoptions, $attempt)) {
+- $verbose && notify("Couldn't regrade state #{$state->id}!");
+- }
+- }
+-
+- // We need rounding here because grades in the DB get truncated
+- // e.g. 0.33333 != 0.3333333, but we want them to be equal here
+- if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5))
+- or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5))
+- or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) {
+- $changed = true;
+- }
+-
+- $replaystate->id = $states[$j]->id;
+- $replaystate->changed = true;
+- $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created
+- save_question_session($question, $replaystate);
++ if ($success = $QTYPES[$question->qtype]->get_question_options($question)) {
++ if (isset($question->_partiallyloaded)) {
++ unset($question->_partiallyloaded);
+ }
+- if ($changed) {
+- // TODO, call a method in quiz to do this, where 'quiz' comes from
+- // the question_attempts table.
+- update_record('quiz_attempts', $attempt);
+- }
+-
+- return $changed;
+- }
+- return false;
+-}
+-
+-/**
+-* Processes an array of student responses, grading and saving them as appropriate
+-*
+-* @param object $question Full question object, passed by reference
+-* @param object $state Full state object, passed by reference
+-* @param object $action object with the fields ->responses which
+-* is an array holding the student responses,
+-* ->action which specifies the action, e.g., QUESTION_EVENTGRADE,
+-* and ->timestamp which is a timestamp from when the responses
+-* were submitted by the student.
+-* @param object $cmoptions
+-* @param object $attempt The attempt is passed by reference so that
+-* during grading its ->sumgrades field can be updated
+-* @return boolean Indicates success/failure
+-*/
+-function question_process_responses(&$question, &$state, $action, $cmoptions, &$attempt) {
+- global $QTYPES;
+-
+- // if no responses are set initialise to empty response
+- if (!isset($action->responses)) {
+- $action->responses = array('' => '');
+ }
+-
+- // make sure these are gone!
+- unset($action->responses['submit'], $action->responses['validate']);
+-
+- // Check the question session is still open
+- if (question_state_is_closed($state)) {
+- return true;
+- }
+-
+- // If $action->event is not set that implies saving
+- if (! isset($action->event)) {
+- debugging('Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER);
+- $action->event = QUESTION_EVENTSAVE;
+- }
+- // If submitted then compare against last graded
+- // responses, not last given responses in this case
+- if (question_isgradingevent($action->event)) {
+- $state->responses = $state->last_graded->responses;
+- }
+-
+- // Check for unchanged responses (exactly unchanged, not equivalent).
+- // We also have to catch questions that the student has not yet attempted
+- $sameresponses = $QTYPES[$question->qtype]->compare_responses($question, $action, $state);
+- if (!empty($state->last_graded) && $state->last_graded->event == QUESTION_EVENTOPEN &&
+- question_isgradingevent($action->event)) {
+- $sameresponses = false;
+- }
+-
+- // If the response has not been changed then we do not have to process it again
+- // unless the attempt is closing or validation is requested
+- if ($sameresponses and QUESTION_EVENTCLOSE != $action->event
+- and QUESTION_EVENTVALIDATE != $action->event) {
+- return true;
+- }
+-
+- // Roll back grading information to last graded state and set the new
+- // responses
+- $newstate = clone($state->last_graded);
+- $newstate->responses = $action->responses;
+- $newstate->seq_number = $state->seq_number + 1;
+- $newstate->changed = true; // will assure that it gets saved to the database
+- $newstate->last_graded = clone($state->last_graded);
+- $newstate->timestamp = $action->timestamp;
+- $state = $newstate;
+-
+- // Set the event to the action we will perform. The question type specific
+- // grading code may override this by setting it to QUESTION_EVENTCLOSE if the
+- // attempt at the question causes the session to close
+- $state->event = $action->event;
+-
+- if (!question_isgradingevent($action->event)) {
+- // Grade the response but don't update the overall grade
+- if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
+- return false;
+- }
+-
+- // Temporary hack because question types are not given enough control over what is going
+- // on. Used by Opaque questions.
+- // TODO fix this code properly.
+- if (!empty($state->believeevent)) {
+- // If the state was graded we need to ...
+- if (question_state_is_graded($state)) {
+- question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
+-
+- // update the attempt grade
+- $attempt->sumgrades -= (float)$state->last_graded->grade;
+- $attempt->sumgrades += (float)$state->grade;
+-
+- // and update the last_graded field.
+- unset($state->last_graded);
+- $state->last_graded = clone($state);
+- unset($state->last_graded->changed);
+- }
+- } else {
+- // Don't allow the processing to change the event type
+- $state->event = $action->event;
+- }
+-
+- } else { // grading event
+-
+- // Unless the attempt is closing, we want to work out if the current responses
+- // (or equivalent responses) were already given in the last graded attempt.
+- if(QUESTION_EVENTCLOSE != $action->event && QUESTION_EVENTOPEN != $state->last_graded->event &&
+- $QTYPES[$question->qtype]->compare_responses($question, $state, $state->last_graded)) {
+- $state->event = QUESTION_EVENTDUPLICATE;
+- }
+-
+- // If we did not find a duplicate or if the attempt is closing, perform grading
+- if ((!$sameresponses and QUESTION_EVENTDUPLICATE != $state->event) or
+- QUESTION_EVENTCLOSE == $action->event) {
+- if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
+- return false;
+- }
+-
+- // Calculate overall grade using correct penalty method
+- question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
+- }
+-
+- // If the state was graded we need to ...
+- if (question_state_is_graded($state)) {
+- // update the attempt grade
+- $attempt->sumgrades -= (float)$state->last_graded->grade;
+- $attempt->sumgrades += (float)$state->grade;
+-
+- // and update the last_graded field.
+- unset($state->last_graded);
+- $state->last_graded = clone($state);
+- unset($state->last_graded->changed);
+- }
++ if ($loadtags && !empty($CFG->usetags)) {
++ require_once($CFG->dirroot . '/tag/lib.php');
++ $question->tags = tag_get_tags_array('question', $question->id);
+ }
+- $attempt->timemodified = $action->timestamp;
+-
+- return true;
++ return $success;
+ }
+
+ /**
+-* Determine if event requires grading
+-*/
+-function question_isgradingevent($event) {
+- return (QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event);
+-}
+-
+-/**
+-* Applies the penalty from the previous graded responses to the raw grade
+-* for the current responses
+-*
+-* The grade for the question in the current state is computed by subtracting the
+-* penalty accumulated over the previous graded responses at the question from the
+-* raw grade. If the timestamp is more than 1 minute beyond the end of the attempt
+-* the grade is set to zero. The ->grade field of the state object is modified to
+-* reflect the new grade but is never allowed to decrease.
+-* @param object $question The question for which the penalty is to be applied.
+-* @param object $state The state for which the grade is to be set from the
+-* raw grade and the cumulative penalty from the last
+-* graded state. The ->grade field is updated by applying
+-* the penalty scheme determined in $cmoptions to the ->raw_grade and
+-* ->last_graded->penalty fields.
+-* @param object $cmoptions The options set by the course module.
+-* The ->penaltyscheme field determines whether penalties
+-* for incorrect earlier responses are subtracted.
+-*/
+-function question_apply_penalty_and_timelimit(&$question, &$state, $attempt, $cmoptions) {
+- // TODO. Quiz dependancy. The fact that the attempt that is passed in here
+- // is from quiz_attempts, and we use things like $cmoptions->timelimit.
+-
+- // deal with penalty
+- if ($cmoptions->penaltyscheme) {
+- $state->grade = $state->raw_grade - $state->sumpenalty;
+- $state->sumpenalty += (float) $state->penalty;
+- } else {
+- $state->grade = $state->raw_grade;
+- }
+-
+- // deal with timelimit
+- if ($cmoptions->timelimit) {
+- // We allow for 5% uncertainty in the following test
+- if ($state->timestamp - $attempt->timestart > $cmoptions->timelimit * 63) {
+- $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
+- if (!has_capability('mod/quiz:ignoretimelimits', get_context_instance(CONTEXT_MODULE, $cm->id),
+- $attempt->userid, false)) {
+- $state->grade = 0;
++ * Updates the question objects with question type specific
++ * information by calling {@link get_question_options()}
++ *
++ * Can be called either with an array of question objects or with a single
++ * question object.
++ *
++ * @param mixed $questions Either an array of question objects to be updated
++ * or just a single question object
++ * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
++ * @return bool Indicates success or failure.
++ */
++function get_question_options(&$questions, $loadtags = false) {
++ if (is_array($questions)) { // deal with an array of questions
++ foreach ($questions as $i => $notused) {
++ if (!_tidy_question($questions[$i], $loadtags)) {
++ return false;
+ }
+ }
++ return true;
++ } else { // deal with single question
++ return _tidy_question($questions, $loadtags);
+ }
+-
+- // deal with closing time
+- if ($cmoptions->timeclose and $state->timestamp > ($cmoptions->timeclose + 60) // allowing 1 minute lateness
+- and !$attempt->preview) { // ignore closing time for previews
+- $state->grade = 0;
+- }
+-
+- // Ensure that the grade does not go down
+- $state->grade = max($state->grade, $state->last_graded->grade);
+ }
+
+ /**
+@@ -1497,168 +804,6 @@ function print_question_icon($question, $return = false) {
+ }
+
+ /**
+-* Returns a html link to the question image if there is one
+-*
+-* @return string The html image tag or the empy string if there is no image.
+-* @param object $question The question object
+-*/
+-function get_question_image($question) {
+-
+- global $CFG;
+- $img = '';
+-
+- if (!$category = get_record('question_categories', 'id', $question->category)){
+- error('invalid category id '.$question->category);
+- }
+- $coursefilesdir = get_filesdir_from_context(get_context_instance_by_id($category->contextid));
+-
+- if ($question->image) {
+-
+- if (substr(strtolower($question->image), 0, 7) == 'http://') {
+- $img .= $question->image;
+-
+- } else {
+- require_once($CFG->libdir .'/filelib.php');
+- $img = get_file_url("$coursefilesdir/{$question->image}");
+- }
+- }
+- return $img;
+-}
+-
+-function question_print_comment_box($question, $state, $attempt, $url) {
+- global $CFG, $QTYPES;
+-
+- $prefix = 'response';
+- $usehtmleditor = can_use_richtext_editor();
+- if (!question_state_is_graded($state) && $QTYPES[$question->qtype]->is_question_manual_graded($question, $attempt->layout)) {
+- $grade = '';
+- } else {
+- $grade = round($state->last_graded->grade, 3);
+- }
+- echo '';
+-
+- if ($usehtmleditor) {
+- use_html_editor();
+- }
+-}
+-
+-/**
+- * Process a manual grading action. That is, use $comment and $grade to update
+- * $state and $attempt. The attempt and the comment text are stored in the
+- * database. $state is only updated in memory, it is up to the call to store
+- * that, if appropriate.
+- *
+- * @param object $question the question
+- * @param object $state the state to be updated.
+- * @param object $attempt the attempt the state belongs to, to be updated.
+- * @param string $comment the new comment from the teacher.
+- * @param mixed $grade the grade the teacher assigned, or '' to not change the grade.
+- * @return mixed true on success, a string error message if a problem is detected
+- * (for example score out of range).
+- */
+-function question_process_comment($question, &$state, &$attempt, $comment, $grade) {
+- $grade = trim($grade);
+- if ($grade < 0 || $grade > $question->maxgrade) {
+- $a = new stdClass;
+- $a->grade = $grade;
+- $a->maxgrade = $question->maxgrade;
+- $a->name = $question->name;
+- return get_string('errormanualgradeoutofrange', 'question', $a);
+- }
+-
+- // Update the comment and save it in the database
+- $comment = trim($comment);
+- $state->manualcomment = $comment;
+- if (!set_field('question_sessions', 'manualcomment', $comment, 'attemptid', $attempt->uniqueid, 'questionid', $question->id)) {
+- return get_string('errorsavingcomment', 'question', $question);
+- }
+-
+- // Update the attempt if the score has changed.
+- if ($grade !== '' && (abs($state->last_graded->grade - $grade) > 0.002 || $state->last_graded->event != QUESTION_EVENTMANUALGRADE)) {
+- $attempt->sumgrades = $attempt->sumgrades - $state->last_graded->grade + $grade;
+- $attempt->timemodified = time();
+- if (!update_record('quiz_attempts', $attempt)) {
+- return get_string('errorupdatingattempt', 'question', $attempt);
+- }
+-
+- // We want to update existing state (rather than creating new one) if it
+- // was itself created by a manual grading event.
+- $state->update = $state->event == QUESTION_EVENTMANUALGRADE;
+-
+- // Update the other parts of the state object.
+- $state->raw_grade = $grade;
+- $state->grade = $grade;
+- $state->penalty = 0;
+- $state->timestamp = time();
+- $state->seq_number++;
+- $state->event = QUESTION_EVENTMANUALGRADE;
+-
+- // Update the last graded state (don't simplify!)
+- unset($state->last_graded);
+- $state->last_graded = clone($state);
+-
+- // We need to indicate that the state has changed in order for it to be saved.
+- $state->changed = 1;
+- }
+-
+- return true;
+-}
+-
+-/**
+-* Construct name prefixes for question form element names
+-*
+-* Construct the name prefix that should be used for example in the
+-* names of form elements created by questions.
+-* This is called by {@link get_question_options()}
+-* to set $question->name_prefix.
+-* This name prefix includes the question id which can be
+-* extracted from it with {@link question_get_id_from_name_prefix()}.
+-*
+-* @return string
+-* @param integer $id The question id
+-*/
+-function question_make_name_prefix($id) {
+- return 'resp' . $id . '_';
+-}
+-
+-/**
+-* Extract question id from the prefix of form element names
+-*
+-* @return integer The question id
+-* @param string $name The name that contains a prefix that was
+-* constructed with {@link question_make_name_prefix()}
+-*/
+-function question_get_id_from_name_prefix($name) {
+- if (!preg_match('/^resp([0-9]+)_/', $name, $matches))
+- return false;
+- return (integer) $matches[1];
+-}
+-
+-/**
+- * Returns the unique id for a new attempt
+- *
+- * Every module can keep their own attempts table with their own sequential ids but
+- * the question code needs to also have a unique id by which to identify all these
+- * attempts. Hence a module, when creating a new attempt, calls this function and
+- * stores the return value in the 'uniqueid' field of its attempts table.
+- */
+-function question_new_attempt_uniqueid($modulename='quiz') {
+- global $CFG;
+- $attempt = new stdClass;
+- $attempt->modulename = $modulename;
+- if (!$id = insert_record('question_attempts', $attempt)) {
+- error('Could not create new entry in question_attempts table');
+- }
+- return $id;
+-}
+-
+-/**
+ * Creates a stamp that uniquely identifies this version of the question
+ *
+ * In future we want this to use a hash of the question data to guarantee that
+@@ -1674,33 +819,8 @@ function question_hash($question) {
+
+ /// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
+ /**
+- * Get the HTML that needs to be included in the head tag when the
+- * questions in $questionlist are printed in the gives states.
+- *
+- * @param array $questionlist a list of questionids of the questions what will appear on this page.
+- * @param array $questions an array of question objects, whose keys are question ids.
+- * Must contain all the questions in $questionlist
+- * @param array $states an array of question state objects, whose keys are question ids.
+- * Must contain the state of all the questions in $questionlist
+- *
+- * @return string some HTML code that can go inside the head tag.
+- */
+-function get_html_head_contributions(&$questionlist, &$questions, &$states) {
+- global $QTYPES;
+-
+- $contributions = array();
+- foreach ($questionlist as $questionid) {
+- $question = $questions[$questionid];
+- $contributions = array_merge($contributions,
+- $QTYPES[$question->qtype]->get_html_head_contributions(
+- $question, $states[$questionid]));
+- }
+- return implode("\n", array_unique($contributions));
+-}
+-
+-/**
+- * Like @see{get_html_head_contributions} but for the editing page
+- * question/question.php.
++ * Get anything that needs to be included in the head of the question editing page
++ * for a particular question type. This function is called by question/question.php.
+ *
+ * @param $question A question object. Only $question->qtype is used.
+ * @return string some HTML code that can go inside the head tag.
+@@ -1712,20 +832,6 @@ function get_editing_head_contributions($question) {
+ }
+
+ /**
+- * Prints a question
+- *
+- * Simply calls the question type specific print_question() method.
+- * @param object $question The question to be rendered.
+- * @param object $state The state to render the question in.
+- * @param integer $number The number for this question.
+- * @param object $cmoptions The options specified by the course module
+- * @param object $options An object specifying the rendering options.
+- */
+-function print_question(&$question, &$state, $number, $cmoptions, $options=null) {
+- global $QTYPES;
+- $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options);
+-}
+-/**
+ * Saves question options
+ *
+ * Simply calls the question type specific save_question_options() method.
+@@ -1736,44 +842,6 @@ function save_question_options($question) {
+ $QTYPES[$question->qtype]->save_question_options($question);
+ }
+
+-/**
+-* Gets all teacher stored answers for a given question
+-*
+-* Simply calls the question type specific get_all_responses() method.
+-*/
+-// ULPGC ecastro
+-function get_question_responses($question, $state) {
+- global $QTYPES;
+- $r = $QTYPES[$question->qtype]->get_all_responses($question, $state);
+- return $r;
+-}
+-
+-
+-/**
+-* Gets the response given by the user in a particular state
+-*
+-* Simply calls the question type specific get_actual_response() method.
+-*/
+-// ULPGC ecastro
+-function get_question_actual_response($question, $state) {
+- global $QTYPES;
+-
+- $r = $QTYPES[$question->qtype]->get_actual_response($question, $state);
+- return $r;
+-}
+-
+-/**
+-* TODO: document this
+-*/
+-// ULPGc ecastro
+-function get_question_fraction_grade($question, $state) {
+- global $QTYPES;
+-
+- $r = $QTYPES[$question->qtype]->get_fractional_grade($question, $state);
+- return $r;
+-}
+-
+-
+ /// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
+
+ /**
+@@ -1796,7 +864,7 @@ function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
+ if ($level == 1) {
+ foreach ($keys as $key) {
+ //If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
+- if (!isset($categories[$key]->processed) && !record_exists('question_categories', 'course', $categories[$key]->course, 'id', $categories[$key]->parent)) {
++ if (!isset($categories[$key]->processed) && !record_exists('question_categories', 'contextid', $categories[$key]->contextid, 'id', $categories[$key]->parent)) {
+ $children[$key] = $categories[$key];
+ $categories[$key]->processed = true;
+ $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
+@@ -2019,6 +1087,7 @@ function question_add_context_in_key($categories){
+ }
+ return $newcatarray;
+ }
++
+ function question_add_tops($categories, $pcontexts){
+ $topcats = array();
+ foreach ($pcontexts as $context){
+@@ -2048,8 +1117,6 @@ function question_categorylist($categoryid) {
+ }
+
+
+-
+-
+ //===========================
+ // Import/Export Functions
+ //===========================
+@@ -2098,7 +1165,6 @@ function get_import_export_formats( $type ) {
+ return $fileformatnames;
+ }
+
+-
+ /**
+ * Create default export filename
+ *
+@@ -2144,6 +1210,7 @@ function default_export_filename($course,$category) {
+
+ return $export_name;
+ }
++
+ class context_to_string_translator{
+ /**
+ * @var array used to translate between contextids and strings for this context.
+@@ -2352,4 +1419,3 @@ function get_filesdir_from_context($context){
+ }
+ return $courseid;
+ }
+-?>
diff --git a/question/type/description/questiontype.php b/question/type/description/questiontype.php
index b79a547a73a..9f632ebe6c2 100644
--- a/question/type/description/questiontype.php
+++ b/question/type/description/questiontype.php
@@ -33,6 +33,10 @@ require_once($CFG->libdir . '/questionlib.php');
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_description extends question_type {
+ function is_real_question_type() {
+ return false;
+ }
+
public function is_usable_by_random() {
return false;
}
diff --git a/question/type/old_questiontype.php b/question/type/old_questiontype.php
index 5aaf4d323d8..3c56eb4844f 100644
--- a/question/type/old_questiontype.php
+++ b/question/type/old_questiontype.php
@@ -192,7 +192,7 @@ class default_questiontype {
/**
* Return an instance of the question editing form definition. This looks for a
* class called edit_{$this->name()}_question_form in the file
- * {$CFG->dirroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
+ * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
* and if it exists returns an instance of it.
*
* @param string $submiturl passed on to the constructor call.
@@ -402,13 +402,13 @@ class default_questiontype {
}
/**
- * Saves question-type specific options
- *
- * This is called by {@link save_question()} to save the question-type specific data
- * @return object $result->error or $result->noticeyesno or $result->notice
- * @param object $question This holds the information from the editing form,
- * it is not a standard question object.
- */
+ * Saves question-type specific options
+ *
+ * This is called by {@link save_question()} to save the question-type specific data
+ * @return object $result->error or $result->noticeyesno or $result->notice
+ * @param object $question This holds the information from the editing form,
+ * it is not a standard question object.
+ */
function save_question_options($question) {
global $DB;
$extra_question_fields = $this->extra_question_fields();
@@ -449,17 +449,17 @@ class default_questiontype {
}
/**
- * Loads the question type specific options for the question.
- *
- * This function loads any question type specific options for the
- * question from the database into the question object. This information
- * is placed in the $question->options field. A question type is
- * free, however, to decide on a internal structure of the options field.
- * @return bool Indicates success or failure.
- * @param object $question The question object for the question. This object
- * should be updated to include the question type
- * specific information (it is passed by reference).
- */
+ * Loads the question type specific options for the question.
+ *
+ * This function loads any question type specific options for the
+ * question from the database into the question object. This information
+ * is placed in the $question->options field. A question type is
+ * free, however, to decide on a internal structure of the options field.
+ * @return bool Indicates success or failure.
+ * @param object $question The question object for the question. This object
+ * should be updated to include the question type
+ * specific information (it is passed by reference).
+ */
function get_question_options(&$question) {
global $CFG, $DB, $OUTPUT;
@@ -880,606 +880,6 @@ class default_questiontype {
}
}
- /**
- * Prints the question including the number, grading details, content,
- * feedback and interactions
- *
- * This function prints the question including the question number,
- * grading details, content for the question, any feedback for the previously
- * submitted responses and the interactions. The default implementation calls
- * various other methods to print each of these parts and most question types
- * will just override those methods.
- * @param object $question The question to be rendered. Question type
- * specific information is included. The
- * maximum possible grade is in ->maxgrade. The name
- * prefix for any named elements is in ->name_prefix.
- * @param object $state The state to render the question in. The grading
- * information is in ->grade, ->raw_grade and
- * ->penalty. The current responses are in
- * ->responses. This is an associative array (or the
- * empty string or null in the case of no responses
- * submitted). The last graded state is in
- * ->last_graded (hence the most recently graded
- * responses are in ->last_graded->responses). The
- * question type specific information is also
- * included.
- * @param integer $number The number for this question.
- * @param object $cmoptions
- * @param object $options An object describing the rendering options.
- */
- function print_question(&$question, &$state, $number, $cmoptions, $options, $context=null) {
- /* The default implementation should work for most question types
- provided the member functions it calls are overridden where required.
- The layout is determined by the template question.html */
-
- global $CFG, $OUTPUT;
-
- $context = $this->get_context_by_category_id($question->category);
- $question->questiontext = quiz_rewrite_question_urls($question->questiontext, 'pluginfile.php', $context->id, 'question', 'questiontext', array($state->attempt, $state->question), $question->id);
-
- $question->generalfeedback = quiz_rewrite_question_urls($question->generalfeedback, 'pluginfile.php', $context->id, 'question', 'generalfeedback', array($state->attempt, $state->question), $question->id);
-
- $isgraded = question_state_is_graded($state->last_graded);
-
- if (isset($question->randomquestionid)) {
- $actualquestionid = $question->randomquestionid;
- } else {
- $actualquestionid = $question->id;
- }
-
- // For editing teachers print a link to an editing popup window
- $editlink = $this->get_question_edit_link($question, $cmoptions, $options);
-
- $generalfeedback = '';
- if ($isgraded && $options->generalfeedback) {
- $generalfeedback = $this->format_text($question->generalfeedback,
- $question->generalfeedbackformat, $cmoptions);
- }
-
- $grade = '';
- if ($question->maxgrade > 0 && $options->scores) {
- if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
- if ($isgraded) {
- $grade = question_format_grade($cmoptions, $state->last_graded->grade).'/';
- } else {
- $grade = '--/';
- }
- }
- $grade .= question_format_grade($cmoptions, $question->maxgrade);
- }
-
- $formatoptions = new stdClass;
- $formatoptions->para = false;
- $comment = format_text($state->manualcomment, $state->manualcommentformat,
- $formatoptions, $cmoptions->course);
- $commentlink = '';
-
- if (!empty($options->questioncommentlink)) {
- $strcomment = get_string('commentorgrade', 'quiz');
-
- $link = new moodle_url("$options->questioncommentlink?attempt=$state->attempt&question=$actualquestionid");
- $action = new popup_action('click', $link, 'commentquestion', array('height' => 480, 'width' => 750));
- $commentlink = $OUTPUT->container($OUTPUT->action_link($link, $strcomment, $action), 'commentlink');
- }
-
- $history = $this->history($question, $state, $number, $cmoptions, $options);
-
- include "$CFG->dirroot/question/type/question.html";
- }
-
- /**
- * Render the question flag, assuming $flagsoption allows it. You will probably
- * never need to override this method.
- *
- * @param object $question the question
- * @param object $state its current state
- * @param integer $flagsoption the option that says whether flags should be displayed.
- */
- protected function print_question_flag($question, $state, $flagsoption) {
- global $CFG, $PAGE;
- switch ($flagsoption) {
- case QUESTION_FLAGSSHOWN:
- $flagcontent = $this->get_question_flag_tag($state->flagged);
- break;
- case QUESTION_FLAGSEDITABLE:
- $id = $question->name_prefix . '_flagged';
- if ($state->flagged) {
- $checked = 'checked="checked" ';
- } else {
- $checked = '';
- }
- $qsid = $state->questionsessionid;
- $aid = $state->attempt;
- $qid = $state->question;
- $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid);
- $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" .
- sesskey() . '&newstate=';
- $flagcontent = '' .
- '' .
- '' . "\n";
- question_init_qengine_js();
- break;
- default:
- $flagcontent = '';
- }
- if ($flagcontent) {
- echo '
' . $flagcontent . "
\n";
- }
- }
-
- /**
- * Work out the actual img tag needed for the flag
- *
- * @param boolean $flagged whether the question is currently flagged.
- * @param string $id an id to be added as an attribute to the img (optional).
- * @return string the img tag.
- */
- protected function get_question_flag_tag($flagged, $id = '') {
- global $OUTPUT;
- if ($id) {
- $id = 'id="' . $id . '" ';
- }
- if ($flagged) {
- $img = 'i/flagged';
- } else {
- $img = 'i/unflagged';
- }
- return 'pix_url($img) .
- '" alt="' . get_string('flagthisquestion', 'question') . '" />';
- }
-
- /**
- * Get a link to an edit icon for this question, if the current user is allowed
- * to edit it.
- *
- * @param object $question the question object.
- * @param object $cmoptions the options from the module. If $cmoptions->thispageurl is set
- * then the link will be to edit the question in this browser window, then return to
- * $cmoptions->thispageurl. Otherwise the link will be to edit in a popup.
- * @return string the HTML of the link, or nothing it the currenty user is not allowed to edit.
- */
- function get_question_edit_link($question, $cmoptions, $options) {
- global $CFG, $OUTPUT;
-
- /// Is this user allowed to edit this question?
- if (!empty($options->noeditlink) || !question_has_capability_on($question, 'edit')) {
- return '';
- }
-
- /// Work out the right URL.
- $url = new moodle_url('/question/question.php', array('id' => $question->id));
- if (!empty($cmoptions->cmid)) {
- $url->param('cmid', $cmoptions->cmid);
- } else if (!empty($cmoptions->course)) {
- $url->param('courseid', $cmoptions->course);
- } else {
- print_error('missingcourseorcmidtolink', 'question');
- }
-
- $icon = new pix_icon('t/edit', get_string('edit'));
-
- $action = null;
- if (!empty($cmoptions->thispageurl)) {
- // The module allow editing in the same window, print an ordinary
- // link with a returnurl.
- $url->param('returnurl', $cmoptions->thispageurl);
- } else {
- // We have to edit in a pop-up.
- $url->param('inpopup', 1);
- $action = new popup_action('click', $link, 'editquestion');
- }
-
- return $OUTPUT->action_icon($url, $icon, $action);
- }
-
- /**
- * Print history of responses
- *
- * Used by print_question()
- */
- function history($question, $state, $number, $cmoptions, $options) {
- global $DB, $OUTPUT;
-
- if (empty($options->history)) {
- return '';
- }
-
- $params = array('aid' => $state->attempt);
- if (isset($question->randomquestionid)) {
- $params['qid'] = $question->randomquestionid;
- $randomprefix = 'random' . $question->id . '-';
- } else {
- $params['qid'] = $question->id;
- $randomprefix = '';
- }
- if ($options->history == 'all') {
- $eventtest = 'event > 0';
- } else {
- $eventtest = 'event IN (' . QUESTION_EVENTS_GRADED . ')';
- }
- $states = $DB->get_records_select('question_states',
- 'attempt = :aid AND question = :qid AND ' . $eventtest, $params, 'seq_number ASC');
- if (count($states) <= 1) {
- return '';
- }
-
- $strreviewquestion = get_string('reviewresponse', 'quiz');
- $table = new html_table();
- $table->width = '100%';
- $table->head = array (
- get_string('numberabbr', 'quiz'),
- get_string('action', 'quiz'),
- get_string('response', 'quiz'),
- get_string('time'),
- );
- if ($options->scores) {
- $table->head[] = get_string('score', 'quiz');
- $table->head[] = get_string('grade', 'quiz');
- }
-
- foreach ($states as $st) {
- if ($randomprefix && strpos($st->answer, $randomprefix) === 0) {
- $st->answer = substr($st->answer, strlen($randomprefix));
- }
- $st->responses[''] = $st->answer;
- $this->restore_session_and_responses($question, $st);
-
- if ($state->id == $st->id) {
- $link = '' . $st->seq_number . '';
- } else if (isset($options->questionreviewlink)) {
- $reviewlink = new moodle_url($options->questionreviewlink);
- $reviewlink->params(array('state'=>$st->id,'question'=>$question->id));
- $link = new moodle_url($reviewlink);
- $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
- $link = $OUTPUT->action_link($link, $st->seq_number, $action, array('title'=>$strreviewquestion));
- } else {
- $link = $st->seq_number;
- }
-
- if ($state->id == $st->id) {
- $b = '';
- $be = '';
- } else {
- $b = '';
- $be = '';
- }
-
- $data = array (
- $link,
- $b.get_string('event'.$st->event, 'quiz').$be,
- $b.$this->response_summary($question, $st).$be,
- $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
- );
- if ($options->scores) {
- $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
- $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
- }
- $table->data[] = $data;
- }
- return html_writer::table($table);
- }
-
- /**
- * Prints the score obtained and maximum score available plus any penalty
- * information
- *
- * This function prints a summary of the scoring in the most recently
- * graded state (the question may not have been submitted for marking at
- * the current state). The default implementation should be suitable for most
- * question types.
- * @param object $question The question for which the grading details are
- * to be rendered. Question type specific information
- * is included. The maximum possible grade is in
- * ->maxgrade.
- * @param object $state The state. In particular the grading information
- * is in ->grade, ->raw_grade and ->penalty.
- * @param object $cmoptions
- * @param object $options An object describing the rendering options.
- */
- function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
- /* The default implementation prints the number of marks if no attempt
- has been made. Otherwise it displays the grade obtained out of the
- maximum grade available and a warning if a penalty was applied for the
- attempt and displays the overall grade obtained counting all previous
- responses (and penalties) */
-
- if (QUESTION_EVENTDUPLICATE == $state->event) {
- echo ' ';
- print_string('duplicateresponse', 'quiz');
- }
- if ($question->maxgrade > 0 && $options->scores) {
- if (question_state_is_graded($state->last_graded)) {
- // Display the grading details from the last graded state
- $grade = new stdClass;
- $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade);
- $grade->max = question_format_grade($cmoptions, $question->maxgrade);
- $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade);
-
- // let student know wether the answer was correct
- $class = question_get_feedback_class($state->last_graded->raw_grade /
- $question->maxgrade);
- echo '
' . get_string($class, 'quiz') . '
';
-
- echo '
';
- // print grade for this submission
- print_string('gradingdetails', 'quiz', $grade);
- if ($cmoptions->penaltyscheme) {
- // print details of grade adjustment due to penalties
- if ($state->last_graded->raw_grade > $state->last_graded->grade){
- echo ' ';
- print_string('gradingdetailsadjustment', 'quiz', $grade);
- }
- // print info about new penalty
- // penalty is relevant only if the answer is not correct and further attempts are possible
- if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
- and (QUESTION_EVENTCLOSEANDGRADE != $state->event)) {
-
- if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
- // A penalty was applied so display it
- echo ' ';
- print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty));
- } else {
- /* No penalty was applied even though the answer was
- not correct (eg. a syntax error) so tell the student
- that they were not penalised for the attempt */
- echo ' ';
- print_string('gradingdetailszeropenalty', 'quiz');
- }
- }
- }
- echo '
';
- }
- }
- }
-
- /**
- * Prints the main content of the question including any interactions
- *
- * This function prints the main content of the question including the
- * interactions for the question in the state given. The last graded responses
- * are printed or indicated and the current responses are selected or filled in.
- * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
- * This method is called from the print_question method.
- * @param object $question The question to be rendered. Question type
- * specific information is included. The name
- * prefix for any named elements is in ->name_prefix.
- * @param object $state The state to render the question in. The grading
- * information is in ->grade, ->raw_grade and
- * ->penalty. The current responses are in
- * ->responses. This is an associative array (or the
- * empty string or null in the case of no responses
- * submitted). The last graded state is in
- * ->last_graded (hence the most recently graded
- * responses are in ->last_graded->responses). The
- * question type specific information is also
- * included.
- * The state is passed by reference because some adaptive
- * questions may want to update it during rendering
- * @param object $cmoptions
- * @param object $options An object describing the rendering options.
- */
- function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
- /* This default implementation prints an error and must be overridden
- by all question type implementations, unless the default implementation
- of print_question has been overridden. */
- global $OUTPUT;
- echo $OUTPUT->notification('Error: Question formulation and input controls has not'
- .' been implemented for question type '.$this->name());
- }
-
- function check_file_access($question, $state, $options, $contextid, $component,
- $filearea, $args) {
-
- if ($component == 'question' && $filearea == 'questiontext') {
- // Question text always visible.
- return true;
-
- } else if ($component == 'question' && $filearea = 'generalfeedback') {
- return $options->generalfeedback && question_state_is_graded($state->last_graded);
-
- } else {
- // Unrecognised component or filearea.
- return false;
- }
- }
-
- /**
- * Prints the submit button(s) for the question in the given state
- *
- * This function prints the submit button(s) for the question in the
- * given state. The name of any button created will be prefixed with the
- * unique prefix for the question in $question->name_prefix. The suffix
- * 'submit' is reserved for the single question submit button and the suffix
- * 'validate' is reserved for the single question validate button (for
- * question types which support it). Other suffixes will result in a response
- * of that name in $state->responses which the printing and grading methods
- * can then use.
- * @param object $question The question for which the submit button(s) are to
- * be rendered. Question type specific information is
- * included. The name prefix for any
- * named elements is in ->name_prefix.
- * @param object $state The state to render the buttons for. The
- * question type specific information is also
- * included.
- * @param object $cmoptions
- * @param object $options An object describing the rendering options.
- */
- function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
- // The default implementation should be suitable for most question types.
- // It prints a mark button in the case where individual marking is allowed.
- if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
- echo '';
- }
- }
-
- /**
- * Return a summary of the student response
- *
- * This function returns a short string of no more than a given length that
- * summarizes the student's response in the given $state. This is used for
- * example in the response history table. This string should already be
- * formatted for output.
- * @return string The summary of the student response
- * @param object $question
- * @param object $state The state whose responses are to be summarized
- * @param int $length The maximum length of the returned string
- */
- function response_summary($question, $state, $length = 80, $formatting = true) {
- // This should almost certainly be overridden
- $responses = $this->get_actual_response($question, $state);
- if ($formatting){
- $responses = $this->format_responses($responses, $question->questiontextformat);
- }
- $responses = implode('; ', $responses);
- return shorten_text($responses, $length);
- }
- /**
- * @param array responses is an array of responses.
- * @return formatted responses
- */
- function format_responses($responses, $format){
- $toreturn = array();
- foreach ($responses as $response){
- $toreturn[] = $this->format_response($response, $format);
- }
- return $toreturn;
- }
- /**
- * @param string response is a response.
- * @return formatted response
- */
- function format_response($response, $format){
- return s($response);
- }
- /**
- * Renders the question for printing and returns the LaTeX source produced
- *
- * This function should render the question suitable for a printed problem
- * or solution sheet in LaTeX and return the rendered output.
- * @return string The LaTeX output.
- * @param object $question The question to be rendered. Question type
- * specific information is included.
- * @param object $state The state to render the question in. The
- * question type specific information is also
- * included.
- * @param object $cmoptions
- * @param string $type Indicates if the question or the solution is to be
- * rendered with the values 'question' and
- * 'solution'.
- */
- function get_texsource(&$question, &$state, $cmoptions, $type) {
- // The default implementation simply returns a string stating that
- // the question is only available online.
-
- return get_string('onlineonly', 'texsheet');
- }
-
- /**
- * Compares two question states for equivalence of the student's responses
- *
- * The responses for the two states must be examined to see if they represent
- * equivalent answers to the question by the student. This method will be
- * invoked for each of the previous states of the question before grading
- * occurs. If the student is found to have already attempted the question
- * with equivalent responses then the attempt at the question is ignored;
- * grading does not occur and the state does not change. Thus they are not
- * penalized for this case.
- * @return boolean
- * @param object $question The question for which the states are to be
- * compared. Question type specific information is
- * included.
- * @param object $state The state of the question. The responses are in
- * ->responses. This is the only field of $state
- * that it is safe to use.
- * @param object $teststate The state whose responses are to be
- * compared. The state will be of the same age or
- * older than $state. If possible, the method should
- * only use the field $teststate->responses, however
- * any field that is set up by restore_session_and_responses
- * can be used.
- */
- function compare_responses(&$question, $state, $teststate) {
- // The default implementation performs a comparison of the response
- // arrays. The ordering of the arrays does not matter.
- // Question types may wish to override this (eg. to ignore trailing
- // white space or to make "7.0" and "7" compare equal).
-
- // In php neither == nor === compare arrays the way you want. The following
- // ensures that the arrays have the same keys, with the same values.
- $result = false;
- $diff1 = array_diff_assoc($state->responses, $teststate->responses);
- if (empty($diff1)) {
- $diff2 = array_diff_assoc($teststate->responses, $state->responses);
- $result = empty($diff2);
- }
-
- return $result;
- }
-
- /**
- * Checks whether a response matches a given answer
- *
- * This method only applies to questions that use teacher-defined answers
- *
- * @return boolean
- */
- function test_response(&$question, &$state, $answer) {
- $response = isset($state->responses['']) ? $state->responses[''] : '';
- return ($response == $answer->answer);
- }
-
- /**
- * Performs response processing and grading
- *
- * This function performs response processing and grading and updates
- * the state accordingly.
- * @return boolean Indicates success or failure.
- * @param object $question The question to be graded. Question type
- * specific information is included.
- * @param object $state The state of the question to grade. The current
- * responses are in ->responses. The last graded state
- * is in ->last_graded (hence the most recently graded
- * responses are in ->last_graded->responses). The
- * question type specific information is also
- * included. The ->raw_grade and ->penalty fields
- * must be updated. The method is able to
- * close the question session (preventing any further
- * attempts at this question) by setting
- * $state->event to QUESTION_EVENTCLOSEANDGRADE
- * @param object $cmoptions
- */
- function grade_responses(&$question, &$state, $cmoptions) {
- // The default implementation uses the test_response method to
- // compare what the student entered against each of the possible
- // answers stored in the question, and uses the grade from the
- // first one that matches. It also sets the marks and penalty.
- // This should be good enought for most simple question types.
-
- $state->raw_grade = 0;
- foreach($question->options->answers as $answer) {
- if($this->test_response($question, $state, $answer)) {
- $state->raw_grade = $answer->fraction;
- break;
- }
- }
-
- // Make sure we don't assign negative or too high marks.
- $state->raw_grade = min(max((float) $state->raw_grade,
- 0.0), 1.0) * $question->maxgrade;
-
- // Update the penalty.
- $state->penalty = $question->penalty * $question->maxgrade;
-
- // mark the state as graded
- $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
-
- return true;
- }
-
/**
* Returns true if the editing wizard is finished, false otherwise.
*
diff --git a/question/type/questiontype.php b/question/type/questiontype.php
index d711416650d..cd2be0d9099 100644
--- a/question/type/questiontype.php
+++ b/question/type/questiontype.php
@@ -1,4 +1,20 @@
-.
+
/**
* The default questiontype class.
*
@@ -11,14 +27,10 @@
* @subpackage questiontypes
*/
+
require_once($CFG->dirroot . '/question/engine/lib.php');
-// DONOTCOMMIT
-class default_questiontype {
- function plugin_dir() {
- return '';
- }
-}
+
/**
* This is the base class for Moodle question types.
*
@@ -36,6 +48,11 @@ class default_questiontype {
* @subpackage questiontypes
*/
class question_type {
+ protected $fileoptions = array(
+ 'subdirs' => false,
+ 'maxfiles' => -1,
+ 'maxbytes' => 0,
+ );
public function __construct() {
}
@@ -47,15 +64,54 @@ class question_type {
return substr(get_class($this), 6);
}
+ /**
+ * @return string the full frankenstyle name for this plugin.
+ */
+ public function plugin_name() {
+ return get_class($this);
+ }
+
+ /**
+ * @return string the name of this question type in the user's language.
+ * You should not need to override this method, the default behaviour should be fine.
+ */
+ public function local_name() {
+ return get_string($this->name(), $this->plugin_name());
+ }
+
/**
* The name this question should appear as in the create new question
- * dropdown.
+ * dropdown. Override this method to return false if you don't want your
+ * question type to be createable, for example if it is an abstract base type,
+ * otherwise, you should not need to override this method.
*
* @return mixed the desired string, or false to hide this question type in the menu.
*/
public function menu_name() {
- $name = $this->name();
- return get_string($name, 'qtype_' . $name);
+ return $this->local_name();
+ }
+
+ /**
+ * Returns a list of other question types that this one requires in order to
+ * work. For example, the calculated question type is a subclass of the
+ * numerical question type, which is a subclass of the shortanswer question
+ * type; and the randomsamatch question type requires the shortanswer type
+ * to be installed.
+ *
+ * @return array any other question types that this one relies on. An empty
+ * array if none.
+ */
+ public function requires_qtypes() {
+ return array();
+ }
+
+ /**
+ * @return boolean override this to return false if this is not really a
+ * question type, for example the description question type is not
+ * really a question type.
+ */
+ public function is_real_question_type() {
+ return true;
}
/**
@@ -139,7 +195,7 @@ class question_type {
/**
* Return an instance of the question editing form definition. This looks for a
* class called edit_{$this->name()}_question_form in the file
- * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
+ * question/type/{$this->name()}/edit_{$this->name()}_question_form.php
* and if it exists returns an instance of it.
*
* @param string $submiturl passed on to the constructor call.
@@ -184,9 +240,12 @@ class question_type {
* @param object $question
* @param string $wizardnow is '' for first page.
*/
- public function display_question_editing_page(&$mform, $question, $wizardnow) {
- $name = $this->name();
- print_heading_with_help($this->get_heading(empty($question->id)), $name, 'qtype_' . $name);
+ public function display_question_editing_page($mform, $question, $wizardnow) {
+ global $OUTPUT;
+ $heading = $this->get_heading(empty($question->id));
+
+ echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
+
$permissionstrs = array();
if (!empty($question->id)){
if ($question->formoptions->canedit){
@@ -200,13 +259,13 @@ class question_type {
}
}
if (!$question->formoptions->movecontext && count($permissionstrs)){
- print_heading(get_string('permissionto', 'question'), 'center', 3);
+ echo $OUTPUT->heading(get_string('permissionto', 'question'), 3);
$html = '
';
foreach ($permissionstrs as $permissionstr){
$html .= '
'.$permissionstr.'
';
}
$html .= '
';
- print_box($html, 'boxwidthnarrow boxaligncenter generalbox');
+ echo $OUTPUT->box($html, 'boxwidthnarrow boxaligncenter generalbox');
}
$mform->display();
}
@@ -217,13 +276,12 @@ class question_type {
* @return string the heading
*/
public function get_heading($adding = false){
- $name = $this->name();
- if ($adding){
+ if ($adding) {
$action = 'adding';
} else {
$action = 'editing';
}
- return get_string($action . $name, 'qtype_' . $name);
+ return get_string($action . $this->name(), $this->plugin_name());
}
/**
@@ -261,34 +319,37 @@ class question_type {
* redisplayed with validation errors, from validation_errors field, which
* is itself an object, shown next to the form fields. (I don't think this is accurate any more.)
*/
- public function save_question($question, $form, $course) {
- global $USER;
+ function save_question($question, $form) {
+ global $USER, $DB, $OUTPUT;
+
+ list($question->category) = explode(',', $form->category);
+ $context = $this->get_context_by_category_id($question->category);
// This default implementation is suitable for most
// question types.
// First, save the basic question itself
$question->name = trim($form->name);
- $question->questiontext = trim($form->questiontext);
- $question->questiontextformat = $form->questiontextformat;
$question->parent = isset($form->parent) ? $form->parent : 0;
$question->length = $this->actual_number_of_questions($question);
$question->penalty = isset($form->penalty) ? $form->penalty : 0;
- if (empty($form->image)) {
- $question->image = '';
+ if (empty($form->questiontext['text'])) {
+ $question->questiontext = '';
} else {
- $question->image = $form->image;
+ $question->questiontext = trim($form->questiontext['text']);;
}
+ $question->questiontextformat = !empty($form->questiontext['format'])?$form->questiontext['format']:0;
- if (empty($form->generalfeedback)) {
+ if (empty($form->generalfeedback['text'])) {
$question->generalfeedback = '';
} else {
- $question->generalfeedback = trim($form->generalfeedback);
+ $question->generalfeedback = trim($form->generalfeedback['text']);
}
+ $question->generalfeedbackformat = !empty($form->generalfeedback['format'])?$form->generalfeedback['format']:0;
if (empty($question->name)) {
- $question->name = shorten_text(strip_tags($question->questiontext), 15);
+ $question->name = shorten_text(strip_tags($form->questiontext['text']), 15);
if (empty($question->name)) {
$question->name = '-';
}
@@ -302,39 +363,42 @@ class question_type {
$question->defaultmark = $form->defaultmark;
}
- list($question->category) = explode(',', $form->category);
-
- if (!empty($question->id)) {
- /// Question already exists, update.
- $question->modifiedby = $USER->id;
- $question->timemodified = time();
- if (!update_record('question', $question)) {
- error('Could not update question!');
- }
-
- } else {
- /// New question.
+ // If the question is new, create it.
+ if (empty($question->id)) {
// Set the unique code
$question->stamp = make_unique_id_code();
$question->createdby = $USER->id;
- $question->modifiedby = $USER->id;
$question->timecreated = time();
- $question->timemodified = time();
- if (!$question->id = insert_record('question', $question)) {
- error('Could not insert new question!');
- }
+ $question->id = $DB->insert_record('question', $question);
}
+ // Now, whether we are updating a existing question, or creating a new
+ // one, we have to do the files processing and update the record.
+ /// Question already exists, update.
+ $question->modifiedby = $USER->id;
+ $question->timemodified = time();
+
+ if (!empty($question->questiontext) && !empty($form->questiontext['itemid'])) {
+ $question->questiontext = file_save_draft_area_files($form->questiontext['itemid'], $context->id, 'question', 'questiontext', (int)$question->id, $this->fileoptions, $question->questiontext);
+ }
+ if (!empty($question->generalfeedback) && !empty($form->generalfeedback['itemid'])) {
+ $question->generalfeedback = file_save_draft_area_files($form->generalfeedback['itemid'], $context->id, 'question', 'generalfeedback', (int)$question->id, $this->fileoptions, $question->generalfeedback);
+ }
+ $DB->update_record('question', $question);
+
// Now to save all the answers and type-specific options
$form->id = $question->id;
$form->qtype = $question->qtype;
$form->category = $question->category;
$form->questiontext = $question->questiontext;
+ $form->questiontextformat = $question->questiontextformat;
+ // current context
+ $form->context = $context;
$result = $this->save_question_options($form);
if (!empty($result->error)) {
- error($result->error);
+ print_error($result->error);
}
if (!empty($result->notice)) {
@@ -342,16 +406,11 @@ class question_type {
}
if (!empty($result->noticeyesno)) {
- notice_yesno($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}",
- "edit.php?courseid={$course->id}");
- print_footer($course);
- exit;
+ throw new coding_exception('$result->noticeyesno no longer supported in save_question.');
}
// Give the question a unique version stamp determined by question_hash()
- if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) {
- error('Could not update question version field');
- }
+ $DB->set_field('question', 'version', question_hash($question), array('id' => $question->id));
return $question;
}
@@ -365,6 +424,7 @@ class question_type {
* it is not a standard question object.
*/
public function save_question_options($question) {
+ global $DB;
$extra_question_fields = $this->extra_question_fields();
if (is_array($extra_question_fields)) {
@@ -372,7 +432,7 @@ class question_type {
$function = 'update_record';
$questionidcolname = $this->questionid_column_name();
- $options = get_record($question_extension_table, $questionidcolname, $question->id);
+ $options = $DB->get_record($question_extension_table, array($questionidcolname => $question->id));
if (!$options) {
$function = 'insert_record';
$options = new stdClass;
@@ -388,7 +448,7 @@ class question_type {
$options->$field = $question->$field;
}
- if (!$function($question_extension_table, $options)) {
+ if (!$DB->{$function}($question_extension_table, $options)) {
$result = new stdClass;
$result->error = 'Could not save question options for ' .
$this->name() . ' question id ' . $question->id;
@@ -403,7 +463,8 @@ class question_type {
}
public function save_hints($formdata, $withparts = false) {
- delete_records('question_hints', 'questionid', $formdata->id);
+ global $DB;
+ $DB->delete_records('question_hints', array('questionid' => $formdata->id));
if (!empty($formdata->hint)) {
$numhints = max(array_keys($formdata->hint)) + 1;
@@ -443,7 +504,7 @@ class question_type {
continue;
}
- insert_record('question_hints', $hint);
+ $DB->insert_record('question_hints', $hint);
}
}
@@ -460,18 +521,22 @@ class question_type {
* specific information (it is passed by reference).
*/
public function get_question_options($question) {
- global $CFG;
+ global $CFG, $DB, $OUTPUT;
+
+ if (!isset($question->options)) {
+ $question->options = new stdClass();
+ }
$extra_question_fields = $this->extra_question_fields();
if (is_array($extra_question_fields)) {
$question_extension_table = array_shift($extra_question_fields);
- $extra_data = get_record($question_extension_table, $this->questionid_column_name(), $question->id, '', '', '', '', implode(', ', $extra_question_fields));
+ $extra_data = $DB->get_record($question_extension_table, array($this->questionid_column_name() => $question->id), implode(', ', $extra_question_fields));
if ($extra_data) {
foreach ($extra_question_fields as $field) {
$question->options->$field = $extra_data->$field;
}
} else {
- notify("Failed to load question options from the table $question_extension_table for questionid " .
+ echo $OUTPUT->notification("Failed to load question options from the table $question_extension_table for questionid " .
$question->id);
return false;
}
@@ -480,21 +545,21 @@ class question_type {
$extra_answer_fields = $this->extra_answer_fields();
if (is_array($extra_answer_fields)) {
$answer_extension_table = array_shift($extra_answer_fields);
- $question->options->answers = get_records_sql('
- SELECT qa.*, qax.' . implode(', qax.', $extra_answer_fields) . '
- FROM ' . $CFG->prefix . 'question_answers qa, ' . $CFG->prefix . '$answer_extension_table qax
- WHERE qa.questionid = ' . $question->id . ' AND qax.answerid = qa.id');
+ $question->options->answers = $DB->get_records_sql("
+ SELECT qa.*, qax." . implode(', qax.', $extra_answer_fields) . "
+ FROM {question_answers} qa, {$answer_extension_table} qax
+ WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id));
if (!$question->options->answers) {
- notify("Failed to load question answers from the table $answer_extension_table for questionid " .
+ echo $OUTPUT->notification("Failed to load question answers from the table $answer_extension_table for questionid " .
$question->id);
return false;
}
} else {
// Don't check for success or failure because some question types do not use the answers table.
- $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC');
+ $question->options->answers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC');
}
- $question->hints = get_records('question_hints', 'questionid', $question->id, 'id ASC');
+ $question->hints = $DB->get_records('question_hints', array('questionid' => $question->id), 'id ASC');
return true;
}
@@ -592,34 +657,32 @@ class question_type {
}
/**
- * Deletes a question from the question-type specific tables
- *
- * @return boolean Success/Failure
- * @param object $question The question being deleted
- */
- public function delete_question($questionid) {
- global $CFG;
- $success = true;
+ * Deletes the question-type specific data when a question is deleted.
+ * @param integer $question the question being deleted.
+ * @param integer $contextid the context this quesiotn belongs to.
+ */
+ public function delete_question($questionid, $contextid) {
+ global $DB;
+
+ $this->delete_files($questionid, $contextid);
$extra_question_fields = $this->extra_question_fields();
if (is_array($extra_question_fields)) {
$question_extension_table = array_shift($extra_question_fields);
- $success = $success && delete_records($question_extension_table,
- $this->questionid_column_name(), $questionid);
+ $DB->delete_records($question_extension_table,
+ array($this->questionid_column_name() => $questionid));
}
$extra_answer_fields = $this->extra_answer_fields();
if (is_array($extra_answer_fields)) {
$answer_extension_table = array_shift($extra_answer_fields);
- $success = $success && delete_records_select($answer_extension_table,
- "answerid IN (SELECT qa.id FROM {$CFG->prefix}question_answers qa WHERE qa.question = $questionid)");
+ $DB->delete_records_select($answer_extension_table,
+ "answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)", array($questionid));
}
- $success = $success && delete_records('question_answers', 'question', $questionid);
+ $DB->delete_records('question_answers', array('question' => $questionid));
- $success = $success && delete_records('question_hints', 'questionid', $questionid);
-
- return $success;
+ $DB->delete_records('question_hints', array('questionid' => $questionid));
}
/**
@@ -681,53 +744,35 @@ class question_type {
}
/**
- * Return any CSS JavaScript required on the head of the question editing
- * page question/question.php.
- *
- * @return an array of bits of HTML to add to the head of pages where
- * this question is displayed in the body. The array should use
- * integer array keys, which have no significance.
+ * Like @see{get_html_head_contributions}, but this method is for CSS and
+ * JavaScript required on the question editing page question/question.php.
*/
public function get_editing_head_contributions() {
// By default, we link to any of the files styles.css, styles.php,
// script.js or script.php that exist in the plugin folder.
// Core question types should not use this mechanism. Their styles
// should be included in the standard theme.
- return $this->find_standard_scripts_and_css();
+ $this->find_standard_scripts();
}
/**
- * Utility method used by @see{get_editing_head_contributions} and
+ * Utility method used by @see{get_html_head_contributions} and
* @see{get_editing_head_contributions}. This looks for any of the files
- * styles.css, styles.php, script.js or script.php that exist in the plugin
- * folder and ensures they get included.
- *
- * @return array as required by get_editing_head_contributions.
+ * script.js or script.php that exist in the plugin folder and ensures they
+ * get included.
*/
- public function find_standard_scripts_and_css() {
+ protected function find_standard_scripts() {
+ global $PAGE;
+
$plugindir = $this->plugin_dir();
- $baseurl = $this->plugin_baseurl();
+ $plugindirrel = 'question/type/' . $this->name();
if (file_exists($plugindir . '/script.js')) {
- require_js($baseurl . '/script.js');
+ $PAGE->requires->js('/' . $plugindirrel . '/script.js');
}
if (file_exists($plugindir . '/script.php')) {
- require_js($baseurl . '/script.php');
+ $PAGE->requires->js('/' . $plugindirrel . '/script.php');
}
-
- $stylesheets = array();
- if (file_exists($plugindir . '/styles.css')) {
- $stylesheets[] = 'styles.css';
- }
- if (file_exists($plugindir . '/styles.php')) {
- $stylesheets[] = 'styles.php';
- }
- $contributions = array();
- foreach ($stylesheets as $stylesheet) {
- $contributions[] = '';
- }
- return $contributions;
}
/**
@@ -746,223 +791,11 @@ class question_type {
*
* @return boolean Whether the wizard's last page was submitted or not.
*/
- public function finished_edit_wizard(&$form) {
+ public function finished_edit_wizard($form) {
//In the default case there is only one edit page.
return true;
}
- /*
- * Find all course / site files linked from a question.
- *
- * Need to check for links to files in question_answers.answer and feedback
- * and in question table in generalfeedback and questiontext fields. Methods
- * on child classes will also check extra question specific fields.
- *
- * Needs to be overriden for child classes that have extra fields containing
- * html.
- *
- * @param string html the html to search
- * @param int courseid search for files for courseid course or set to siteid for
- * finding site files.
- * @return array of url, relative url is key and array with one item = question id as value
- * relative url is relative to course/site files directory root.
- */
- public function find_file_links($question, $courseid){
- $urls = array();
-
- /// Question image
- if ($question->image != ''){
- if (substr(strtolower($question->image), 0, 7) == 'http://') {
- $matches = array();
-
- //support for older questions where we have a complete url in image field
- if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
- if ($cleanedurl = question_url_check($urls[$matches[2]])){
- $urls[$cleanedurl] = null;
- }
- }
- } else {
- if ($question->image != ''){
- if ($cleanedurl = question_url_check($question->image)){
- $urls[$cleanedurl] = null;//will be set later
- }
- }
-
- }
-
- }
-
- /// Questiontext and general feedback.
- $urls += question_find_file_links_from_html($question->questiontext, $courseid);
- $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
-
- /// Answers, if this question uses them.
- if (isset($question->options->answers)){
- foreach ($question->options->answers as $answerkey => $answer){
- /// URLs in the answers themselves, if appropriate.
- if ($this->has_html_answers()) {
- $urls += question_find_file_links_from_html($answer->answer, $courseid);
- }
- /// URLs in the answer feedback.
- $urls += question_find_file_links_from_html($answer->feedback, $courseid);
- }
- }
-
- /// Set all the values of the array to the question object
- if ($urls){
- $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
- }
- return $urls;
- }
-
- /*
- * Find all course / site files linked from a question.
- *
- * Need to check for links to files in question_answers.answer and feedback
- * and in question table in generalfeedback and questiontext fields. Methods
- * on child classes will also check extra question specific fields.
- *
- * Needs to be overriden for child classes that have extra fields containing
- * html.
- *
- * @param string html the html to search
- * @param int course search for files for courseid course or set to siteid for
- * finding site files.
- * @return array of files, file name is key and array with one item = question id as value
- */
- public function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
- global $CFG;
- $updateqrec = false;
-
- /// Question image
- if (!empty($question->image)){
- //support for older questions where we have a complete url in image field
- if (substr(strtolower($question->image), 0, 7) == 'http://') {
- $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
- } else {
- $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
- }
- if ($questionimage != $question->image){
- $question->image = $questionimage;
- $updateqrec = true;
- }
- }
-
- /// Questiontext and general feedback.
- $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
- $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
-
- /// If anything has changed, update it in the database.
- if ($updateqrec){
- if (!update_record('question', addslashes_recursive($question))){
- error ('Couldn\'t update question '.$question->name);
- }
- }
-
-
- /// Answers, if this question uses them.
- if (isset($question->options->answers)){
- //answers that do not need updating have been unset
- foreach ($question->options->answers as $answer){
- $answerchanged = false;
- /// URLs in the answers themselves, if appropriate.
- if ($this->has_html_answers()) {
- $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
- }
- /// URLs in the answer feedback.
- $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
- /// If anything has changed, update it in the database.
- if ($answerchanged){
- if (!update_record('question_answers', addslashes_recursive($answer))){
- error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id);
- }
- }
- }
- }
- }
-
-/// BACKUP FUNCTIONS ////////////////////////////
-
- /*
- * Backup the data in the question
- *
- * This is used in question/backuplib.php
- */
- public function backup($bf,$preferences,$question,$level=6) {
-
- $status = true;
- $extraquestionfields = $this->extra_question_fields();
-
- if (is_array($extraquestionfields)) {
- $questionextensiontable = array_shift($extraquestionfields);
- $record = get_record($questionextensiontable, $this->questionid_column_name(), $question);
- if ($record) {
- $tagname = strtoupper($this->name());
- $status = $status && fwrite($bf, start_tag($tagname, $level, true));
- foreach ($extraquestionfields as $field) {
- if (!isset($record->$field)) {
- echo "No data for field $field when backuping " .
- $this->name() . ' question id ' . $question;
- return false;
- }
- fwrite($bf, full_tag(strtoupper($field), $level + 1, false, $record->$field));
- }
- $status = $status && fwrite($bf, end_tag($tagname, $level, true));
- }
- }
-
- $extraasnwersfields = $this->extra_answer_fields();
- if (is_array($extraasnwersfields)) {
- //TODO backup the answers, with any extra data.
- } else {
- $status = $status && question_backup_answers($bf, $preferences, $question);
- }
- return $status;
- }
-
-/// RESTORE FUNCTIONS /////////////////
-
- /*
- * Restores the data in the question
- *
- * This is used in question/restorelib.php
- */
- public function restore($old_question_id,$new_question_id,$info,$restore) {
-
- $status = true;
- $extraquestionfields = $this->extra_question_fields();
-
- if (is_array($extraquestionfields)) {
- $questionextensiontable = array_shift($extraquestionfields);
- $tagname = strtoupper($this->name());
- $recordinfo = $info['#'][$tagname][0];
-
- $record = new stdClass;
- $qidcolname = $this->questionid_column_name();
- $record->$qidcolname = $new_question_id;
- foreach ($extraquestionfields as $field) {
- $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']);
- }
- if (!insert_record($questionextensiontable, $record)) {
- echo "Can't insert record in $questionextensiontable when restoring " .
- $this->name() . ' question id ' . $question;
- $status = false;
- }
- }
- //TODO restore extra data in answers
- return $status;
- }
-
- public function restore_map($old_question_id,$new_question_id,$info,$restore) {
- // There is nothing to decode
- return true;
- }
-
- public function restore_recode_answer($state, $restore) {
- // There is nothing to decode
- return $state->answer;
- }
-
/// IMPORT/EXPORT FUNCTIONS /////////////////
/*
@@ -988,7 +821,7 @@ class question_type {
$qo->qtype = $question_type;
foreach ($extraquestionfields as $field) {
- $qo->$field = addslashes($format->getpath($data, array('#',$field,0,'#'), $qo->$field));
+ $qo->$field = $format->getpath($data, array('#',$field,0,'#'), $qo->$field);
}
// run through the answers
@@ -1072,6 +905,142 @@ class question_type {
$question->qtype = $this->qtype;
return array($form, $question);
}
+
+ /**
+ * Get question context by category id
+ * @param int $category
+ * @return object $context
+ */
+ function get_context_by_category_id($category) {
+ global $DB;
+ $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$category));
+ $context = get_context_instance_by_id($contextid);
+ return $context;
+ }
+
+ /**
+ * Save the file belonging to one text field.
+ *
+ * @param array $field the data from the form (or from import). This will
+ * normally have come from the formslib editor element, so it will be an
+ * array with keys 'text', 'format' and 'itemid'. However, when we are
+ * importing, it will be an array with keys 'text', 'format' and 'files'
+ * @param object $context the context the question is in.
+ * @param string $component indentifies the file area question.
+ * @param string $filearea indentifies the file area questiontext, generalfeedback,answerfeedback.
+ * @param integer $itemid identifies the file area.
+ *
+ * @return string the text for this field, after files have been processed.
+ */
+ protected function import_or_save_files($field, $context, $component, $filearea, $itemid) {
+ if (!empty($field['itemid'])) {
+ // This is the normal case. We are safing the questions editing form.
+ return file_save_draft_area_files($field['itemid'], $context->id, $component,
+ $filearea, $itemid, $this->fileoptions, trim($field['text']));
+
+ } else if (!empty($field['files'])) {
+ // This is the case when we are doing an import.
+ foreach ($field['files'] as $file) {
+ $this->import_file($context, $component, $filearea, $itemid, $file);
+ }
+ }
+ return trim($field['text']);
+ }
+
+ /**
+ * Move all the files belonging to this question from one context to another.
+ * @param integer $questionid the question being moved.
+ * @param integer $oldcontextid the context it is moving from.
+ * @param integer $newcontextid the context it is moving to.
+ */
+ public function move_files($questionid, $oldcontextid, $newcontextid) {
+ $fs = get_file_storage();
+ $fs->move_area_files_to_new_context($oldcontextid,
+ $newcontextid, 'question', 'questiontext', $questionid);
+ $fs->move_area_files_to_new_context($oldcontextid,
+ $newcontextid, 'question', 'generalfeedback', $questionid);
+ }
+
+ /**
+ * Move all the files belonging to this question's answers when the question
+ * is moved from one context to another.
+ * @param integer $questionid the question being moved.
+ * @param integer $oldcontextid the context it is moving from.
+ * @param integer $newcontextid the context it is moving to.
+ * @param boolean $answerstoo whether there is an 'answer' question area,
+ * as well as an 'answerfeedback' one. Default false.
+ */
+ protected function move_files_in_answers($questionid, $oldcontextid, $newcontextid, $answerstoo = false) {
+ global $DB;
+ $fs = get_file_storage();
+
+ $answerids = $DB->get_records_menu('question_answers',
+ array('question' => $questionid), 'id', 'id,1');
+ foreach ($answerids as $answerid => $notused) {
+ if ($answerstoo) {
+ $fs->move_area_files_to_new_context($oldcontextid,
+ $newcontextid, 'question', 'answer', $answerid);
+ }
+ $fs->move_area_files_to_new_context($oldcontextid,
+ $newcontextid, 'question', 'answerfeedback', $answerid);
+ }
+ }
+
+ /**
+ * Delete all the files belonging to this question.
+ * @param integer $questionid the question being deleted.
+ * @param integer $contextid the context the question is in.
+ */
+ protected function delete_files($questionid, $contextid) {
+ $fs = get_file_storage();
+ $fs->delete_area_files($contextid, 'question', 'questiontext', $questionid);
+ $fs->delete_area_files($contextid, 'question', 'generalfeedback', $questionid);
+ }
+
+ /**
+ * Delete all the files belonging to this question's answers.
+ * @param integer $questionid the question being deleted.
+ * @param integer $contextid the context the question is in.
+ * @param boolean $answerstoo whether there is an 'answer' question area,
+ * as well as an 'answerfeedback' one. Default false.
+ */
+ protected function delete_files_in_answers($questionid, $contextid, $answerstoo = false) {
+ global $DB;
+ $fs = get_file_storage();
+
+ $answerids = $DB->get_records_menu('question_answers',
+ array('question' => $questionid), 'id', 'id,1');
+ foreach ($answerids as $answerid => $notused) {
+ if ($answerstoo) {
+ $fs->delete_area_files($contextid, 'question', 'answer', $answerid);
+ }
+ $fs->delete_area_files($contextid, 'question', 'answerfeedback', $answerid);
+ }
+ }
+
+ function import_file($context, $component, $filearea, $itemid, $file) {
+ $fs = get_file_storage();
+ $record = new stdclass;
+ if (is_object($context)) {
+ $record->contextid = $context->id;
+ } else {
+ $record->contextid = $context;
+ }
+ $record->component = $component;
+ $record->filearea = $filearea;
+ $record->itemid = $itemid;
+ $record->filename = $file->name;
+ $record->filepath = '/';
+ return $fs->create_file_from_string($record, $this->decode_file($file));
+ }
+
+ function decode_file($file) {
+ switch ($file->encoding) {
+ case 'base64':
+ default:
+ return base64_decode($file->content);
+ }
+ }
}
diff --git a/question/type/rendererbase.php b/question/type/rendererbase.php
index 3fad513b482..57e32a485cc 100644
--- a/question/type/rendererbase.php
+++ b/question/type/rendererbase.php
@@ -200,7 +200,7 @@ abstract class qtype_renderer extends renderer_base {
* @return string html fragment.
*/
function feedback_image($fraction, $selected = true) {
- global $CFG;
+ global $OUTPUT;
$state = question_state::graded_state_for_fraction($fraction);
@@ -218,7 +218,7 @@ abstract class qtype_renderer extends renderer_base {
}
$attributes = array(
- 'src' => $CFG->pixpath . '/i/' . $icon . '.gif',
+ 'src' => $OUTPUT->pix_url('i/' . $icon),
'alt' => get_string($state->get_feedback_class(), 'question'),
'class' => 'questioncorrectnessicon',
);
diff --git a/question/type/simpletest/testquestiontype.php b/question/type/simpletest/testquestiontype.php
index 77e07b15471..b491527a6e8 100644
--- a/question/type/simpletest/testquestiontype.php
+++ b/question/type/simpletest/testquestiontype.php
@@ -1,74 +1,44 @@
.
+
/**
- * Tests for some of ../questiontype.php
+ * Unit tests for the question type base class.
*
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package moodlecore
+ * @subpackage questiontypes
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
if (!defined('MOODLE_INTERNAL')) {
die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
}
require_once($CFG->dirroot . '/question/type/questiontype.php');
-class default_questiontype_test extends UnitTestCase {
+
+/**
+ * Tests for some of ../questionbase.php
+ *
+ * @copyright 2008 The Open University
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class question_type_test extends UnitTestCase {
public static $includecoverage = array('question/type/questiontype.php');
- protected $qtype;
-
- public function setUp() {
- $this->qtype = new default_questiontype();
- }
-
- public function tearDown() {
- $this->qtype = null;
- }
-
- function test_compare_responses() {
- $question = new stdClass;
- $state = new stdClass;
- $teststate = new stdClass;
-
- $state->responses = array();
- $teststate->responses = array();
- $this->assertTrue($this->qtype->compare_responses($question, $state, $teststate));
-
- $state->responses = array('' => 'frog');
- $teststate->responses = array('' => 'toad');
- $this->assertFalse($this->qtype->compare_responses($question, $state, $teststate));
-
- $state->responses = array('x' => 'frog');
- $teststate->responses = array('y' => 'frog');
- $this->assertFalse($this->qtype->compare_responses($question, $state, $teststate));
-
- $state->responses = array(1 => 1, 2 => 2);
- $teststate->responses = array(2 => 2, 1 => 1);
- $this->assertTrue($this->qtype->compare_responses($question, $state, $teststate));
- }
}
-
-