MDL-43749 normalise quiz database structure.
authorTim Hunt <T.J.Hunt@open.ac.uk>
Wed, 22 Jan 2014 18:19:31 +0000 (18:19 +0000)
committerTim Hunt <T.J.Hunt@open.ac.uk>
Sun, 2 Mar 2014 09:00:40 +0000 (10:00 +0100)
The sequence of questions that made up a quiz used to be stored as a
comma-separated list in quiz.questions. Now the same information is
stored in the rows in the quiz_slots table. This is not just 'better' in
a database design sense, but it allows for the future changes we will
need as we enhance the quiz in the MDL-40987 epic.

Having changed the database structure, all the rest of the code needs to
be changed to account for it, and that is done here.

Note that there are not many unit tests for the changed bit. That is
because as part of MDL-40987 we will be changing the code further, and
we will add unit tests then.

48 files changed:
backup/moodle2/restore_stepslib.php
lib/questionlib.php
mod/quiz/accessrule/delaybetweenattempts/tests/rule_test.php
mod/quiz/accessrule/ipaddress/tests/rule_test.php
mod/quiz/accessrule/numattempts/tests/rule_test.php
mod/quiz/accessrule/openclosedate/tests/rule_test.php
mod/quiz/accessrule/password/tests/rule_test.php
mod/quiz/accessrule/safebrowser/tests/rule_test.php
mod/quiz/accessrule/securewindow/tests/rule_test.php
mod/quiz/accessrule/timelimit/tests/rule_test.php
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/backup/moodle2/restore_quiz_stepslib.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/edit.php
mod/quiz/editlib.php
mod/quiz/lib.php
mod/quiz/locallib.php
mod/quiz/report/grading/report.php
mod/quiz/report/overview/report.php
mod/quiz/report/reportlib.php
mod/quiz/report/responses/report.php
mod/quiz/report/statistics/report.php
mod/quiz/tests/attempt_walkthrough_from_csv_test.php
mod/quiz/tests/attempt_walkthrough_test.php
mod/quiz/tests/editlib_test.php
mod/quiz/tests/generator/lib.php
mod/quiz/tests/locallib_test.php
mod/quiz/tests/quizobj_test.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/view.php
question/engine/upgrade/upgradelib.php
question/type/calculated/questiontype.php
question/type/calculated/tests/upgradelibnewqe_test.php
question/type/calculatedmulti/tests/upgradelibnewqe_test.php
question/type/calculatedsimple/tests/upgradelibnewqe_test.php
question/type/description/tests/upgradelibnewqe_test.php
question/type/essay/tests/upgradelibnewqe_test.php
question/type/match/tests/upgradelibnewqe_test.php
question/type/multianswer/edit_multianswer_form.php
question/type/multianswer/tests/upgradelibnewqe_test.php
question/type/multichoice/tests/upgradelibnewqe_test.php
question/type/numerical/tests/upgradelibnewqe_test.php
question/type/random/tests/upgradelibnewqe_test.php
question/type/shortanswer/tests/upgradelibnewqe_test.php
question/type/truefalse/tests/upgradelibnewqe_test.php

index 68c24d6..c2628ab 100644 (file)
@@ -4236,7 +4236,7 @@ abstract class restore_questions_activity_structure_step extends restore_activit
 
     /**
      * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
-     * @param object $data contains all the grouped attempt data ot process.
+     * @param object $data contains all the grouped attempt data to process.
      * @param pbject $quiz data about the activity the attempts belong to. Required
      * fields are (basically this only works for the quiz module):
      *      oldquestions => list of question ids in this activity - using old ids.
@@ -4298,7 +4298,8 @@ abstract class restore_questions_activity_structure_step extends restore_activit
         $this->inform_new_usage_id($usage->id);
 
         $data->uniqueid = $usage->id;
-        $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas, $quiz->questions);
+        $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
+                 $this->questions_recode_layout($quiz->oldquestions));
     }
 
     protected function find_question_session_and_states($data, $questionid) {
index 5cdfb69..755d7a5 100644 (file)
@@ -718,51 +718,59 @@ function question_preview_popup_params() {
  * 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.
  *
- * @global object
- * @global object
- * @param array $questionids array of question ids.
+ * @param array $questionids array of question ids to load. If null, then all
+ * questions matched by $join will be loaded.
  * @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.
+ * You must use named placeholders.
+ * @param string $orderby what to order the results by. Optional, default is unspecified order.
  *
  * @return array partially complete question objects. You need to call get_question_options
  * on them before they can be properly used.
  */
-function question_preload_questions($questionids, $extrafields = '', $join = '',
-        $extraparams = array()) {
+function question_preload_questions($questionids = null, $extrafields = '', $join = '',
+        $extraparams = array(), $orderby = '') {
     global $DB;
-    if (empty($questionids)) {
-        return array();
+
+    if ($questionids === null) {
+        $where = '';
+        $params = array();
+    } else {
+        if (empty($questionids)) {
+            return array();
+        }
+
+        list($questionidcondition, $params) = $DB->get_in_or_equal(
+                $questionids, SQL_PARAMS_NAMED, 'qid0000');
+        $where = 'WHERE q.id ' . $questionidcondition;
     }
+
     if ($join) {
-        $join = ' JOIN '.$join;
+        $join = 'JOIN ' . $join;
     }
+
     if ($extrafields) {
         $extrafields = ', ' . $extrafields;
     }
-    list($questionidcondition, $params) = $DB->get_in_or_equal(
-            $questionids, SQL_PARAMS_NAMED, 'qid0000');
-    $sql = 'SELECT q.*, qc.contextid' . $extrafields . ' FROM {question} q
-            JOIN {question_categories} qc ON q.category = qc.id' .
-            $join .
-          ' WHERE q.id ' . $questionidcondition;
 
-    // Load the questions
-    if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) {
-        return array();
+    if ($orderby) {
+        $orderby = 'ORDER BY ' . $orderby;
     }
 
+    $sql = "SELECT q.*, qc.contextid{$extrafields}
+              FROM {question} q
+              JOIN {question_categories} qc ON q.category = qc.id
+              {$join}
+             {$where}
+          {$orderby}";
+
+    // Load the questions.
+    $questions = $DB->get_records_sql($sql, $extraparams + $params);
     foreach ($questions as $question) {
         $question->_partiallyloaded = true;
     }
 
-    // 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.
-
     return $questions;
 }
 
index dc27fca..87cb47e 100644 (file)
@@ -44,7 +44,6 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $quiz->delay1 = 1000;
         $quiz->delay2 = 0;
         $quiz->timeclose = 0;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -79,7 +78,6 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $quiz->delay1 = 0;
         $quiz->delay2 = 1000;
         $quiz->timeclose = 0;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -119,7 +117,6 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $quiz->delay1 = 2000;
         $quiz->delay2 = 1000;
         $quiz->timeclose = 0;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -171,7 +168,6 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $quiz->delay1 = 2000;
         $quiz->delay2 = 1000;
         $quiz->timeclose = 15000;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -228,7 +224,6 @@ class quizaccess_delaybetweenattempts_testcase extends basic_testcase {
         $quiz->delay1 = 2000;
         $quiz->delay2 = 1000;
         $quiz->timeclose = 0;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 3f8f520..42bd089 100644 (file)
@@ -48,7 +48,6 @@ class quizaccess_ipaddress_testcase extends basic_testcase {
         // does not always work, for example using the mac install package on my laptop.
         $quiz->subnet = getremoteaddr(null);
         if (!empty($quiz->subnet)) {
-            $quiz->questions = '';
             $quizobj = new quiz($quiz, $cm, null);
             $rule = new quizaccess_ipaddress($quizobj, 0);
 
@@ -61,7 +60,6 @@ class quizaccess_ipaddress_testcase extends basic_testcase {
         }
 
         $quiz->subnet = '0.0.0.0';
-        $quiz->questions = '';
         $quizobj = new quiz($quiz, $cm, null);
         $rule = new quizaccess_ipaddress($quizobj, 0);
 
index 5ab1985..2a1dc0e 100644 (file)
@@ -41,7 +41,6 @@ class quizaccess_numattempts_testcase extends basic_testcase {
     public function test_num_attempts_access_rule() {
         $quiz = new stdClass();
         $quiz->attempts = 3;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 70cb819..08beabe 100644 (file)
@@ -43,7 +43,6 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $quiz->timeopen = 0;
         $quiz->timeclose = 0;
         $quiz->overduehandling = 'autosubmit';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -73,7 +72,6 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $quiz->timeopen = 10000;
         $quiz->timeclose = 0;
         $quiz->overduehandling = 'autosubmit';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -105,7 +103,6 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $quiz->timeopen = 0;
         $quiz->timeclose = 20000;
         $quiz->overduehandling = 'autosubmit';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -144,7 +141,6 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $quiz->timeopen = 10000;
         $quiz->timeclose = 20000;
         $quiz->overduehandling = 'autosubmit';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
@@ -197,7 +193,6 @@ class quizaccess_openclosedate_testcase extends basic_testcase {
         $quiz->timeclose = 20000;
         $quiz->overduehandling = 'graceperiod';
         $quiz->graceperiod = 1000;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 5fede6f..526ed6f 100644 (file)
@@ -41,7 +41,6 @@ class quizaccess_password_testcase extends basic_testcase {
     public function test_password_access_rule() {
         $quiz = new stdClass();
         $quiz->password = 'frog';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 470ecc9..09f1d57 100644 (file)
@@ -42,7 +42,6 @@ class quizaccess_safebrowser_testcase extends basic_testcase {
     public function test_safebrowser_access_rule() {
         $quiz = new stdClass();
         $quiz->browsersecurity = 'safebrowser';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 4ee3c74..3b4fa23 100644 (file)
@@ -43,7 +43,6 @@ class quizaccess_securewindow_testcase extends basic_testcase {
     public function test_securewindow_access_rule() {
         $quiz = new stdClass();
         $quiz->browsersecurity = 'securewindow';
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 65de692..491604e 100644 (file)
@@ -40,7 +40,6 @@ class quizaccess_timelimit_testcase extends basic_testcase {
     public function test_time_limit_access_rule() {
         $quiz = new stdClass();
         $quiz->timelimit = 3600;
-        $quiz->questions = '';
         $cm = new stdClass();
         $cm->id = 0;
         $quizobj = new quiz($quiz, $cm, null);
index 302a752..3e7cfee 100644 (file)
@@ -65,7 +65,6 @@ class quiz {
     protected $cm;
     protected $quiz;
     protected $context;
-    protected $questionids;
 
     // Fields set later if that data is needed.
     protected $questions = null;
@@ -89,12 +88,6 @@ class quiz {
         if ($getcontext && !empty($cm->id)) {
             $this->context = context_module::instance($cm->id);
         }
-        $questionids = quiz_questions_in_quiz($this->quiz->questions);
-        if ($questionids) {
-            $this->questionids = explode(',', quiz_questions_in_quiz($this->quiz->questions));
-        } else {
-            $this->questionids = array(); // Which idiot made explode(',', '') = array('')?
-        }
     }
 
     /**
@@ -132,13 +125,10 @@ class quiz {
      * Load just basic information about all the questions in this quiz.
      */
     public function preload_questions() {
-        if (empty($this->questionids)) {
-            throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
-        }
-        $this->questions = question_preload_questions($this->questionids,
-                'qqi.maxmark, qqi.id AS instance',
-                '{quiz_question_instances} qqi ON qqi.quizid = :quizid AND q.id = qqi.questionid',
-                array('quizid' => $this->quiz->id));
+        $this->questions = question_preload_questions(null,
+                'slot.maxmark, slot.id AS slotid, slot.slot, slot.page',
+                '{quiz_slots} slot ON slot.quizid = :quizid AND q.id = slot.questionid',
+                array('quizid' => $this->quiz->id), 'slot.slot');
     }
 
     /**
@@ -148,8 +138,11 @@ class quiz {
      * @param array $questionids question ids of the questions to load. null for all.
      */
     public function load_questions($questionids = null) {
+        if ($this->questions === null) {
+            throw new coding_exception('You must call preload_questions before calling load_questions.');
+        }
         if (is_null($questionids)) {
-            $questionids = $this->questionids;
+            $questionids = array_keys($this->questions);
         }
         $questionstoprocess = array();
         foreach ($questionids as $id) {
@@ -226,7 +219,10 @@ class quiz {
      * @return whether any questions have been added to this quiz.
      */
     public function has_questions() {
-        return !empty($this->questionids);
+        if ($this->questions === null) {
+            $this->preload_questions();
+        }
+        return !empty($this->questions);
     }
 
     /**
@@ -242,7 +238,7 @@ class quiz {
      */
     public function get_questions($questionids = null) {
         if (is_null($questionids)) {
-            $questionids = $this->questionids;
+            $questionids = array_keys($this->questions);
         }
         $questions = array();
         foreach ($questionids as $id) {
@@ -530,7 +526,7 @@ class quiz_attempt {
         $this->pagelayout = array();
 
         // Break up the layout string into pages.
-        $pagelayouts = explode(',0', quiz_clean_layout($this->attempt->layout, true));
+        $pagelayouts = explode(',0', $this->attempt->layout);
 
         // Strip off any empty last page (normally there is one).
         if (end($pagelayouts) == '') {
index e229eb6..6ea4309 100644 (file)
@@ -47,7 +47,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
             'reviewspecificfeedback', 'reviewgeneralfeedback',
             'reviewrightanswer', 'reviewoverallfeedback',
             'questionsperpage', 'navmethod', 'shufflequestions', 'shuffleanswers',
-            'questions', 'sumgrades', 'grade', 'timecreated',
+            'sumgrades', 'grade', 'timecreated',
             'timemodified', 'password', 'subnet', 'browsersecurity',
             'delay1', 'delay2', 'showuserpicture', 'showblocks'));
 
@@ -57,7 +57,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
         $qinstances = new backup_nested_element('question_instances');
 
         $qinstance = new backup_nested_element('question_instance', array('id'), array(
-            'questionid', 'maxmark'));
+            'slot', 'page', 'questionid', 'maxmark'));
 
         $feedbacks = new backup_nested_element('feedbacks');
 
@@ -107,7 +107,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
         // Define sources.
         $quiz->set_source_table('quiz', array('id' => backup::VAR_ACTIVITYID));
 
-        $qinstance->set_source_table('quiz_question_instances',
+        $qinstance->set_source_table('quiz_slots',
                 array('quizid' => backup::VAR_PARENTID));
 
         $feedback->set_source_table('quiz_feedback',
index 283ab4b..3f859ec 100644 (file)
@@ -92,9 +92,10 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
         $data->timecreated = $this->apply_date_offset($data->timecreated);
         $data->timemodified = $this->apply_date_offset($data->timemodified);
 
-        // Needed by {@link process_quiz_attempt_legacy}.
-        $this->oldquizlayout = $data->questions;
-        $data->questions = $this->questions_recode_layout($data->questions);
+        if (property_exists($data, 'questions')) {
+            // Needed by {@link process_quiz_attempt_legacy}, in which case it will be present.
+            $this->oldquizlayout = $data->questions;
+        }
 
         // The setting quiz->attempts can come both in data->attempts and
         // data->attempts_number, handle both. MDL-26229.
@@ -243,10 +244,36 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
             $data->maxmark = $data->grade;
         }
 
+        if (!property_exists($data, 'slot')) {
+            $page = 1;
+            $slot = 1;
+            foreach (explode(',', $this->oldquizlayout) as $item) {
+                if ($item == 0) {
+                    $page += 1;
+                    continue;
+                }
+                if ($item == $data->questionid) {
+                    $data->slot = $slot;
+                    $data->page = $page;
+                    break;
+                }
+                $slot += 1;
+            }
+        }
+
+        if (!property_exists($data, 'slot')) {
+            // There was a question_instance in the backup file for a question
+            // that was not acutally in the quiz. Drop it.
+            $this->log('question ' . $data->questionid . ' was associated with quiz ' .
+                    $quiz->id . ' but not actually used. ' .
+                    'The instance has been ignored.', backup::LOG_INFO);
+            return;
+        }
+
         $data->quizid = $this->get_new_parentid('quiz');
         $data->questionid = $this->get_mappingid('question', $data->questionid);
 
-        $DB->insert_record('quiz_question_instances', $data);
+        $DB->insert_record('quiz_slots', $data);
     }
 
     protected function process_quiz_feedback($data) {
index df2ada8..3b85920 100644 (file)
@@ -33,7 +33,6 @@
         <FIELD NAME="navmethod" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="free" SEQUENCE="false" COMMENT="Any constraints on how the user is allowed to navigate around the quiz. Currently recognised values are 'free' and 'seq'."/>
         <FIELD NAME="shufflequestions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the question order should be shuffled for each attempt."/>
         <FIELD NAME="shuffleanswers" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether the parts of the question should be shuffled, in those question types that support it."/>
-        <FIELD NAME="questions" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="Comma-separated list of question ids, with 0s for page breaks. The quiz layout. See also the quiz_question_instances table."/>
         <FIELD NAME="sumgrades" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total of all the question instance maxmarks."/>
         <FIELD NAME="grade" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="The total that the quiz overall grade is scaled to be out of."/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time when the quiz was added to the course."/>
         <INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
       </INDEXES>
     </TABLE>
-    <TABLE NAME="quiz_question_instances" COMMENT="Stores the maximum possible grade (weight) for each question used in a quiz.">
+    <TABLE NAME="quiz_slots" COMMENT="Stores the question used in a quiz, with the order, and for each question, which page it appears on, and the maximum mark (weight).">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Where this question comes in order in the list of questions in this quiz. Like question_attempts.slot."/>
         <FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references quiz.id."/>
+        <FIELD NAME="page" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The page number that this questions appears on. If the question in slot n appears on page p, then the question in slot n+1 must appear on page p or p+1. Well, except that when a quiz is being created, there may be empty pages, which would cause the page number to jump here."/>
         <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
         <FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="7" COMMENT="How many marks this question contributes to quiz.sumgrades."/>
       </FIELDS>
         <KEY NAME="quizid" TYPE="foreign" FIELDS="quizid" REFTABLE="quiz" REFFIELDS="id"/>
         <KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
       </KEYS>
+      <INDEXES>
+        <INDEX NAME="quizid-slot" UNIQUE="true" FIELDS="quizid, slot"/>
+      </INDEXES>
     </TABLE>
     <TABLE NAME="quiz_feedback" COMMENT="Feedback given to students based on which grade band their overall score lies.">
       <FIELDS>
index 1b5a7c1..7cfab3b 100644 (file)
@@ -515,6 +515,252 @@ function xmldb_quiz_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2014021300, 'quiz');
     }
 
+    if ($oldversion < 2014022000) {
+
+        // Define table quiz_question_instances to be renamed to quiz_slots.
+        $table = new xmldb_table('quiz_question_instances');
+
+        // Launch rename table for quiz_question_instances.
+        $dbman->rename_table($table, 'quiz_slots');
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022000, 'quiz');
+    }
+
+    if ($oldversion < 2014022001) {
+
+        // Define field slot to be added to quiz_slots.
+        $table = new xmldb_table('quiz_slots');
+        $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'id');
+
+        // Conditionally launch add field slot.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022001, 'quiz');
+    }
+
+    if ($oldversion < 2014022002) {
+
+        // Define field page to be added to quiz_slots.
+        $table = new xmldb_table('quiz_slots');
+        $field = new xmldb_field('page', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'quizid');
+
+        // Conditionally launch add field page.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022002, 'quiz');
+    }
+
+    if ($oldversion < 2014022003) {
+
+        // Use the information in the old quiz.questions column to fill in the
+        // new slot and page columns.
+        $numquizzes = $DB->count_records('quiz');
+        if ($numquizzes > 0) {
+            $pbar = new progress_bar('quizquestionstoslots', 500, true);
+            $transaction = $DB->start_delegated_transaction();
+
+            $numberdone = 0;
+            $quizzes = $DB->get_recordset('quiz', null, 'id', 'id,questions,sumgrades');
+            foreach ($quizzes as $quiz) {
+                if ($quiz->questions === '') {
+                    $questionsinorder = array();
+                } else {
+                    $questionsinorder = explode(',', $quiz->questions);
+                }
+
+                $questionidtoslotrowid = $DB->get_records_menu('quiz_slots',
+                        array('quizid' => $quiz->id), '', 'questionid, id');
+
+                $problemfound = false;
+                $currentpage = 1;
+                $slot = 1;
+                foreach ($questionsinorder as $questionid) {
+                    if ($questionid === '0') {
+                        // Page break.
+                        $currentpage++;
+                        continue;
+                    }
+
+                    if (array_key_exists($questionid, $questionidtoslotrowid)) {
+                        // Normal case. quiz_slots entry is present.
+                        // Just need to add slot and page.
+                        $quizslot = new stdClass();
+                        $quizslot->id   = $questionidtoslotrowid[$questionid];
+                        $quizslot->slot = $slot;
+                        $quizslot->page = $currentpage;
+                        $DB->update_record('quiz_slots', $quizslot);
+
+                        unset($questionidtoslotrowid[$questionid]); // So we can do a sanity check later.
+                        $slot++;
+                        continue;
+
+                    } else {
+                        // This should not happen. The question was listed in
+                        // quiz.questions, but there was not an entry for it in
+                        // quiz_slots (formerly quiz_question_instances).
+                        // Previously, if such question ids were found, then
+                        // starting an attempt at the quiz would throw an exception.
+                        // Here, we try to add the missing data.
+                        $problemfound = true;
+                        $defaultmark = $DB->get_field('question', 'defaultmark',
+                                array('id' => $questionid), IGNORE_MISSING);
+                        if ($defaultmark === false) {
+                            debugging('During upgrade, detected that question ' .
+                                    $questionid . ' was listed as being part of quiz ' .
+                                    $quiz->id . ' but this question no longer exists. Ignoring it.', DEBUG_NORMAL);
+
+                            // Non-existent question. Ignore it completely.
+                            continue;
+                        }
+
+                        debugging('During upgrade, detected that question ' .
+                                $questionid . ' was listed as being part of quiz ' .
+                                $quiz->id . ' but there was not entry for it in ' .
+                                'quiz_question_instances/quiz_slots. Creating an entry with default mark.', DEBUG_NORMAL);
+                        $quizslot = new stdClass();
+                        $quizslot->quizid     = $quiz->id;
+                        $quizslot->slot       = $slot;
+                        $quizslot->page       = $currentpage;
+                        $quizslot->questionid = $questionid;
+                        $quizslot->maxmark    = $defaultmark;
+                        $DB->insert_record('quiz_slots', $quizslot);
+
+                        $slot++;
+                        continue;
+                    }
+
+                }
+
+                // Now, as a sanity check, ensure we have done all the
+                // quiz_slots rows linked to this quiz.
+                if (!empty($questionidtoslotrowid)) {
+                    debugging('During upgrade, detected that questions ' .
+                            implode(', ', array_keys($questionidtoslotrowid)) .
+                            ' had instances in quiz ' . $quiz->id . ' but were not actually used. ' .
+                            'The instances have been removed.', DEBUG_NORMAL);
+
+                    $DB->delete_records_list('quiz_slots', 'id', array_values($questionidtoslotrowid));
+                    $problemfound = true;
+                }
+
+                // If there were problems found, we probably need to re-compute
+                // quiz.sumgrades.
+                if ($problemfound) {
+                    // C/f the quiz_update_sumgrades function in locallib.php,
+                    // but note that what we do here is a bit simpler.
+                    $newsumgrades = $DB->get_field_sql(
+                            "SELECT SUM(maxmark)
+                               FROM {quiz_slots}
+                              WHERE quizid = ?",
+                            array($quiz->id));
+                    if (abs($newsumgrades - $quiz->sumgrades) > 0.000005) {
+                        debugging('Because of the previously mentioned problems, ' .
+                                'sumgrades for quiz ' . $quiz->id .
+                                ' was changed from ' . $quiz->sumgrades . ' to ' .
+                                $newsumgrades . ' You should probably check that this quiz is still working: ' .
+                                $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id . '.', DEBUG_NORMAL);
+                        $DB->set_field('quiz', 'sumgrades', $newsumgrades, array('id' => $quiz->id));
+                    }
+                }
+
+                // Done with this quiz. Update progress bar.
+                $numberdone++;
+                $pbar->update($numberdone, $numquizzes,
+                        "Upgrading quiz structure - {$numberdone}/{$numquizzes}.");
+            }
+
+            $transaction->allow_commit();
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022003, 'quiz');
+    }
+
+    if ($oldversion < 2014022004) {
+
+        // If, for any reason, there were any quiz_slots missed, then try
+        // to do something about that now before we add the NOT NULL constraints.
+        // In fact, becuase of the sanity check at the end of the above check,
+        // any such quiz_slots rows must refer to a non-existent quiz id, so
+        // delete them.
+        $DB->delete_records_select('quiz_slots',
+                'NOT EXISTS (SELECT 1 FROM {quiz} q WHERE q.id = quizid)');
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022004, 'quiz');
+
+        // Now, if any quiz_slots rows are left with slot or page NULL, something
+        // is badly wrong.
+        if ($DB->record_exists_select('quiz_slots', 'slot IS NULL OR page IS NULL')) {
+            throw new coding_exception('Something went wrong in the quiz upgrade step for MDL-43749. ' .
+                    'Some quiz_slots still contain NULLs which will break the NOT NULL constraints we need to add. ' .
+                    'Please report this problem at http://tracker.moodle.org/ so that it can be investigated. Thank you.');
+        }
+    }
+
+    if ($oldversion < 2014022005) {
+
+        // Changing nullability of field slot on table quiz_slots to not null.
+        $table = new xmldb_table('quiz_slots');
+        $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'id');
+
+        // Launch change of nullability for field slot.
+        $dbman->change_field_notnull($table, $field);
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022005, 'quiz');
+    }
+
+    if ($oldversion < 2014022006) {
+
+        // Changing nullability of field page on table quiz_slots to not null.
+        $table = new xmldb_table('quiz_slots');
+        $field = new xmldb_field('page', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'quizid');
+
+        // Launch change of nullability for field page.
+        $dbman->change_field_notnull($table, $field);
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022006, 'quiz');
+    }
+
+    if ($oldversion < 2014022007) {
+
+        // Define index quizid-slot (unique) to be added to quiz_slots.
+        $table = new xmldb_table('quiz_slots');
+        $index = new xmldb_index('quizid-slot', XMLDB_INDEX_UNIQUE, array('quizid', 'slot'));
+
+        // Conditionally launch add index quizid-slot.
+        if (!$dbman->index_exists($table, $index)) {
+            $dbman->add_index($table, $index);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022007, 'quiz');
+    }
+
+    if ($oldversion < 2014022008) {
+
+        // Define field questions to be dropped from quiz.
+        $table = new xmldb_table('quiz');
+        $field = new xmldb_field('questions');
+
+        // Conditionally launch drop field questions.
+        if ($dbman->field_exists($table, $field)) {
+            $dbman->drop_field($table, $field);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014022008, 'quiz');
+    }
+
     return true;
 }
 
index 884fa23..22eb253 100644 (file)
@@ -119,7 +119,6 @@ $scrollpos = optional_param('scrollpos', '', PARAM_INT);
 
 list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
         question_edit_setup('editq', '/mod/quiz/edit.php', true);
-$quiz->questions = quiz_clean_layout($quiz->questions);
 
 $defaultcategoryobj = question_make_default_categories($contexts->all());
 $defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
@@ -161,23 +160,14 @@ add_to_log($cm->course, 'quiz', 'editquestions',
 // You need mod/quiz:manage in addition to question capabilities to access this page.
 require_capability('mod/quiz:manage', $contexts->lowest());
 
-if (empty($quiz->grades)) {
-    $quiz->grades = quiz_get_all_question_grades($quiz);
-}
-
 // Process commands ============================================================
-if ($quiz->shufflequestions) {
-    // Strip page breaks before processing actions, so that re-ordering works
-    // as expected when shuffle questions is on.
-    $quiz->questions = quiz_repaginate($quiz->questions, 0);
-}
 
 // Get the list of question ids had their check-boxes ticked.
-$selectedquestionids = array();
+$selectedslots = array();
 $params = (array) data_submitted();
 foreach ($params as $key => $value) {
     if (preg_match('!^s([0-9]+)$!', $key, $matches)) {
-        $selectedquestionids[] = $matches[1];
+        $selectedslots[] = $matches[1];
     }
 }
 
@@ -186,15 +176,13 @@ if ($scrollpos) {
     $afteractionurl->param('scrollpos', $scrollpos);
 }
 if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) {
-    $quiz->questions = quiz_move_question_up($quiz->questions, $up);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    quiz_move_question_up($quiz, $up);
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
 
 if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
-    $quiz->questions = quiz_move_question_down($quiz->questions, $down);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    quiz_move_question_down($quiz, $down);
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
@@ -202,8 +190,7 @@ if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
 if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) {
     // Re-paginate the quiz.
     $questionsperpage = optional_param('questionsperpage', $quiz->questionsperpage, PARAM_INT);
-    $quiz->questions = quiz_repaginate($quiz->questions, $questionsperpage );
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    quiz_repaginate_questions($quiz->id, $questionsperpage );
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
@@ -248,51 +235,46 @@ if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
 }
 
 if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) &&
-        !empty($selectedquestionids) && confirm_sesskey()) {
-    foreach ($selectedquestionids as $questionid) {
-        $quiz->questions = quiz_add_page_break_after($quiz->questions, $questionid);
+        !empty($selectedslots) && confirm_sesskey()) {
+    foreach ($selectedslots as $slot) {
+        quiz_add_page_break_after_slot($quiz, $slot);
     }
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
 
 $addpage = optional_param('addpage', false, PARAM_INT);
 if ($addpage !== false && confirm_sesskey()) {
-    $quiz->questions = quiz_add_page_break_at($quiz->questions, $addpage);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    quiz_add_page_break_after_slot($quiz, $addpage);
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
 
 $deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT);
 if (($deleteemptypage !== false) && confirm_sesskey()) {
-    $quiz->questions = quiz_delete_empty_page($quiz->questions, $deleteemptypage);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    quiz_delete_empty_page($quiz, $deleteemptypage);
     quiz_delete_previews($quiz);
     redirect($afteractionurl);
 }
 
 $remove = optional_param('remove', false, PARAM_INT);
-if ($remove && confirm_sesskey()) {
+if ($remove && confirm_sesskey() && quiz_has_question_use($quiz, $remove)) {
     // Remove a question from the quiz.
     // We require the user to have the 'use' capability on the question,
     // so that then can add it back if they remove the wrong one by mistake,
     // but, if the question is missing, it can always be removed.
-    if ($DB->record_exists('question', array('id' => $remove))) {
-        quiz_require_question_use($remove);
-    }
-    quiz_remove_question($quiz, $remove);
+    quiz_remove_slot($quiz, $remove);
     quiz_delete_previews($quiz);
     quiz_update_sumgrades($quiz);
     redirect($afteractionurl);
 }
 
 if (optional_param('quizdeleteselected', false, PARAM_BOOL) &&
-        !empty($selectedquestionids) && confirm_sesskey()) {
-    foreach ($selectedquestionids as $questionid) {
-        if (quiz_has_question_use($questionid)) {
-            quiz_remove_question($quiz, $questionid);
+        !empty($selectedslots) && confirm_sesskey()) {
+    // Work backwards, since removing a question renumbers following slots.
+    foreach (array_reverse($selectedslots) as $slot) {
+        if (quiz_has_question_use($quiz, $slot)) {
+            quiz_remove_slot($quiz, $slot);
         }
     }
     quiz_delete_previews($quiz);
@@ -304,8 +286,6 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
     $deletepreviews = false;
     $recomputesummarks = false;
 
-    $oldquestions = explode(',', $quiz->questions); // The questions in the old order.
-    $questions = array(); // For questions in the new order.
     $rawdata = (array) data_submitted();
     $moveonpagequestions = array();
     $moveselectedonpage = optional_param('moveselectedonpagetop', 0, PARAM_INT);
@@ -313,73 +293,111 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
         $moveselectedonpage = optional_param('moveselectedonpagebottom', 0, PARAM_INT);
     }
 
+    $newslotorder = array();
     foreach ($rawdata as $key => $value) {
         if (preg_match('!^g([0-9]+)$!', $key, $matches)) {
             // Parse input for question -> grades.
-            $questionid = $matches[1];
-            $quiz->grades[$questionid] = unformat_float($value);
-            quiz_update_question_instance($quiz->grades[$questionid], $questionid, $quiz);
+            $slotnumber = $matches[1];
+            $newgrade = unformat_float($value);
+            quiz_update_slot_maxmark($DB->get_record('quiz_slots',
+                    array('quizid' => $quiz->id, 'slot' => $slotnumber), '*', MUST_EXIST), $newgrade);
             $deletepreviews = true;
             $recomputesummarks = true;
 
         } else if (preg_match('!^o(pg)?([0-9]+)$!', $key, $matches)) {
             // Parse input for ordering info.
-            $questionid = $matches[2];
+            $slotnumber = $matches[2];
             // Make sure two questions don't overwrite each other. If we get a second
             // question with the same position, shift the second one along to the next gap.
             $value = clean_param($value, PARAM_INT);
-            while (array_key_exists($value, $questions)) {
+            while (array_key_exists($value, $newslotorder)) {
                 $value++;
             }
             if ($matches[1]) {
                 // This is a page-break entry.
-                $questions[$value] = 0;
+                $newslotorder[$value] = 0;
             } else {
-                $questions[$value] = $questionid;
+                $newslotorder[$value] = $slotnumber;
             }
             $deletepreviews = true;
         }
     }
 
-    // If ordering info was given, reorder the questions.
-    if ($questions) {
-        ksort($questions);
-        $questions[] = 0;
-        $quiz->questions = implode(',', $questions);
-        $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
-        $deletepreviews = true;
-    }
-
-    // Get a list of questions to move, later to be added in the appropriate
-    // place in the string.
     if ($moveselectedonpage) {
-        $questions = explode(',', $quiz->questions);
-        $newquestions = array();
-        // Remove the questions from their original positions first.
-        foreach ($questions as $questionid) {
-            if (!in_array($questionid, $selectedquestionids)) {
-                $newquestions[] = $questionid;
+
+        // Make up a $newslotorder, then let the next if statement do the work.
+        $oldslots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 'slot');
+
+        $beforepage = array();
+        $onpage = array();
+        $afterpage = array();
+        foreach ($oldslots as $oldslot) {
+            if (in_array($oldslot->slot, $selectedslots)) {
+                $onpage[] = $oldslot;
+            } else if ($oldslot->page <= $moveselectedonpage) {
+                $beforepage[] = $oldslot;
+            } else {
+                $afterpage[] = $oldslot;
             }
         }
-        $questions = $newquestions;
 
-        // Move to the end of the selected page.
-        $pagebreakpositions = array_keys($questions, 0);
-        $numpages = count($pagebreakpositions);
+        $newslotorder = array();
+        $currentpage = 1;
+        $index = 10;
+        foreach ($beforepage as $slot) {
+            while ($currentpage < $slot->page) {
+                $newslotorder[$index] = 0;
+                $index += 10;
+                $currentpage += 1;
+            }
+            $newslotorder[$index] = $slot->slot;
+            $index += 10;
+        }
 
-        // Ensure the target page number is in range.
-        for ($i = $moveselectedonpage; $i > $numpages; $i--) {
-            $questions[] = 0;
-            $pagebreakpositions[] = count($questions) - 1;
+        while ($currentpage < $moveselectedonpage) {
+            $newslotorder[$index] = 0;
+            $index += 10;
+            $currentpage += 1;
+        }
+        foreach ($onpage as $slot) {
+            $newslotorder[$index] = $slot->slot;
+            $index += 10;
         }
-        $moveselectedpos = $pagebreakpositions[$moveselectedonpage - 1];
 
-        // Do the move.
-        array_splice($questions, $moveselectedpos, 0, $selectedquestionids);
-        $quiz->questions = implode(',', $questions);
+        foreach ($afterpage as $slot) {
+            while ($currentpage < $slot->page) {
+                $newslotorder[$index] = 0;
+                $index += 10;
+                $currentpage += 1;
+            }
+            $newslotorder[$index] = $slot->slot;
+            $index += 10;
+        }
+    }
 
-        // Update the database.
-        $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    // If ordering info was given, reorder the questions.
+    if ($newslotorder) {
+        ksort($newslotorder);
+        $currentpage = 1;
+        $currentslot = 1;
+        $slotreorder = array();
+        $slotpages = array();
+        foreach ($newslotorder as $slotnumber) {
+            if ($slotnumber == 0) {
+                $currentpage += 1;
+                continue;
+            }
+            $slotreorder[$slotnumber] = $currentslot;
+            $slotpages[$currentslot] = $currentpage;
+            $currentslot += 1;
+        }
+        $trans = $DB->start_delegated_transaction();
+        update_field_with_unique_index('quiz_slots',
+                'slot', $slotreorder, array('quizid' => $quiz->id));
+        foreach ($slotpages as $slotnumber => $page) {
+            $DB->set_field('quiz_slots', 'page', $page, array('quizid' => $quiz->id, 'slot' => $slotnumber));
+        }
+        $trans->allow_commit();
         $deletepreviews = true;
     }
 
@@ -421,7 +439,11 @@ echo $OUTPUT->header();
 $quizeditconfig = new stdClass();
 $quizeditconfig->url = $thispageurl->out(true, array('qbanktool' => '0'));
 $quizeditconfig->dialoglisteners = array();
-$numberoflisteners = max(quiz_number_of_pages($quiz->questions), 1);
+$numberoflisteners = $DB->get_field_sql("
+    SELECT COALESCE(MAX(page), 1)
+      FROM {quiz_slots}
+     WHERE quizid = ?", array($quiz->id));
+
 for ($pageiter = 1; $pageiter <= $numberoflisteners; $pageiter++) {
     $quizeditconfig->dialoglisteners[] = 'addrandomdialoglaunch_' . $pageiter;
 }
@@ -487,7 +509,6 @@ echo '<div class="quizcontents ' . $quizcontentsclass . '" id="quizcontentsblock
 if ($quiz->shufflequestions) {
     $repaginatingdisabledhtml = 'disabled="disabled"';
     $repaginatingdisabled = true;
-    $quiz->questions = quiz_repaginate($quiz->questions, $quiz->questionsperpage);
 } else {
     $repaginatingdisabledhtml = '';
     $repaginatingdisabled = false;
index 27156c2..cd2f9e1 100644 (file)
@@ -48,12 +48,20 @@ function quiz_require_question_use($questionid) {
 
 /**
  * Verify that the question exists, and the user has permission to use it.
- * @param int $questionid The id of the question.
+ * @param object $quiz the quiz settings.
+ * @param int $slot which question in the quiz to test.
  * @return bool whether the user can use this question.
  */
-function quiz_has_question_use($questionid) {
+function quiz_has_question_use($quiz, $slot) {
     global $DB;
-    $question = $DB->get_record('question', array('id' => $questionid), '*', MUST_EXIST);
+    $question = $DB->get_record_sql("
+            SELECT q.*
+              FROM {quiz_slots} slot
+              JOIN {question} q ON q.id = slot.questionid
+             WHERE slot.quizid = ? AND slot.slot = ?", array($quiz->id, $slot));
+    if (!$question) {
+        return false;
+    }
     return question_has_capability_on($question, 'use');
 }
 
@@ -62,118 +70,127 @@ function quiz_has_question_use($questionid) {
  * @param object $quiz the quiz object.
  * @param int $questionid The id of the question to be deleted.
  */
-function quiz_remove_question($quiz, $questionid) {
+function quiz_remove_slot($quiz, $slotnumber) {
     global $DB;
 
-    $questionids = explode(',', $quiz->questions);
-    $key = array_search($questionid, $questionids);
-    if ($key === false) {
+    $slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'slot' => $slotnumber));
+    $maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($quiz->id));
+    if (!$slot) {
         return;
     }
 
-    unset($questionids[$key]);
-    $quiz->questions = implode(',', $questionids);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
-    $DB->delete_records('quiz_question_instances',
-            array('quizid' => $quiz->instance, 'questionid' => $questionid));
+    $trans = $DB->start_delegated_transaction();
+    $DB->delete_records('quiz_slots', array('id' => $slot->id));
+    for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
+        $DB->set_field('quiz_slots', 'slot', $i - 1,
+                array('quizid' => $quiz->id, 'slot' => $i));
+    }
+    $trans->allow_commit();
 }
 
 /**
  * Remove an empty page from the quiz layout. If that is not possible, do nothing.
- * @param string $layout the existinng layout, $quiz->questions.
- * @param int $index the position into $layout where the empty page should be removed.
- * @return the updated layout
+ * @param object $quiz the quiz settings.
+ * @param int $pagenumber the page number to delete.
  */
-function quiz_delete_empty_page($layout, $index) {
-    $questionids = explode(',', $layout);
-
-    if ($index < -1 || $index >= count($questionids) - 1) {
-        return $layout;
-    }
+function quiz_delete_empty_page($quiz, $pagenumber) {
+    global $DB;
 
-    if (($index >= 0 && $questionids[$index] != 0) || $questionids[$index + 1] != 0) {
-        return $layout; // This was not an empty page.
+    if ($DB->record_exists('quiz_slots', array('quizid' => $quiz->id, 'page' => $pagenumber))) {
+        // This was not an empty page.
+        return;
     }
 
-    unset($questionids[$index + 1]);
-
-    return implode(',', $questionids);
+    $DB->execute('UPDATE {quiz_slots} SET page = page - 1 WHERE quizid = ? AND page > ?',
+            array($quiz->id, $pagenumber));
 }
 
 /**
  * Add a question to a quiz
  *
  * Adds a question to a quiz by updating $quiz as well as the
- * quiz and quiz_question_instances tables. It also adds a page break
- * if required.
- * @param int $id The id of the question to be added
+ * quiz and quiz_slots tables. It also adds a page break if required.
+ * @param int $questionid The id of the question to be added
  * @param object $quiz The extended quiz object as used by edit.php
  *      This is updated by this function
  * @param int $page Which page in quiz to add the question on. If 0 (default),
  *      add at the end
+ * @param float $maxmark The maximum mark to set for this question. (Optional,
+ *      defaults to question.defaultmark.
  * @return bool false if the question was already in the quiz
  */
-function quiz_add_quiz_question($id, $quiz, $page = 0) {
+function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null) {
     global $DB;
-    $questions = explode(',', quiz_clean_layout($quiz->questions));
-    if (in_array($id, $questions)) {
+    $slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id),
+            'slot', 'questionid, slot, page, id');
+    if (array_key_exists($questionid, $slots)) {
         return false;
     }
 
-    // Remove ending page break if it is not needed.
-    if ($breaks = array_keys($questions, 0)) {
-        // Determine location of the last two page breaks.
-        $end = end($breaks);
-        $last = prev($breaks);
-        $last = $last ? $last : -1;
-        if (!$quiz->questionsperpage || (($end - $last - 1) < $quiz->questionsperpage)) {
-            array_pop($questions);
+    $trans = $DB->start_delegated_transaction();
+
+    $maxpage = 1;
+    $numonlastpage = 0;
+    foreach ($slots as $slot) {
+        if ($slot->page > $maxpage) {
+            $maxpage = $slot->page;
+            $numonlastpage = 1;
+        } else {
+            $numonlastpage += 1;
         }
     }
+
+    // Add the new question instance.
+    $slot = new stdClass();
+    $slot->quizid = $quiz->id;
+    $slot->questionid = $questionid;
+
+    if ($maxmark !== null) {
+        $slot->maxmark = $maxmark;
+    } else {
+        $slot->maxmark = $DB->get_field('question', 'defaultmark', array('id' => $questionid));
+    }
+
     if (is_int($page) && $page >= 1) {
-        $numofpages = quiz_number_of_pages($quiz->questions);
-        if ($numofpages<$page) {
-            // The page specified does not exist in quiz.
-            $page = 0;
-        } else {
-            // Add ending page break - the following logic requires doing
-            // this at this point.
-            $questions[] = 0;
-            $currentpage = 1;
-            $addnow = false;
-            foreach ($questions as $question) {
-                if ($question == 0) {
-                    $currentpage++;
-                    // The current page is the one after the one we want to add on,
-                    // so we add the question before adding the current page.
-                    if ($currentpage == $page + 1) {
-                        $questions_new[] = $id;
-                    }
-                }
-                $questions_new[] = $question;
+        // Adding on a given page.
+        $lastslotbefore = 1;
+        foreach (array_reverse($slots) as $otherslot) {
+            if ($otherslot->page > $page) {
+                $DB->set_field('quiz_slots', 'slot', $otherslot->slot + 1, array('id' => $otherslot->id));
+            } else {
+                $lastslotbefore = $otherslot->slot;
+                break;
             }
-            $questions = $questions_new;
         }
-    }
-    if ($page == 0) {
-        // Add question.
-        $questions[] = $id;
-        // Add ending page break.
-        $questions[] = 0;
-    }
+        $slot->slot = $lastslotbefore + 1;
+        $slot->page = min($page, $maxpage + 1);
 
-    // Save new questionslist in database.
-    $quiz->questions = implode(',', $questions);
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
+    } else {
+        $lastslot = end($slots);
+        if ($lastslot) {
+            $slot->slot = $lastslot->slot + 1;
+        } else {
+            $slot->slot = 1;
+        }
+        if ($quiz->questionsperpage && $numonlastpage >= $quiz->questionsperpage) {
+            $slot->page = $maxpage + 1;
+        } else {
+            $slot->page = $maxpage;
+        }
+    }
 
-    // Add the new question instance.
-    $instance = new stdClass();
-    $instance->quizid = $quiz->id;
-    $instance->questionid = $id;
-    $instance->maxmark = $DB->get_field('question', 'defaultmark', array('id' => $id));
-    $DB->insert_record('quiz_question_instances', $instance);
+    $DB->insert_record('quiz_slots', $slot);
+    $trans->allow_commit();
 }
 
+/**
+ * Add a random question to the quiz at a given point.
+ * @param object $quiz the quiz settings.
+ * @param int $addonpage the page on which to add the question.
+ * @param int $categoryid the question category to add the question from.
+ * @param int $number the number of random questions to add.
+ * @param bool $includesubcategories whether to include questoins from subcategories.
+ */
 function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
         $includesubcategories) {
     global $DB;
@@ -195,7 +212,7 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
                 AND " . $DB->sql_compare_text('questiontext') . " = ?
                 AND NOT EXISTS (
                         SELECT *
-                          FROM {quiz_question_instances}
+                          FROM {quiz_slots}
                          WHERE questionid = q.id)
             ORDER BY id", array($category->id, $includesubcategories))) {
         // Take as many of these as needed.
@@ -228,136 +245,103 @@ function quiz_add_random_questions($quiz, $addonpage, $categoryid, $number,
 }
 
 /**
- * Add a page break after at particular position$.
- * @param string $layout the existinng layout, $quiz->questions.
- * @param int $index the position into $layout where the empty page should be removed.
- * @return the updated layout
+ * Add a page break after a particular slot.
+ * @param object $quiz the quiz settings.
+ * @param int $slot the slot to add the page break after.
  */
-function quiz_add_page_break_at($layout, $index) {
-    $questionids = explode(',', $layout);
-    if ($index < 0 || $index >= count($questionids)) {
-        return $layout;
-    }
-
-    array_splice($questionids, $index, 0, '0');
-
-    return implode(',', $questionids);
-}
-
-/**
- * Add a page break after a particular question.
- * @param string $layout the existinng layout, $quiz->questions.
- * @param int $qustionid the question to add the page break after.
- * @return the updated layout
- */
-function quiz_add_page_break_after($layout, $questionid) {
-    $questionids = explode(',', $layout);
-    $key = array_search($questionid, $questionids);
-    if ($key === false || !$questionid) {
-        return $layout;
-    }
-
-    array_splice($questionids, $key + 1, 0, '0');
-
-    return implode(',', $questionids);
-}
-
-/**
- * Update the database after $quiz->questions has been changed. For example,
- * this deletes preview attempts and updates $quiz->sumgrades.
- * @param $quiz the quiz object.
- */
-function quiz_save_new_layout($quiz) {
+function quiz_add_page_break_after_slot($quiz, $slot) {
     global $DB;
-    $DB->set_field('quiz', 'questions', $quiz->questions, array('id' => $quiz->id));
-    quiz_delete_previews($quiz);
-    quiz_update_sumgrades($quiz);
+
+    $DB->execute('UPDATE {quiz_slots} SET page = page + 1 WHERE quizid = ? AND slot > ?',
+            array($quiz->id, $slot));
 }
 
 /**
- * Save changes to question instance
+ * Change the max mark for a slot.
  *
- * Saves changes to the question grades in the quiz_question_instances table.
+ * Saves changes to the question grades in the quiz_slots table and any
+ * corresponding question_attempts.
  * It does not update 'sumgrades' in the quiz table.
  *
- * @param float    $maxmark    the maximal grade for the question.
- * @param int      $questionid the question id.
- * @param stdClass $quiz       the quiz settings.
+ * @param stdClass $slot    row from the quiz_slots table.
+ * @param float    $maxmark the new maxmark.
  */
-function quiz_update_question_instance($maxmark, $questionid, $quiz) {
+function quiz_update_slot_maxmark($slot, $maxmark) {
     global $DB;
-    $instance = $DB->get_record('quiz_question_instances', array('quizid' => $quiz->id,
-            'questionid' => $questionid));
-    $slot = quiz_get_slot_for_question($quiz, $questionid);
-
-    if (!$instance || !$slot) {
-        throw new coding_exception('Attempt to change the max mark of a quesion not in the quiz.');
-    }
 
-    if (abs($maxmark - $instance->maxmark) < 1e-7) {
+    if (abs($maxmark - $slot->maxmark) < 1e-7) {
         // Grade has not changed. Nothing to do.
         return;
     }
 
-    $instance->maxmark = $maxmark;
-    $DB->update_record('quiz_question_instances', $instance);
-    question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($quiz->id),
-            $slot, $maxmark);
+    $slot->maxmark = $maxmark;
+    $DB->update_record('quiz_slots', $slot);
+    question_engine::set_max_mark_in_attempts(new qubaids_for_quiz($slot->quizid),
+            $slot->slot, $maxmark);
 }
 
-// Private function used by the following two.
-function _quiz_move_question($layout, $questionid, $shift) {
-    if (!$questionid || !($shift == 1 || $shift == -1)) {
-        return $layout;
+/**
+ * Private function used by the following two.
+ * @param object $quiz the quiz settings.
+ * @param int $slotnumber the slot to move up.
+ * @param int $shift +1 means move down, -1 means move up.
+ */
+function _quiz_move_question($quiz, $slotnumber, $shift) {
+    global $DB;
+
+    if (!$slotnumber || !($shift == 1 || $shift == -1)) {
+        return;
     }
 
-    $questionids = explode(',', $layout);
-    $key = array_search($questionid, $questionids);
-    if ($key === false) {
-        return $layout;
+    $slot = $DB->get_record('quiz_slots',
+            array('quizid' => $quiz->id, 'slot' => $slotnumber));
+    if (!$slot) {
+        return;
     }
 
-    $otherkey = $key + $shift;
-    if ($otherkey < 0 || $otherkey >= count($questionids) - 1) {
-        return $layout;
+    $otherslot = $DB->get_record('quiz_slots',
+            array('quizid' => $quiz->id, 'slot' => $slotnumber + $shift));
+    if (!$otherslot) {
+        return;
     }
 
-    $temp = $questionids[$otherkey];
-    $questionids[$otherkey] = $questionids[$key];
-    $questionids[$key] = $temp;
+    if ($otherslot->page != $slot->page) {
+        $DB->set_field('quiz_slots', 'page', $slot->page + $shift, array('id' => $slot->id));
+        return;
+    }
 
-    return implode(',', $questionids);
+    $trans = $DB->start_delegated_transaction();
+    $DB->set_field('quiz_slots', 'slot', -1,               array('id' => $slot->id));
+    $DB->set_field('quiz_slots', 'slot', $slot->slot,      array('id' => $otherslot->id));
+    $DB->set_field('quiz_slots', 'slot', $otherslot->slot, array('id' => $slot->id));
+    $trans->allow_commit();
 }
 
 /**
- * Move a particular question one space earlier in the $quiz->questions list.
+ * Move a particular question one space earlier in the quiz.
  * If that is not possible, do nothing.
- * @param string $layout the existinng layout, $quiz->questions.
- * @param int $questionid the id of a question.
- * @return the updated layout
+ * @param object $quiz the quiz settings.
+ * @param int $slot the slot to move up.
  */
-function quiz_move_question_up($layout, $questionid) {
-    return _quiz_move_question($layout, $questionid, -1);
+function quiz_move_question_up($quiz, $slot) {
+    _quiz_move_question($quiz, $slot, -1);
 }
 
 /**
- * Move a particular question one space later in the $quiz->questions list.
+ * Move a particular question one space later in the quiz.
  * If that is not possible, do nothing.
- * @param string $layout the existinng layout, $quiz->questions.
- * @param int $questionid the id of a question.
- * @return the updated layout
+ * @param object $quiz the quiz settings.
+ * @param int $slot the slot to move down.
  */
-function quiz_move_question_down($layout, $questionid) {
-    return _quiz_move_question($layout, $questionid, +1);
+function quiz_move_question_down($quiz, $slot) {
+    return _quiz_move_question($quiz, $slot, 1);
 }
 
 /**
  * Prints a list of quiz questions for the edit.php main view for edit
  * ($reordertool = false) and order and paging ($reordertool = true) tabs
  *
- * @param object $quiz This is not the standard quiz object used elsewhere but
- *     it contains the quiz layout in $quiz->questions and the grades in
- *     $quiz->grades
+ * @param object $quiz The quiz settings.
  * @param moodle_url $pageurl The url of the current page with the parameters required
  *     for links returning to the current page, as a moodle_url object
  * @param bool $allowdelete Indicates whether the delete icons should be displayed
@@ -389,21 +373,14 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
     $strtype = get_string('type', 'quiz');
     $strpreview = get_string('preview', 'quiz');
 
-    if ($quiz->questions) {
-        list($usql, $params) = $DB->get_in_or_equal(explode(',', $quiz->questions));
-        $params[] = $quiz->id;
-        $questions = $DB->get_records_sql("SELECT q.*, qc.contextid, qqi.maxmark
-                              FROM {question} q
-                              JOIN {question_categories} qc ON qc.id = q.category
-                              JOIN {quiz_question_instances} qqi ON qqi.questionid = q.id
-                             WHERE q.id $usql AND qqi.quizid = ?", $params);
-    } else {
-        $questions = array();
-    }
+    $questions = $DB->get_records_sql("SELECT slot.slot, q.*, qc.contextid, slot.page, slot.maxmark
+                          FROM {quiz_slots} slot
+                     LEFT JOIN {question} q ON q.id = slot.questionid
+                     LEFT JOIN {question_categories} qc ON qc.id = q.category
+                         WHERE slot.quizid = ?
+                      ORDER BY slot.slot", array($quiz->id));
 
-    $layout = quiz_clean_layout($quiz->questions);
-    $order = explode(',', $layout);
-    $lastindex = count($order) - 1;
+    $lastindex = count($questions) - 1;
 
     $disabled = '';
     $pagingdisabled = '';
@@ -468,6 +445,18 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
         echo $reordercontrolstop;
     }
 
+    // Build fake order for backwards compatibility.
+    $currentpage = 1;
+    $order = array();
+    foreach ($questions as $question) {
+        while ($question->page > $currentpage) {
+            $currentpage += 1;
+            $order[] = 0;
+        }
+        $order[] = $question->slot;
+    }
+    $order[] = 0;
+
     // The current question ordinal (no descriptions).
     $qno = 1;
     // The current question (includes questions and descriptions).
@@ -476,28 +465,26 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
     $pagecount = 0;
 
     $pageopen = false;
+    $lastslot = 0;
 
     $returnurl = $pageurl->out_as_local_url(false);
     $questiontotalcount = count($order);
 
-    foreach ($order as $count => $qnum) {
+    foreach ($order as $count => $qnum) { // Note: $qnum is acutally slot number, if it is not 0.
 
         $reordercheckbox = '';
         $reordercheckboxlabel = '';
         $reordercheckboxlabelclose = '';
 
         // If the questiontype is missing change the question type.
-        if ($qnum && !array_key_exists($qnum, $questions)) {
-            $fakequestion = new stdClass();
-            $fakequestion->id = $qnum;
-            $fakequestion->category = 0;
-            $fakequestion->qtype = 'missingtype';
-            $fakequestion->name = get_string('missingquestion', 'quiz');
-            $fakequestion->questiontext = ' ';
-            $fakequestion->questiontextformat = FORMAT_HTML;
-            $fakequestion->length = 1;
-            $questions[$qnum] = $fakequestion;
-            $quiz->grades[$qnum] = 0;
+        if ($qnum && $questions[$qnum]->qtype === null) {
+            $questions[$qnum]->id = $qnum;
+            $questions[$qnum]->category = 0;
+            $questions[$qnum]->qtype = 'missingtype';
+            $questions[$qnum]->name = get_string('missingquestion', 'quiz');
+            $questions[$qnum]->questiontext = ' ';
+            $questions[$qnum]->questiontextformat = FORMAT_HTML;
+            $questions[$qnum]->length = 1;
 
         } else if ($qnum && !question_bank::qtype_exists($questions[$qnum]->qtype)) {
             $questions[$qnum]->qtype = 'missingtype';
@@ -521,7 +508,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                 if ($allowdelete) {
                     echo '<div class="quizpagedelete">';
                     echo $OUTPUT->action_icon($pageurl->out(true,
-                            array('deleteemptypage' => $count - 1, 'sesskey'=>sesskey())),
+                            array('deleteemptypage' => $pagecount, 'sesskey' => sesskey())),
                             new pix_icon('t/delete', $strremove),
                             new component_action('click',
                                     'M.core_scroll_manager.save_scroll_action'),
@@ -550,9 +537,9 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                 $reordercheckboxlabel = '';
                 $reordercheckboxlabelclose = '';
                 if ($reordertool) {
-                    $reordercheckbox = '<input type="checkbox" name="s' . $question->id .
-                        '" id="s' . $question->id . '" />';
-                    $reordercheckboxlabel = '<label for="s' . $question->id . '">';
+                    $reordercheckbox = '<input type="checkbox" name="s' . $question->slot .
+                        '" id="s' . $question->slot . '" />';
+                    $reordercheckboxlabel = '<label for="s' . $question->slot . '">';
                     $reordercheckboxlabelclose = '</label>';
                 }
                 if ($question->length == 0) {
@@ -582,7 +569,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                             $upbuttonclass = 'upwithoutdown';
                         }
                         echo $OUTPUT->action_icon($pageurl->out(true,
-                                array('up' => $question->id, 'sesskey'=>sesskey())),
+                                array('up' => $question->slot, 'sesskey' => sesskey())),
                                 new pix_icon('t/up', $strmoveup),
                                 new component_action('click',
                                         'M.core_scroll_manager.save_scroll_action'),
@@ -590,10 +577,10 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                     }
 
                 }
-                if ($count < $lastindex - 1) {
+                if ($count < $questiontotalcount - 2) {
                     if (!$hasattempts) {
                         echo $OUTPUT->action_icon($pageurl->out(true,
-                                array('down' => $question->id, 'sesskey'=>sesskey())),
+                                array('down' => $question->slot, 'sesskey' => sesskey())),
                                 new pix_icon('t/down', $strmovedown),
                                 new component_action('click',
                                         'M.core_scroll_manager.save_scroll_action'),
@@ -605,7 +592,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                     // Remove from quiz, not question delete.
                     if (!$hasattempts) {
                         echo $OUTPUT->action_icon($pageurl->out(true,
-                                array('remove' => $question->id, 'sesskey'=>sesskey())),
+                                array('remove' => $question->slot, 'sesskey' => sesskey())),
                                 new pix_icon('t/delete', $strremove),
                                 new component_action('click',
                                         'M.core_scroll_manager.save_scroll_action'),
@@ -619,15 +606,15 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
 <div class="points">
 <form method="post" action="edit.php" class="quizsavegradesform"><div>
     <fieldset class="invisiblefieldset" style="display: block;">
-    <label for="<?php echo "inputq$question->id" ?>"><?php echo $strmaxmark; ?></label>:<br />
+    <label for="<?php echo 'inputq' . $question->slot; ?>"><?php echo $strmaxmark; ?></label>:<br />
     <input type="hidden" name="sesskey" value="<?php echo sesskey() ?>" />
     <?php echo html_writer::input_hidden_params($pageurl); ?>
     <input type="hidden" name="savechanges" value="save" />
                     <?php
-                    echo '<input type="text" name="g' . $question->id .
-                            '" id="inputq' . $question->id .
+                    echo '<input type="text" name="g' . $question->slot .
+                            '" id="inputq' . $question->slot .
                             '" size="' . ($quiz->decimalpoints + 2) .
-                            '" value="' . (0 + $quiz->grades[$qnum]) .
+                            '" value="' . (0 + $question->maxmark) .
                             '" tabindex="' . ($lastindex + $qno) . '" />';
                     ?>
         <input type="submit" class="pointssubmitbutton" value="<?php echo $strsave; ?>" />
@@ -650,9 +637,9 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                         ?>
 <div class="qorder">
                         <?php
-                        echo '<label class="accesshide" for="o' . $question->id . '">' .
+                        echo '<label class="accesshide" for="o' . $question->slot . '">' .
                                 get_string('questionposition', 'quiz', $qnodisplay) . '</label>';
-                        echo '<input type="text" name="o' . $question->id .
+                        echo '<input type="text" name="o' . $question->slot .
                                 '" id="o' . $question->id . '"' .
                                 '" size="2" value="' . (10*$count + 10) .
                                 '" tabindex="' . ($lastindex + $qno) . '" />';
@@ -705,7 +692,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
                     echo $OUTPUT->container_start('addpage');
                     $url = new moodle_url($pageurl->out_omit_querystring(),
                             array('cmid' => $quiz->cmid, 'courseid' => $quiz->course,
-                                    'addpage' => $count, 'sesskey' => sesskey()));
+                                    'addpage' => $pagecount, 'sesskey' => sesskey()));
                     echo $OUTPUT->single_button($url, get_string('addpagehere', 'quiz'), 'post',
                             array('disabled' => $hasattempts,
                             'actions' => array(new component_action('click',
@@ -728,9 +715,7 @@ function quiz_print_question_list($quiz, $pageurl, $allowdelete, $reordertool,
  * Print all the controls for adding questions directly into the
  * specific page in the edit tab of edit.php
  *
- * @param object $quiz This is not the standard quiz object used elsewhere but
- *     it contains the quiz layout in $quiz->questions and the grades in
- *     $quiz->grades
+ * @param object $quiz The quiz settings.
  * @param moodle_url $pageurl The url of the current page with the parameters required
  *     for links returning to the current page, as a moodle_url object
  * @param int $page the current page number.
@@ -934,7 +919,7 @@ function quiz_print_singlequestion_reordertool($question, $returnurl, $quiz) {
  * @param object $questionurl The url of the question editing page as a moodle_url object
  * @param object $quiz The quiz in the context of which the question is being displayed
  */
-function quiz_print_randomquestion_reordertool(&$question, &$pageurl, &$quiz) {
+function quiz_print_randomquestion_reordertool($question, $pageurl, $quiz) {
     global $DB, $OUTPUT;
 
     // Load the category, and the number of available questions in it.
@@ -1311,7 +1296,7 @@ function quiz_print_grading_form($quiz, $pageurl, $tabindex) {
  * @param object $quiz The quiz object of the quiz in question
  */
 function quiz_print_status_bar($quiz) {
-    global $CFG;
+    global $DB;
 
     $bits = array();
 
@@ -1320,7 +1305,7 @@ function quiz_print_status_bar($quiz) {
             array('class' => 'totalpoints'));
 
     $bits[] = html_writer::tag('span',
-            get_string('numquestionsx', 'quiz', quiz_number_of_questions_in_quiz($quiz->questions)),
+            get_string('numquestionsx', 'quiz', $DB->count_records('quiz_slots', array('quizid' => $quiz->id))),
             array('class' => 'numberofquestions'));
 
     $timenow = time();
index 60ab922..d1afdb8 100644 (file)
@@ -81,7 +81,6 @@ function quiz_add_instance($quiz) {
 
     // Process the options from the form.
     $quiz->created = time();
-    $quiz->questions = '';
     $result = quiz_process_options($quiz);
     if ($result && is_string($result)) {
         return $result;
@@ -122,13 +121,6 @@ function quiz_update_instance($quiz, $mform) {
     $quiz->sumgrades = $oldquiz->sumgrades;
     $quiz->grade     = $oldquiz->grade;
 
-    // Repaginate, if asked to.
-    if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
-        $quiz->questions = quiz_repaginate(quiz_clean_layout($oldquiz->questions, true),
-                $quiz->questionsperpage);
-    }
-    unset($quiz->repaginatenow);
-
     // Update the database.
     $quiz->id = $quiz->instance;
     $DB->update_record('quiz', $quiz);
@@ -151,6 +143,11 @@ function quiz_update_instance($quiz, $mform) {
     // Delete any previous preview attempts.
     quiz_delete_previews($quiz);
 
+    // Repaginate, if asked to.
+    if (!$quiz->shufflequestions && !empty($quiz->repaginatenow)) {
+        quiz_repaginate_questions($quiz->id, $quiz->questionsperpage);
+    }
+
     return true;
 }
 
@@ -170,7 +167,7 @@ function quiz_delete_instance($id) {
     quiz_delete_all_attempts($quiz);
     quiz_delete_all_overrides($quiz);
 
-    $DB->delete_records('quiz_question_instances', array('quizid' => $quiz->id));
+    $DB->delete_records('quiz_slots', array('quizid' => $quiz->id));
     $DB->delete_records('quiz_feedback', array('quizid' => $quiz->id));
 
     $events = $DB->get_records('event', array('modulename' => 'quiz', 'instance' => $quiz->id));
@@ -1288,7 +1285,7 @@ function quiz_questions_in_use($questionids) {
     global $DB, $CFG;
     require_once($CFG->libdir . '/questionlib.php');
     list($test, $params) = $DB->get_in_or_equal($questionids);
-    return $DB->record_exists_select('quiz_question_instances',
+    return $DB->record_exists_select('quiz_slots',
             'questionid ' . $test, $params) || question_engine::questions_in_use(
             $questionids, new qubaid_join('{quiz_attempts} quiza',
             'quiza.uniqueid', 'quiza.preview = 0'));
index 480e7a5..72098e7 100644 (file)
@@ -110,10 +110,7 @@ function quiz_create_attempt(quiz $quizobj, $attemptnumber, $lastattempt, $timen
         $attempt->quiz = $quiz->id;
         $attempt->userid = $userid;
         $attempt->preview = 0;
-        $attempt->layout = quiz_clean_layout($quiz->questions, true);
-        if ($quiz->shufflequestions) {
-            $attempt->layout = quiz_repaginate($attempt->layout, $quiz->questionsperpage, true);
-        }
+        $attempt->layout = '';
     } else {
         // Build on last attempt.
         if (empty($lastattempt)) {
@@ -165,9 +162,8 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
     $quizobj->load_questions();
 
     // Add them all to the $quba.
-    $idstoslots = array();
     $questionsinuse = array_keys($quizobj->get_questions());
-    foreach ($quizobj->get_questions() as $i => $questiondata) {
+    foreach ($quizobj->get_questions() as $questiondata) {
         if ($questiondata->qtype != 'random') {
             if (!$quizobj->get_quiz()->shuffleanswers) {
                 $questiondata->options->shuffleanswers = false;
@@ -189,7 +185,7 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
             }
         }
 
-        $idstoslots[$i] = $quba->add_question($question, $questiondata->maxmark);
+        $quba->add_question($question, $questiondata->maxmark);
         $questionsinuse[] = $question->id;
     }
 
@@ -211,16 +207,36 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
 
     $quba->start_all_questions($variantstrategy, $timenow);
 
-    // Update attempt layout.
-    $newlayout = array();
-    foreach (explode(',', $attempt->layout) as $qid) {
-        if ($qid != 0) {
-            $newlayout[] = $idstoslots[$qid];
-        } else {
-            $newlayout[] = 0;
+    // Work out the attempt layout.
+    $layout = array();
+    if ($quizobj->get_quiz()->shufflequestions) {
+        $slots = $quba->get_slots();
+        shuffle($slots);
+
+        $questionsonthispage = 0;
+        foreach ($slots as $slot) {
+            if ($questionsonthispage && $questionsonthispage == $quizobj->get_quiz()->questionsperpage) {
+                $layout[] = 0;
+                $questionsonthispage = 0;
+            }
+            $layout[] = $slot;
+            $questionsonthispage += 1;
+        }
+
+    } else {
+        $currentpage = null;
+        foreach ($quizobj->get_questions() as $slot) {
+            if ($currentpage !== null && $slot->page != $currentpage) {
+                $layout[] = 0;
+            }
+            $layout[] = $slot->slot;
+            $currentpage = $slot->page;
         }
     }
-    $attempt->layout = implode(',', $newlayout);
+
+    $layout[] = 0;
+    $attempt->layout = implode(',', $layout);
+
     return $attempt;
 }
 
@@ -386,127 +402,35 @@ function quiz_has_attempts($quizid) {
 // Functions to do with quiz layout and pages //////////////////////////////////
 
 /**
- * Returns a comma separated list of question ids for the quiz
- *
- * @param string $layout The string representing the quiz layout. Each page is
- *      represented as a comma separated list of question ids and 0 indicating
- *      page breaks. So 5,2,0,3,0 means questions 5 and 2 on page 1 and question
- *      3 on page 2
- * @return string comma separated list of question ids, without page breaks.
+ * Repaginate the questions in a quiz
+ * @param int $quizid the id of the quiz to repaginate.
+ * @param int $slotsperpage number of items to put on each page. 0 means unlimited.
  */
-function quiz_questions_in_quiz($layout) {
-    $questions = str_replace(',0', '', quiz_clean_layout($layout, true));
-    if ($questions === '0') {
-        return '';
-    } else {
-        return $questions;
-    }
-}
-
-/**
- * Returns the number of pages in a quiz layout
- *
- * @param string $layout The string representing the quiz layout. Always ends in ,0
- * @return int The number of pages in the quiz.
- */
-function quiz_number_of_pages($layout) {
-    return substr_count(',' . $layout, ',0');
-}
-
-/**
- * Returns the number of questions in the quiz layout
- *
- * @param string $layout the string representing the quiz layout.
- * @return int The number of questions in the quiz.
- */
-function quiz_number_of_questions_in_quiz($layout) {
-    $layout = quiz_questions_in_quiz(quiz_clean_layout($layout));
-    $count = substr_count($layout, ',');
-    if ($layout !== '') {
-        $count++;
-    }
-    return $count;
-}
-
-/**
- * Re-paginates the quiz layout
- *
- * @param string $layout  The string representing the quiz layout. If there is
- *      if there is any doubt about the quality of the input data, call
- *      quiz_clean_layout before you call this function.
- * @param int $perpage The number of questions per page
- * @param bool $shuffle Should the questions be reordered randomly?
- * @return string the new layout string
- */
-function quiz_repaginate($layout, $perpage, $shuffle = false) {
-    $questions = quiz_questions_in_quiz($layout);
-    if (!$questions) {
-        return '0';
-    }
+function quiz_repaginate_questions($quizid, $slotsperpage) {
+    global $DB;
+    $trans = $DB->start_delegated_transaction();
 
-    $questions = explode(',', quiz_questions_in_quiz($layout));
-    if ($shuffle) {
-        shuffle($questions);
-    }
+    $slots = $DB->get_records('quiz_slots', array('quizid' => $quizid),
+            'slot');
 
-    $onthispage = 0;
-    $layout = array();
-    foreach ($questions as $question) {
-        if ($perpage and $onthispage >= $perpage) {
-            $layout[] = 0;
-            $onthispage = 0;
+    $currentpage = 1;
+    $slotsonthispage = 0;
+    foreach ($slots as $slot) {
+        if ($slotsonthispage && $slotsonthispage == $slotsperpage) {
+            $currentpage += 1;
+            $slotsonthispage = 0;
         }
-        $layout[] = $question;
-        $onthispage += 1;
+        if ($slot->page != $currentpage) {
+            $DB->set_field('quiz_slots', 'page', $currentpage, array('id' => $slot->id));
+        }
+        $slotsonthispage += 1;
     }
 
-    $layout[] = 0;
-    return implode(',', $layout);
+    $trans->allow_commit();
 }
 
 // Functions to do with quiz grades ////////////////////////////////////////////
 
-/**
- * Creates an array of maximum grades for a quiz
- *
- * The grades are extracted from the quiz_question_instances table.
- * @param object $quiz The quiz settings.
- * @return array of grades indexed by question id. These are the maximum
- *      possible grades that students can achieve for each of the questions.
- */
-function quiz_get_all_question_grades($quiz) {
-    global $CFG, $DB;
-
-    $questionlist = quiz_questions_in_quiz($quiz->questions);
-    if (empty($questionlist)) {
-        return array();
-    }
-
-    $params = array($quiz->id);
-    $wheresql = '';
-    if (!is_null($questionlist)) {
-        list($usql, $question_params) = $DB->get_in_or_equal(explode(',', $questionlist));
-        $wheresql = ' AND questionid ' . $usql;
-        $params = array_merge($params, $question_params);
-    }
-
-    $instances = $DB->get_records_sql("SELECT questionid, maxmark, id
-                                    FROM {quiz_question_instances}
-                                    WHERE quizid = ?{$wheresql}", $params);
-
-    $list = explode(',', $questionlist);
-    $grades = array();
-
-    foreach ($list as $qid) {
-        if (isset($instances[$qid])) {
-            $grades[$qid] = $instances[$qid]->maxmark;
-        } else {
-            $grades[$qid] = 1;
-        }
-    }
-    return $grades;
-}
-
 /**
  * Convert the raw grade stored in $attempt into a grade out of the maximum
  * grade for this quiz.
@@ -602,7 +526,7 @@ function quiz_update_sumgrades($quiz) {
     $sql = 'UPDATE {quiz}
             SET sumgrades = COALESCE((
                 SELECT SUM(maxmark)
-                FROM {quiz_question_instances}
+                FROM {quiz_slots}
                 WHERE quizid = {quiz}.id
             ), 0)
             WHERE id = ?';
@@ -1425,73 +1349,6 @@ function quiz_get_combined_reviewoptions($quiz, $attempts) {
     return array($someoptions, $alloptions);
 }
 
-/**
- * Clean the question layout from various possible anomalies:
- * - Remove consecutive ","'s
- * - Remove duplicate question id's
- * - Remove extra "," from beginning and end
- * - Finally, add a ",0" in the end if there is none
- *
- * @param $string $layout the quiz layout to clean up, usually from $quiz->questions.
- * @param bool $removeemptypages If true, remove empty pages from the quiz. False by default.
- * @return $string the cleaned-up layout
- */
-function quiz_clean_layout($layout, $removeemptypages = false) {
-    // Remove repeated ','s. This can happen when a restore fails to find the right
-    // id to relink to.
-    $layout = preg_replace('/,{2,}/', ',', trim($layout, ','));
-
-    // Remove duplicate question ids.
-    $layout = explode(',', $layout);
-    $cleanerlayout = array();
-    $seen = array();
-    foreach ($layout as $item) {
-        if ($item == 0) {
-            $cleanerlayout[] = '0';
-        } else if (!in_array($item, $seen)) {
-            $cleanerlayout[] = $item;
-            $seen[] = $item;
-        }
-    }
-
-    if ($removeemptypages) {
-        // Avoid duplicate page breaks.
-        $layout = $cleanerlayout;
-        $cleanerlayout = array();
-        $stripfollowingbreaks = true; // Ensure breaks are stripped from the start.
-        foreach ($layout as $item) {
-            if ($stripfollowingbreaks && $item == 0) {
-                continue;
-            }
-            $cleanerlayout[] = $item;
-            $stripfollowingbreaks = $item == 0;
-        }
-    }
-
-    // Add a page break at the end if there is none.
-    if (end($cleanerlayout) !== '0') {
-        $cleanerlayout[] = '0';
-    }
-
-    return implode(',', $cleanerlayout);
-}
-
-/**
- * Get the slot for a question with a particular id.
- * @param object $quiz the quiz settings.
- * @param int $questionid the of a question in the quiz.
- * @return int the corresponding slot. Null if the question is not in the quiz.
- */
-function quiz_get_slot_for_question($quiz, $questionid) {
-    $questionids = quiz_questions_in_quiz($quiz->questions);
-    foreach (explode(',', $questionids) as $key => $id) {
-        if ($id == $questionid) {
-            return $key + 1;
-        }
-    }
-    return null;
-}
-
 // Functions for sending notification messages /////////////////////////////////
 
 /**
index 344bc37..617431d 100644 (file)
@@ -123,9 +123,9 @@ class quiz_grading_report extends quiz_default_report {
                     $this->currentgroup, '', false);
         }
 
-        $questionsinquiz = quiz_questions_in_quiz($quiz->questions);
+        $hasquestions = quiz_has_questions($quiz->id);
         $counts = null;
-        if ($slot && $questionsinquiz) {
+        if ($slot && $hasquestions) {
             // Make sure there is something to do.
             $statecounts = $this->get_question_state_summary(array($slot));
             foreach ($statecounts as $record) {
@@ -144,7 +144,7 @@ class quiz_grading_report extends quiz_default_report {
         $this->print_header_and_tabs($cm, $course, $quiz, 'grading');
 
         // What sort of page to display?
-        if (!$questionsinquiz) {
+        if (!$hasquestions) {
             echo quiz_no_questions_message($quiz, $cm, $this->context);
 
         } else if (!$slot) {
index 6430291..ebd571c 100644 (file)
@@ -102,7 +102,7 @@ class quiz_overview_report extends quiz_attempts_report {
             }
         }
 
-        $hasquestions = quiz_questions_in_quiz($quiz->questions);
+        $hasquestions = quiz_has_questions($quiz->id);
         if (!$table->is_downloading()) {
             if (!$hasquestions) {
                 echo quiz_no_questions_message($quiz, $cm, $this->context);
index cd01c64..f75ced3 100644 (file)
@@ -77,6 +77,15 @@ function quiz_report_unindex($datum) {
     return $datumunkeyed;
 }
 
+/**
+ * Are there any questions in this quiz?
+ * @param int $quizid the quiz id.
+ */
+function quiz_has_questions($quizid) {
+    global $DB;
+    return $DB->record_exists('quiz_slots', array('quizid' => $quizid));
+}
+
 /**
  * Get the slots of real questions (not descriptions) in this quiz, in order.
  * @param object $quiz the quiz.
@@ -86,41 +95,23 @@ function quiz_report_unindex($datum) {
 function quiz_report_get_significant_questions($quiz) {
     global $DB;
 
-    $questionids = quiz_questions_in_quiz($quiz->questions);
-    if (empty($questionids)) {
-        return array();
-    }
+    $qsbyslot = $DB->get_records_sql("
+            SELECT slot.slot,
+                   q.id,
+                   q.length,
+                   slot.maxmark
 
-    list($usql, $params) = $DB->get_in_or_equal(explode(',', $questionids));
-    $params[] = $quiz->id;
-    $questions = $DB->get_records_sql("
-SELECT
-    q.id,
-    q.length,
-    qqi.maxmark
+              FROM {question} q
+              JOIN {quiz_slots} slot ON slot.questionid = q.id
 
-FROM {question} q
-JOIN {quiz_question_instances} qqi ON qqi.questionid = q.id
+             WHERE slot.quizid = ?
+               AND q.length > 0
 
-WHERE
-    q.id $usql AND
-    qqi.quizid = ? AND
-    q.length > 0", $params);
+          ORDER BY slot.slot", array($quiz->id));
 
-    $qsbyslot = array();
     $number = 1;
-    foreach (explode(',', $questionids) as $key => $id) {
-        if (!array_key_exists($id, $questions)) {
-            continue;
-        }
-
-        $slot = $key + 1;
-        $question = $questions[$id];
-        $question->slot = $slot;
+    foreach ($qsbyslot as $question) {
         $question->number = $number;
-
-        $qsbyslot[$slot] = $question;
-
         $number += $question->length;
     }
 
index d97cd03..dab6fa6 100644 (file)
@@ -115,7 +115,7 @@ class quiz_responses_report extends quiz_attempts_report {
             }
         }
 
-        $hasquestions = quiz_questions_in_quiz($quiz->questions);
+        $hasquestions = quiz_has_questions($quiz->id);
         if (!$table->is_downloading()) {
             if (!$hasquestions) {
                 echo quiz_no_questions_message($quiz, $cm, $this->context);
index daa540c..6a41ddf 100644 (file)
@@ -59,7 +59,7 @@ class quiz_statistics_report extends quiz_default_report {
 
         $this->context = context_module::instance($cm->id);
 
-        if (!quiz_questions_in_quiz($quiz->questions)) {
+        if (!quiz_has_questions($quiz->id)) {
             $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
             echo quiz_no_questions_message($quiz, $cm, $this->context);
             return true;
index 4cf3d74..5598b3d 100644 (file)
@@ -113,10 +113,7 @@ class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
         $this->randqids = array();
         foreach ($slots as $slotno => $slotquestion) {
             if ($slotquestion['type'] !== 'random') {
-                quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0);
-                // Setting default mark above does not affect the grade for multi-answer question type (and maybe others??).
-                // Set the mark again just to be sure.
-                quiz_update_question_instance($slotquestion['mark'], $slotquestion['id'], $this->quiz);
+                quiz_add_quiz_question($slotquestion['id'], $this->quiz, 0, $slotquestion['mark']);
             } else {
                 quiz_add_random_questions($this->quiz, 0, $slotquestion['catid'], 1, 0);
                 $this->randqids[$slotno] = $qidsbycat[$slotquestion['catid']];
index 862823a..cb9ceba 100644 (file)
@@ -79,6 +79,7 @@ class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase {
         $attempt = quiz_create_attempt($quizobj, 1, false, $timenow, false, $user1->id);
 
         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow);
+        $this->assertEquals('1,2,0', $attempt->layout);
 
         quiz_attempt_save_started($quizobj, $quba, $attempt);
 
@@ -132,7 +133,7 @@ class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase {
         // Make a quiz.
         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 
-        $quiz = $quizgenerator->create_instance(array('course'=>$SITE->id, 'questionsperpage' => 0, 'grade' => 100.0,
+        $quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 2, 'grade' => 100.0,
                                                       'sumgrades' => 4));
 
         $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
@@ -174,6 +175,7 @@ class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase {
             $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
 
             quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(1 => $randomqidtoselect));
+            $this->assertEquals('1,2,0,3,4,0', $attempt->layout);
 
             quiz_attempt_save_started($quizobj, $quba, $attempt);
 
@@ -266,8 +268,10 @@ class mod_quiz_attempt_walkthrough_testcase extends advanced_testcase {
 
         $timenow = time();
         $attempt = quiz_create_attempt($quizobj, 1, false, $timenow);
+
         // Select variant.
         quiz_start_new_attempt($quizobj, $quba, $attempt, 1, $timenow, array(), array(1 => $variantno));
+        $this->assertEquals('1,0', $attempt->layout);
         quiz_attempt_save_started($quizobj, $quba, $attempt);
 
         // Process some responses from the student.
index a1b011d..8acf988 100644 (file)
@@ -37,52 +37,6 @@ require_once($CFG->dirroot . '/mod/quiz/editlib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class mod_quiz_editlib_testcase extends basic_testcase {
-    public function test_quiz_move_question_up() {
-        $this->assertEquals(quiz_move_question_up('0', 123), '0');
-        $this->assertEquals(quiz_move_question_up('1,2,0', 1), '1,2,0');
-        $this->assertEquals(quiz_move_question_up('1,2,0', 0), '1,2,0');
-        $this->assertEquals(quiz_move_question_up('1,2,0', 2), '2,1,0');
-        $this->assertEquals(quiz_move_question_up('1,2,0,3,4,0', 3), '1,2,3,0,4,0');
-        $this->assertEquals(quiz_move_question_up('1,2,3,0,4,0', 4), '1,2,3,4,0,0');
-    }
-
-    public function test_quiz_move_question_down() {
-        $this->assertEquals(quiz_move_question_down('0', 123), '0');
-        $this->assertEquals(quiz_move_question_down('1,2,0', 2), '1,2,0');
-        $this->assertEquals(quiz_move_question_down('1,2,0', 0), '1,2,0');
-        $this->assertEquals(quiz_move_question_down('1,2,0', 1), '2,1,0');
-        $this->assertEquals(quiz_move_question_down('1,2,0,3,4,0', 2), '1,0,2,3,4,0');
-        $this->assertEquals(quiz_move_question_down('1,0,2,3,0,4,0', 1), '0,1,2,3,0,4,0');
-    }
-
-    public function test_quiz_delete_empty_page() {
-        $this->assertEquals(quiz_delete_empty_page('0', 0), '0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0', 2), '1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('0,1,2,0', -1), '1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('0,1,2,0', 0), '0,1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0', 3), '1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0', -1), '1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0,0', 2), '1,2,0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0,0', 1), '1,2,0,0');
-        $this->assertEquals(quiz_delete_empty_page('1,2,0,0,3,4,0', 2), '1,2,0,3,4,0');
-        $this->assertEquals(quiz_delete_empty_page('0,0,1,2,0', 0), '0,1,2,0');
-    }
-
-    public function test_quiz_add_page_break_after() {
-        $this->assertEquals(quiz_add_page_break_after('0', 1), '0');
-        $this->assertEquals(quiz_add_page_break_after('1,2,0', 1), '1,0,2,0');
-        $this->assertEquals(quiz_add_page_break_after('1,2,0', 2), '1,2,0,0');
-        $this->assertEquals(quiz_add_page_break_after('1,2,0', 0), '1,2,0');
-    }
-
-    public function test_quiz_add_page_break_at() {
-        $this->assertEquals(quiz_add_page_break_at('0', 0), '0,0');
-        $this->assertEquals(quiz_add_page_break_at('1,2,0', 0), '0,1,2,0');
-        $this->assertEquals(quiz_add_page_break_at('1,2,0', 1), '1,0,2,0');
-        $this->assertEquals(quiz_add_page_break_at('1,2,0', 2), '1,2,0,0');
-        $this->assertEquals(quiz_add_page_break_at('1,2,0', 3), '1,2,0');
-    }
-
     public function test_quiz_question_tostring() {
         $question = new stdClass();
         $question->qtype = 'multichoice';
index 6e45917..c13fc2f 100644 (file)
@@ -52,7 +52,6 @@ class mod_quiz_generator extends testing_module_generator {
             'questionsperpage'       => 1,
             'shufflequestions'       => 0,
             'shuffleanswers'         => 1,
-            'questions'              => '',
             'sumgrades'              => 0,
             'grade'                  => 0,
             'timecreated'            => time(),
index 177947c..e4575f5 100644 (file)
@@ -37,96 +37,6 @@ require_once($CFG->dirroot . '/mod/quiz/locallib.php');
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class mod_quiz_locallib_testcase extends basic_testcase {
-    public function test_quiz_questions_in_quiz() {
-        $this->assertEquals(quiz_questions_in_quiz(''), '');
-        $this->assertEquals(quiz_questions_in_quiz('0'), '');
-        $this->assertEquals(quiz_questions_in_quiz('0,0'), '');
-        $this->assertEquals(quiz_questions_in_quiz('0,0,0'), '');
-        $this->assertEquals(quiz_questions_in_quiz('1'), '1');
-        $this->assertEquals(quiz_questions_in_quiz('1,2'), '1,2');
-        $this->assertEquals(quiz_questions_in_quiz('1,0,2'), '1,2');
-        $this->assertEquals(quiz_questions_in_quiz('0,1,0,0,2,0'), '1,2');
-    }
-
-    public function test_quiz_number_of_pages() {
-        $this->assertEquals(quiz_number_of_pages('0'), 1);
-        $this->assertEquals(quiz_number_of_pages('0,0'), 2);
-        $this->assertEquals(quiz_number_of_pages('0,0,0'), 3);
-        $this->assertEquals(quiz_number_of_pages('1,0'), 1);
-        $this->assertEquals(quiz_number_of_pages('1,2,0'), 1);
-        $this->assertEquals(quiz_number_of_pages('1,0,2,0'), 2);
-        $this->assertEquals(quiz_number_of_pages('1,2,3,0'), 1);
-        $this->assertEquals(quiz_number_of_pages('1,2,3,0'), 1);
-        $this->assertEquals(quiz_number_of_pages('0,1,0,0,2,0'), 4);
-    }
-
-    public function test_quiz_number_of_questions_in_quiz() {
-        $this->assertEquals(quiz_number_of_questions_in_quiz('0'), 0);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('0,0'), 0);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('0,0,0'), 0);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('1,0'), 1);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('1,2,0'), 2);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('1,0,2,0'), 2);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('1,2,3,0'), 3);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('1,2,3,0'), 3);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('0,1,0,0,2,0'), 2);
-        $this->assertEquals(quiz_number_of_questions_in_quiz('10,,0,0'), 1);
-    }
-
-    public function test_quiz_clean_layout() {
-        // Without stripping empty pages.
-        $this->assertEquals(quiz_clean_layout(',,1,,,2,,'), '1,2,0');
-        $this->assertEquals(quiz_clean_layout(''), '0');
-        $this->assertEquals(quiz_clean_layout('0'), '0');
-        $this->assertEquals(quiz_clean_layout('0,0'), '0,0');
-        $this->assertEquals(quiz_clean_layout('0,0,0'), '0,0,0');
-        $this->assertEquals(quiz_clean_layout('1'), '1,0');
-        $this->assertEquals(quiz_clean_layout('1,2'), '1,2,0');
-        $this->assertEquals(quiz_clean_layout('1,0,2'), '1,0,2,0');
-        $this->assertEquals(quiz_clean_layout('0,1,0,0,2,0'), '0,1,0,0,2,0');
-
-        // With stripping empty pages.
-        $this->assertEquals(quiz_clean_layout('', true), '0');
-        $this->assertEquals(quiz_clean_layout('0', true), '0');
-        $this->assertEquals(quiz_clean_layout('0,0', true), '0');
-        $this->assertEquals(quiz_clean_layout('0,0,0', true), '0');
-        $this->assertEquals(quiz_clean_layout('1', true), '1,0');
-        $this->assertEquals(quiz_clean_layout('1,2', true), '1,2,0');
-        $this->assertEquals(quiz_clean_layout('1,0,2', true), '1,0,2,0');
-        $this->assertEquals(quiz_clean_layout('0,1,0,0,2,0', true), '1,0,2,0');
-    }
-
-    public function test_quiz_repaginate() {
-        // Test starting with 1 question per page.
-        $this->assertEquals(quiz_repaginate('1,0,2,0,3,0', 0), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('1,0,2,0,3,0', 3), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('1,0,2,0,3,0', 2), '1,2,0,3,0');
-        $this->assertEquals(quiz_repaginate('1,0,2,0,3,0', 1), '1,0,2,0,3,0');
-
-        // Test starting with all on one page page.
-        $this->assertEquals(quiz_repaginate('1,2,3,0', 0), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('1,2,3,0', 3), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('1,2,3,0', 2), '1,2,0,3,0');
-        $this->assertEquals(quiz_repaginate('1,2,3,0', 1), '1,0,2,0,3,0');
-
-        // Test single question case.
-        $this->assertEquals(quiz_repaginate('100,0', 0), '100,0');
-        $this->assertEquals(quiz_repaginate('100,0', 1), '100,0');
-
-        // No questions case.
-        $this->assertEquals(quiz_repaginate('0', 0), '0');
-
-        // Test empty pages are removed.
-        $this->assertEquals(quiz_repaginate('1,2,3,0,0,0', 0), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('1,0,0,0,2,3,0', 0), '1,2,3,0');
-        $this->assertEquals(quiz_repaginate('0,0,0,1,2,3,0', 0), '1,2,3,0');
-
-        // Test shuffle option.
-        $this->assertTrue(in_array(quiz_repaginate('1,2,0', 0, true),
-            array('1,2,0', '2,1,0')));
-        $this->assertTrue(in_array(quiz_repaginate('1,2,0', 1, true),
-            array('1,0,2,0', '2,0,1,0')));
-    }
 
     public function test_quiz_rescale_grade() {
         $quiz = new stdClass();
@@ -145,13 +55,6 @@ class mod_quiz_locallib_testcase extends basic_testcase {
             format_float(0.247, 3));
     }
 
-    public function test_quiz_get_slot_for_question() {
-        $quiz = new stdClass();
-        $quiz->questions = '1,2,0,7,0';
-        $this->assertEquals(1, quiz_get_slot_for_question($quiz, 1));
-        $this->assertEquals(3, quiz_get_slot_for_question($quiz, 7));
-    }
-
     public function test_quiz_attempt_state_in_progress() {
         $attempt = new stdClass();
         $attempt->state = quiz_attempt::IN_PROGRESS;
index b105356..defff60 100644 (file)
@@ -42,7 +42,6 @@ class mod_quiz_class_testcase extends basic_testcase {
         $quiz->reviewattempt = 0x10010;
         $quiz->timeclose = 0;
         $quiz->attempts = 0;
-        $quiz->questions = '1,2,0,3,4,0';
 
         $cm = new stdClass();
         $cm->id = 123;
@@ -65,19 +64,4 @@ class mod_quiz_class_testcase extends basic_testcase {
         $this->assertEquals(get_string('noreviewuntil', 'quiz', userdate($closetime)),
             $quizobj->cannot_review_message(mod_quiz_display_options::LATER_WHILE_OPEN));
     }
-
-    public function test_empty_quiz() {
-        $quiz = new stdClass();
-        $quiz->reviewattempt = 0x10010;
-        $quiz->timeclose = 0;
-        $quiz->attempts = 0;
-        $quiz->questions = '0';
-
-        $cm = new stdClass();
-        $cm->id = 123;
-
-        $quizobj = new quiz($quiz, $cm, new stdClass(), false);
-
-        $this->assertFalse($quizobj->has_questions());
-    }
 }
index 6b63887..006ca9d 100644 (file)
@@ -2,14 +2,36 @@ This files describes API changes in the quiz code.
 
 === 2.7 ===
 
-* The columns of the quiz_question_instances table have been renamed to match
-  the coding guidelines. Specifically
+* The old quiz.questions database column (comma-separated list of question ids)
+  is gone, and instead the quiz_question_instances table has been renamed to
+  to quiz_slots. Some of the columns of that table have been renamed to match
+  the coding guidelines. Specifically:
       quiz     -> quizid
       question -> questionid
       grade    -> maxmark
-
+  also there are two new columns:
+      slot     -  numbers the questions in the quiz in order, as on the edit quiz page.
+      page     -  new way to determine which question is on which page.
+  naturally, other parts of the code and APIs have been updated to reflect that
+  change.
+
+* The following functions, which were part of the internal workings of the quiz,
+  have been removed.
+      quiz_get_slot_for_question
+      quiz_number_of_questions_in_quiz
+      quiz_repaginate               (there is now a quiz_repaginate_questions with a different API).
+      quiz_add_page_break_at        (see quiz_add_page_break_after_slot)
+      quiz_add_page_break_after     (see quiz_add_page_break_after_slot)
+      quiz_number_of_pages
+      quiz_remove_question          (see quiz_remove_slot)
+      quiz_update_question_instance (see quiz_update_slot_maxmark)
+
+* The following internal functions have had their API changed.
+      quiz_delete_empty_page: has had its arguments changed to $quiz and $pagenumber.
+      quiz_has_question_use: now takes $quiz and $slot, not $questionid.
 
 === 2.6 ===
+
 * As part of improving the page usability and accessibility, we updated the
   heading levels for quiz module so it has a proper nesting. (MDL-41615)
 
index c130634..4943793 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014021300; // The current module version (Date: YYYYMMDDXX).
+$plugin->version   = 2014022008; // The current module version (Date: YYYYMMDDXX).
 $plugin->requires  = 2013110500; // Requires this Moodle version.
 $plugin->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics).
 $plugin->cron      = 60;
index 877cb60..0a028f2 100644 (file)
@@ -184,7 +184,7 @@ if ($quiz->attempts != 1) {
 }
 
 // Determine wheter a start attempt button should be displayed.
-$viewobj->quizhasquestions = (bool) quiz_clean_layout($quiz->questions, true);
+$viewobj->quizhasquestions = $quizobj->has_questions();
 $viewobj->preventmessages = array();
 if (!$viewobj->quizhasquestions) {
     $viewobj->buttontext = '';
index 3901b29..e62e7ab 100644 (file)
@@ -286,10 +286,10 @@ class question_engine_upgrade_question_loader {
 
         if ($quizid) {
             $question = $DB->get_record_sql("
-                SELECT q.*, qqi.maxmark
+                SELECT q.*, slot.maxmark
                 FROM {question} q
-                JOIN {quiz_question_instances} qqi ON qqi.questionid = q.id
-                WHERE q.id = ? AND qqi.quizid = ?", array($questionid, $quizid));
+                JOIN {quiz_slots} slot ON slot.questionid = q.id
+                WHERE q.id = ? AND slot.quizid = ?", array($questionid, $quizid));
         } else {
             $question = $DB->get_record('question', array('id' => $questionid));
         }
index 9cf6fd2..b2e3b2a 100644 (file)
@@ -1706,12 +1706,12 @@ class qtype_calculated extends question_type {
                     $line++;
                     $text .= "<td align=\"left\" style=\"white-space:nowrap;\">$qu->name</td>";
                     // TODO MDL-43779 should not have quiz-specific code here.
-                    $nbofquiz = $DB->count_records('quiz_question_instances', array('questionid' => $qu->id));
+                    $nbofquiz = $DB->count_records('quiz_slots', array('questionid' => $qu->id));
                     $nbofattempts = $DB->count_records_sql("
                             SELECT count(1)
-                              FROM {quiz_question_instances} qqi
-                              JOIN {quiz_attempts} quiza ON quiza.quiz = qqi.quizid
-                             WHERE qqi.questionid = ?
+                              FROM {quiz_slots} slot
+                              JOIN {quiz_attempts} quiza ON quiza.quiz = slot.quizid
+                             WHERE slot.questionid = ?
                                AND quiza.preview = 0", array($qu->id));
                     if ($nbofquiz > 0) {
                         $text .= "<td align=\"center\">$nbofquiz</td>";
index dacb346..43ba1b7 100644 (file)
@@ -55,7 +55,6 @@ class qtype_calculated_attempt_upgrader_test extends question_attempt_upgrader_t
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -254,7 +253,6 @@ Remember to type a unit.',
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -496,7 +494,6 @@ Remember to type a unit.',
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index 923acad..29e7a8e 100644 (file)
@@ -55,7 +55,6 @@ class qtype_calculatedmulti_attempt_upgrader_test extends question_attempt_upgra
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -276,7 +275,6 @@ class qtype_calculatedmulti_attempt_upgrader_test extends question_attempt_upgra
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -517,7 +515,6 @@ class qtype_calculatedmulti_attempt_upgrader_test extends question_attempt_upgra
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index cc241ff..385e38a 100644 (file)
@@ -55,7 +55,6 @@ class qtype_calculatedsimple_attempt_upgrader_test extends question_attempt_upgr
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -241,7 +240,6 @@ class qtype_calculatedsimple_attempt_upgrader_test extends question_attempt_upgr
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -467,7 +465,6 @@ class qtype_calculatedsimple_attempt_upgrader_test extends question_attempt_upgr
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '16,0,17,0,18,0',
             'sumgrades' => '3.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index 6a93545..b9528c4 100644 (file)
@@ -60,7 +60,6 @@ class qtype_description_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '4940,0,5043,0,4945,0,4942,0,5566,0',
             'sumgrades' => '5',
             'grade' => '10',
             'timecreated' => '0',
@@ -206,7 +205,6 @@ class qtype_description_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '8492,0,8487,8488,8489,8490,0,8441,0,8443,0,8486,0,8444,0,8445,0,8494,0,8446,8429,0,8447,8430,0,8448,8431,0,8449,8432,0,8450,8433,0,8451,8434,0,8452,8435,0,8453,8436,0,8454,8437,0,8493,0,8455,8456,8457,0,8458,8459,8460,0,8461,8462,8463,8438,0,8464,8465,8466,0,8467,8468,8469,0,8470,8471,8472,8439,0,8473,8440,0,8474,8475,8476,0,8477,8478,8479,0,8480,8481,8482,0,8483,8484,8485,8442,0',
             'sumgrades' => '0',
             'grade' => '10',
             'timecreated' => '0',
@@ -355,7 +353,6 @@ class qtype_description_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '8691,0,8690,8692,8693,8694,0,8695,0,8696,0,8697,0,8698,0,8699,0,8700,0,8701,8702,0,8703,8704,0,8705,8706,0,8707,8708,0,8709,8710,0,8711,8712,0,8713,8714,0,8715,8716,0,8717,8718,0,8719,0,8720,8721,8722,0,8723,8724,8725,0,8726,8727,8728,8729,0,8730,8731,8732,0,8733,8734,8735,0,8736,8737,8738,8739,0,8740,8741,0,8742,8743,8744,0,8745,8746,8747,0,8748,8749,8750,0,8752,8751,8753,8754,0',
             'sumgrades' => '0',
             'grade' => '10',
             'timecreated' => '0',
index f240e3a..b76e7e9 100644 (file)
@@ -64,7 +64,6 @@ class qtype_essay_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '90042,0,90043,0,90045,0,90052,0,90053,0,90054,0,90055,0,90056,0,90057,0,90058,0,90059,0,90046,0,90044,0,90047,0,90048,0,90049,0',
             'sumgrades' => '100',
             'grade' => '100',
             'timecreated' => '0',
@@ -295,7 +294,6 @@ class qtype_essay_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '3664,3716,0,3663,3717,0,3718,3719,0,3720,0,3733,3727,0,3728,3730,0,3731,3732,0,3726,3729,0',
             'sumgrades' => '0',
             'grade' => '0',
             'timecreated' => '0',
@@ -475,7 +473,6 @@ class qtype_essay_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '3664,3716,0,3663,3717,0,3718,3719,0,3720,0,3733,3727,0,3728,3730,0,3731,3732,0,3726,3729,0',
             'sumgrades' => '0',
             'grade' => '0',
             'timecreated' => '0',
index 4c6b4b4..9188d57 100644 (file)
@@ -59,7 +59,6 @@ class qtype_match_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '2',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '689,690,0,691,692,0,693,694,0,695,696,0,697,698,0',
             'sumgrades' => '48',
             'grade' => '48',
             'timecreated' => '0',
@@ -317,7 +316,6 @@ class qtype_match_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '509,0,510,0,511,0,738,0,514,0',
             'sumgrades' => '5',
             'grade' => '10',
             'timecreated' => '0',
@@ -527,7 +525,6 @@ class qtype_match_attempt_upgrader_test extends question_attempt_upgrader_test_b
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '11163,0,11164,0,11165,0,11135,0,11166,0',
             'sumgrades' => '5',
             'grade' => '10',
             'timecreated' => '0',
index b52e4d1..178733f 100644 (file)
@@ -65,13 +65,13 @@ class qtype_multianswer_edit_form extends question_edit_form {
         if (isset($question->id) && $question->id != 0) {
             // TODO MDL-43779 should not have quiz-specific code here.
             $this->savedquestiondisplay = fullclone($question);
-            $this->nb_of_quiz = $DB->count_records('quiz_question_instances', array('questionid' => $question->id));
+            $this->nb_of_quiz = $DB->count_records('quiz_slots', array('questionid' => $question->id));
             $this->used_in_quiz = $this->nb_of_quiz > 0;
             $this->nb_of_attempts = $DB->count_records_sql("
                     SELECT count(1)
-                      FROM {quiz_question_instances} qqi
-                      JOIN {quiz_attempts} quiza ON quiza.quiz = qqi.quizid
-                     WHERE qqi.questionid = ?
+                      FROM {quiz_slots} slot
+                      JOIN {quiz_attempts} quiza ON quiza.quiz = slot.quizid
+                     WHERE slot.questionid = ?
                        AND quiza.preview = 0", array($question->id));
         }
 
index 925e81f..825e87a 100644 (file)
@@ -56,7 +56,6 @@ class qtype_multianswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -260,7 +259,6 @@ class qtype_multianswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -443,7 +441,6 @@ class qtype_multianswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -668,7 +665,6 @@ class qtype_multianswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -1354,7 +1350,6 @@ b) What grade would you give it? _____',
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -2012,7 +2007,6 @@ b) What grade would you give it? _____',
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '28,19,0',
             'sumgrades' => '14.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index 9fd6e09..f6859b7 100644 (file)
@@ -59,7 +59,6 @@ class qtype_multichoice_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '0',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '1,0',
             'timecreated' => '0',
             'timemodified' => '1278603396',
             'timelimit' => '0',
@@ -296,7 +295,6 @@ class qtype_multichoice_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '0',
             'shufflequestions' => '1',
             'shuffleanswers' => '1',
-            'questions' => '71,72,73,89,74,75,76,92,77,78,79,93,80,90,81,91,94,95,82,83,84,85,86,87,88,0',
             'sumgrades' => '25',
             'grade' => '25',
             'timecreated' => '0',
@@ -464,7 +462,6 @@ class qtype_multichoice_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '0',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '218,221,219,220,223,224,222,0',
             'sumgrades' => '7',
             'grade' => '0',
             'timecreated' => '0',
@@ -659,7 +656,6 @@ class qtype_multichoice_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '2',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '2855,2856,0,2857,2858,0,2859,2860,0,2861,2862,0,2863,2864,0,2865,0',
             'sumgrades' => '59',
             'grade' => '59',
             'timecreated' => '0',
@@ -898,7 +894,6 @@ class qtype_multichoice_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '0',
             'shufflequestions' => '1',
             'shuffleanswers' => '1',
-            'questions' => '71,72,73,89,74,75,76,92,77,78,79,93,80,90,81,91,94,95,82,83,84,85,86,87,88,0',
             'sumgrades' => '25',
             'grade' => '25',
             'timecreated' => '0',
@@ -1032,7 +1027,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '2',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '178,179,0,180,181,0,182,183,0,184,185,0,189,187,0',
             'sumgrades' => '10',
             'grade' => '10',
             'timecreated' => '0',
@@ -1236,7 +1230,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '2',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '26132,26128,0,26143,26140,0,26144,26141,0,26145,26142,0,26126,26127,0,26134,26135,0,26131,26133,0,26130,26129,0,26136,26137,0,26139,26138,0',
             'sumgrades' => '0',
             'grade' => '0',
             'timecreated' => '0',
@@ -1712,7 +1705,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '161,0,160,0,162,0,163,0,164,0,165,0,166,0,190,0,191,0,192,0,193,0,194,0,195,0,196,0,197,0,198,0,168,0,169,0,170,0,171,0,172,0,173,0,174,0,175,0,176,0,177,0,178,0,179,0,180,0,181,0',
             'sumgrades' => '30',
             'grade' => '10',
             'timecreated' => '0',
@@ -1950,7 +1942,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '89002,0,89040,0,89042,0,89043,0,89044,0,89045,0,89046,0,89047,0',
             'sumgrades' => '8',
             'grade' => '8',
             'timecreated' => '0',
@@ -2154,7 +2145,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '2',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '3859,3860,0,3861,3862,0,3863,3864,0,3865,3866,0,3867,3868,0',
             'sumgrades' => '50',
             'grade' => '50',
             'timecreated' => '0',
@@ -2376,7 +2366,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '242,0,243,0,244,0,245,0,246,0,247,0',
             'sumgrades' => '0',
             'grade' => '0',
             'timecreated' => '0',
@@ -2585,7 +2574,6 @@ public function test_multichoice_deferredfeedback_qsession140() {
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '242,0,243,0,244,0,245,0,246,0,247,0',
             'sumgrades' => '0',
             'grade' => '0',
             'timecreated' => '0',
index ad675d1..ce6ae98 100644 (file)
@@ -60,7 +60,6 @@ class qtype_numerical_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '4184,0,4185,0,4154,0,4186,0,4187,0,4188,0,4189,0,4190,0,4162,0,4191,0,4192,0,4193,0,4254,0,4195,0,4196,0,4163,0,4197,0,4198,0,4199,0,4164,0,4200,0,4165,0,4201,0,4202,0,4166,0,4203,0,4204,0,4205,0,4167,0,4155,0,4168,0,4206,0,4207,0,4208,0,4209,0,4210,0,4211,0,4212,0,4213,0,4214,0,4156,0,4215,0,4216,0,4217,0,4169,0,4170,0,4157,0,4218,0,4219,0,4220,0,4171,0,4221,0,4172,0,4222,0,4223,0,4224,0,4225,0,4226,0,4227,0,4228,0,4173,0,4229,0,4230,0,4231,0,4232,0,4174,0,4233,0,4234,0,4235,0,4236,0,4237,0,4238,0,4239,0,4240,0,4241,0,4242,0,4158,0,4243,0,4244,0,4245,0,4246,0,4159,0,4175,0,4247,0,4176,0,4248,0,4177,0,4160,0,4249,0,4178,0,4250,0,4161,0,4251,0,4179,0,4252,0,4180,0,4181,0,4182,0,4253,0,4183,0',
             'sumgrades' => '100',
             'grade' => '100',
             'timecreated' => '0',
@@ -283,7 +282,6 @@ class qtype_numerical_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '6,12,13,15,14,0',
             'sumgrades' => '5.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index 8b0c3a8..33a5804 100644 (file)
@@ -62,7 +62,6 @@ class qtype_random_attempt_upgrader_test extends question_attempt_upgrader_test_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '28698,0,34245,0,34248,0,35005,0,35009,0,35013,0',
             'sumgrades' => '5',
             'grade' => '100',
             'timecreated' => '0',
@@ -289,7 +288,6 @@ class qtype_random_attempt_upgrader_test extends question_attempt_upgrader_test_
             'questionsperpage' => '3',
             'shufflequestions' => '1',
             'shuffleanswers' => '1',
-            'questions' => '101983,101984,101985,0,101986,101987,101988,0,101989,101990,101991,0,101992,101993,101994,0,101995,101996,101997,0',
             'sumgrades' => '15',
             'grade' => '10',
             'timecreated' => '0',
@@ -538,7 +536,6 @@ class qtype_random_attempt_upgrader_test extends question_attempt_upgrader_test_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '68646,0,81245,0,81246,0,81247,0,81248,0,81249,0,81250,0,82795,0,82797,0,82798,0,82799,0,82800,0,82801,0,82802,0,82803,0,82804,0,82805,0,82806,0,82807,0',
             'sumgrades' => '18',
             'grade' => '18',
             'timecreated' => '0',
@@ -805,7 +802,6 @@ class qtype_random_attempt_upgrader_test extends question_attempt_upgrader_test_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '6,12,13,15,14,0',
             'sumgrades' => '5.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
index 671743f..8dc03ff 100644 (file)
@@ -60,7 +60,6 @@ class qtype_shortanswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '4184,0,4185,0,4154,0,4186,0,4187,0,4188,0,4189,0,4190,0,4162,0,4191,0,4192,0,4193,0,4254,0,4195,0,4196,0,4163,0,4197,0,4198,0,4199,0,4164,0,4200,0,4165,0,4201,0,4202,0,4166,0,4203,0,4204,0,4205,0,4167,0,4155,0,4168,0,4206,0,4207,0,4208,0,4209,0,4210,0,4211,0,4212,0,4213,0,4214,0,4156,0,4215,0,4216,0,4217,0,4169,0,4170,0,4157,0,4218,0,4219,0,4220,0,4171,0,4221,0,4172,0,4222,0,4223,0,4224,0,4225,0,4226,0,4227,0,4228,0,4173,0,4229,0,4230,0,4231,0,4232,0,4174,0,4233,0,4234,0,4235,0,4236,0,4237,0,4238,0,4239,0,4240,0,4241,0,4242,0,4158,0,4243,0,4244,0,4245,0,4246,0,4159,0,4175,0,4247,0,4176,0,4248,0,4177,0,4160,0,4249,0,4178,0,4250,0,4161,0,4251,0,4179,0,4252,0,4180,0,4181,0,4182,0,4253,0,4183,0',
             'sumgrades' => '100',
             'grade' => '100',
             'timecreated' => '0',
@@ -271,7 +270,6 @@ class qtype_shortanswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '0',
             'shufflequestions' => '1',
             'shuffleanswers' => '1',
-            'questions' => '10218,10216,10219,10220,10217,0',
             'sumgrades' => '5',
             'grade' => '5',
             'timecreated' => '0',
@@ -444,7 +442,6 @@ class qtype_shortanswer_attempt_upgrader_test extends question_attempt_upgrader_
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '4184,0,4185,0,4154,0,4186,0,4187,0,4188,0,4189,0,4190,0,4162,0,4191,0,4192,0,4193,0,4254,0,4195,0,4196,0,4163,0,4197,0,4198,0,4199,0,4164,0,4200,0,4165,0,4201,0,4202,0,4166,0,4203,0,4204,0,4205,0,4167,0,4155,0,4168,0,4206,0,4207,0,4208,0,4209,0,4210,0,4211,0,4212,0,4213,0,4214,0,4156,0,4215,0,4216,0,4217,0,4169,0,4170,0,4157,0,4218,0,4219,0,4220,0,4171,0,4221,0,4172,0,4222,0,4223,0,4224,0,4225,0,4226,0,4227,0,4228,0,4173,0,4229,0,4230,0,4231,0,4232,0,4174,0,4233,0,4234,0,4235,0,4236,0,4237,0,4238,0,4239,0,4240,0,4241,0,4242,0,4158,0,4243,0,4244,0,4245,0,4246,0,4159,0,4175,0,4247,0,4176,0,4248,0,4177,0,4160,0,4249,0,4178,0,4250,0,4161,0,4251,0,4179,0,4252,0,4180,0,4181,0,4182,0,4253,0,4183,0',
             'sumgrades' => '100',
             'grade' => '100',
             'timecreated' => '0',
index d36abfc..ec1e8a2 100644 (file)
@@ -60,7 +60,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'showblocks' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '0',
-            'questions' => '3859,3860,0,3861,3862,0,3863,3864,0,3865,3866,0,3867,3868,0',
             'sumgrades' => '50',
             'grade' => '50',
             'timecreated' => '0',
@@ -250,7 +249,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '9043,0,9057,0,9062,0,9241,0',
             'sumgrades' => '4',
             'grade' => '4',
             'timecreated' => '0',
@@ -429,7 +427,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '0',
             'shufflequestions' => '1',
             'shuffleanswers' => '1',
-            'questions' => '96,98,104,106,101,108,111,102,113,0',
             'sumgrades' => '9',
             'grade' => '9',
             'timecreated' => '0',
@@ -603,7 +600,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '30,0',
             'sumgrades' => '10.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -794,7 +790,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '30,0',
             'sumgrades' => '10.00000',
             'grade' => '10.00000',
             'timecreated' => '0',
@@ -991,7 +986,6 @@ class qtype_truefalse_attempt_upgrader_test extends question_attempt_upgrader_te
             'questionsperpage' => '1',
             'shufflequestions' => '0',
             'shuffleanswers' => '1',
-            'questions' => '1,0',
             'sumgrades' => '1.00000',
             'grade' => '10.00000',
             'timecreated' => '0',