MDL-20636 Merge remote-tracking branch 'moodle/master' into upgrade
authorTim Hunt <T.J.Hunt@open.ac.uk>
Wed, 11 May 2011 19:29:49 +0000 (20:29 +0100)
committerTim Hunt <T.J.Hunt@open.ac.uk>
Wed, 11 May 2011 19:29:49 +0000 (20:29 +0100)
Conflicts:
lib/filestorage/file_storage.php
mod/quiz/attemptlib.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/report/overview/overview_table.php
mod/quiz/report/overview/report.php
mod/quiz/report/responses/report.php
mod/quiz/report/responses/responses_table.php
mod/quiz/report/statistics/db/install.xml
mod/quiz/report/statistics/qstats.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/version.php
mod/quiz/review.php
mod/quiz/reviewquestion.php
mod/quiz/startattempt.php
mod/quiz/styles.css
mod/quiz/view.php
question/type/essay/questiontype.php
question/type/match/backup/moodle2/backup_qtype_match_plugin.class.php
question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
question/type/numerical/display.html
question/type/numerical/questiontype.php
question/type/questiontype.php
question/type/random/questiontype.php
question/type/shortanswer/questiontype.php
theme/base/style/question.css

27 files changed:
1  2 
admin/generator.php
backup/moodle2/backup_stepslib.php
backup/moodle2/restore_stepslib.php
lib/adminlib.php
lib/cronlib.php
lib/moodlelib.php
local/qedatabase/db/install.php
local/qedatabase/db/upgrade.php
mod/quiz/overrides.php
mod/quiz/renderer.php
mod/quiz/report/attemptsreport.php
mod/quiz/report/grading/report.php
mod/quiz/report/overview/overview_table.php
mod/quiz/report/reportlib.php
mod/quiz/report/statistics/db/install.xml
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/report/statistics/qstats.php
mod/quiz/report/statistics/report.php
mod/quiz/styles.css
mod/quiz/view.php
question/editlib.php
question/engine/bank.php
question/engine/datalib.php
question/engine/simpletest/testdatalib.php
question/type/match/backup/moodle2/restore_qtype_match_plugin.class.php
question/type/multianswer/questiontype.php
theme/base/style/question.css

index 353b320,0e0c11e..5d642af
mode 100755,100644..100644
Simple merge
Simple merge
Simple merge
diff --cc lib/cronlib.php
Simple merge
@@@ -7020,11 -7085,10 +7085,11 @@@ function get_plugin_types($fullpaths=tr
                        'webservice'    => 'webservice',
                        'repository'    => 'repository',
                        'portfolio'     => 'portfolio',
 -                      'qtype'         => 'question/type',
 +                      'qbehaviour'    => 'question/behaviour',
                        'qformat'       => 'question/format',
 +                      'qtype'         => 'question/type',
                        'plagiarism'    => 'plagiarism',
-                       'theme'         => 'theme'); // this is a bit hacky, themes may be in dataroot too
+                       'theme'         => 'theme'); // this is a bit hacky, themes may be in $CFG->themedir too
  
          $mods = get_plugin_list('mod');
          foreach ($mods as $mod => $moddir) {
index 76ba795,0000000..56401da
mode 100755,000000..100755
--- /dev/null
@@@ -1,691 -1,0 +1,696 @@@
 +<?php
 +
 +function xmldb_local_qedatabase_install() {
 +    global $DB;
 +    $dbman = $DB->get_manager();
 +
++    // Bit of a hack to prevent errors like "Cannot downgrade local_qedatabase from ... to ...".
++    $oldversion = 2008000000;
++    $DB->set_field('config_plugins', 'value', $oldversion,
++            array('plugin' => 'local_qedatabase', 'name' => 'version'));
++
 +    // Add new preferredbehaviour column to the quiz table.
 +    if ($oldversion < 2008000100) {
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('preferredbehaviour');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, null, null, null, 'timeclose');
 +        if (!$dbman->field_exists($table, $field)) {
 +            $dbman->add_field($table, $field);
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000100, 'local', 'qedatabase');
 +    }
 +
 +    // Populate preferredbehaviour column based on old optionflags column.
 +    if ($oldversion < 2008000101) {
 +        $DB->set_field_select('quiz', 'preferredbehaviour', 'deferredfeedback',
 +                'optionflags = 0');
 +        $DB->set_field_select('quiz', 'preferredbehaviour', 'adaptive',
 +                'optionflags <> 0 AND penaltyscheme <> 0');
 +        $DB->set_field_select('quiz', 'preferredbehaviour', 'adaptivenopenalty',
 +                'optionflags <> 0 AND penaltyscheme = 0');
 +
 +        set_config('preferredbehaviour', 'deferredfeedback', 'quiz');
 +        set_config('fix_preferredbehaviour', 0, 'quiz');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000101, 'local', 'qedatabase');
 +    }
 +
 +    // Add a not-NULL constraint to the preferredmodel field now that it is populated.
 +    if ($oldversion < 2008000102) {
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('preferredbehaviour');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null, 'timeclose');
 +
 +        $dbman->change_field_notnull($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000102, 'local', 'qedatabase');
 +    }
 +
 +    // Drop the old optionflags field.
 +    if ($oldversion < 2008000103) {
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('optionflags');
 +        $dbman->drop_field($table, $field);
 +
 +        unset_config('optionflags', 'quiz');
 +        unset_config('fix_optionflags', 'quiz');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000103, 'local', 'qedatabase');
 +    }
 +
 +    // Drop the old penaltyscheme field.
 +    if ($oldversion < 2008000104) {
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('penaltyscheme');
 +        $dbman->drop_field($table, $field);
 +
 +        unset_config('penaltyscheme', 'quiz');
 +        unset_config('fix_penaltyscheme', 'quiz');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000104, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000110) {
 +
 +        // Changing nullability of field sumgrades on table quiz_attempts to null
 +        $table = new xmldb_table('quiz_attempts');
 +        $field = new xmldb_field('sumgrades');
 +        $field->set_attributes(XMLDB_TYPE_NUMBER, '10, 5', null, null, null, null, 'attempt');
 +
 +        // Launch change of nullability for field sumgrades
 +        $dbman->change_field_notnull($table, $field);
 +
 +        // Launch change of default for field sumgrades
 +        $dbman->change_field_default($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000110, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000111) {
 +
 +        // Changing the default of field penalty on table question to 0.3333333
 +        $table = new xmldb_table('question');
 +        $field = new xmldb_field('penalty');
 +        $field->set_attributes(XMLDB_TYPE_FLOAT, null, null, XMLDB_NOTNULL, null, '0.3333333', 'defaultgrade');
 +
 +        // Launch change of default for field penalty
 +        $dbman->change_field_default($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000111, 'local', 'qedatabase');
 +    }
 +
 +// Update the quiz from the old single review column to seven new columns.
 +
 +    if ($oldversion < 2008000200) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewattempt');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'review');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000200, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000201) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewcorrectness');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewattempt');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000201, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000202) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewmarks');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewcorrectness');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000202, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000203) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewspecificfeedback');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewmarks');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000203, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000204) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewgeneralfeedback');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewspecificfeedback');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000204, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000205) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewrightanswer');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewgeneralfeedback');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000205, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000206) {
 +
 +        // Define field reviewattempt to be added to quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('reviewoverallfeedback');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '6', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'reviewrightanswer');
 +
 +        // Launch add field reviewattempt
 +        $dbman->add_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000206, 'local', 'qedatabase');
 +    }
 +
 +    define('QUIZ_NEW_DURING',            0x10000);
 +    define('QUIZ_NEW_IMMEDIATELY_AFTER', 0x01000);
 +    define('QUIZ_NEW_LATER_WHILE_OPEN',  0x00100);
 +    define('QUIZ_NEW_AFTER_CLOSE',       0x00010);
 +
 +    define('QUIZ_OLD_IMMEDIATELY', 0x3c003f);
 +    define('QUIZ_OLD_OPEN',        0x3c00fc0);
 +    define('QUIZ_OLD_CLOSED',      0x3c03f000);
 +
 +    define('QUIZ_OLD_RESPONSES',       1*0x1041); // Show responses
 +    define('QUIZ_OLD_SCORES',          2*0x1041); // Show scores
 +    define('QUIZ_OLD_FEEDBACK',        4*0x1041); // Show question feedback
 +    define('QUIZ_OLD_ANSWERS',         8*0x1041); // Show correct answers
 +    define('QUIZ_OLD_SOLUTIONS',      16*0x1041); // Show solutions
 +    define('QUIZ_OLD_GENERALFEEDBACK',32*0x1041); // Show question general feedback
 +    define('QUIZ_OLD_OVERALLFEEDBACK', 1*0x4440000); // Show quiz overall feedback
 +
 +    // Copy the old review settings
 +    if ($oldversion < 2008000210) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewattempt = " . $DB->sql_bitor($DB->sql_bitor(
 +                    QUIZ_NEW_DURING,
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000210, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000211) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewcorrectness = " . $DB->sql_bitor($DB->sql_bitor(
 +                    QUIZ_NEW_DURING,
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000211, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000212) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewmarks = " . $DB->sql_bitor($DB->sql_bitor(
 +                    QUIZ_NEW_DURING,
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000212, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000213) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewspecificfeedback = " . $DB->sql_bitor($DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000213, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000214) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewgeneralfeedback = " . $DB->sql_bitor($DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000214, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000215) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewrightanswer = " . $DB->sql_bitor($DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_DURING . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000215, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000216) {
 +        $DB->execute("
 +            UPDATE {quiz}
 +            SET reviewoverallfeedback = " . $DB->sql_bitor($DB->sql_bitor(
 +                    0,
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_IMMEDIATELY_AFTER . ' ELSE 0 END'), $DB->sql_bitor(
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_LATER_WHILE_OPEN . ' ELSE 0 END',
 +                    'CASE WHEN ' . $DB->sql_bitand('review', QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK) .
 +                        ' <> 0 THEN ' . QUIZ_NEW_AFTER_CLOSE . ' ELSE 0 END')) . "
 +        ");
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000216, 'local', 'qedatabase');
 +    }
 +
 +    // And, do the same for the defaults
 +    if ($oldversion < 2008000217) {
 +        if (empty($CFG->quiz_review)) {
 +            $CFG->quiz_review = 0;
 +        }
 +
 +        set_config('reviewattempt',
 +                QUIZ_NEW_DURING |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_RESPONSES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_RESPONSES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_RESPONSES ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewcorrectness',
 +                QUIZ_NEW_DURING |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewmarks',
 +                QUIZ_NEW_DURING |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_SCORES ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_SCORES ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_SCORES ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewspecificfeedback',
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_DURING : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_FEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewgeneralfeedback',
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_DURING : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_GENERALFEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewrightanswer',
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? QUIZ_NEW_DURING : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_ANSWERS ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_ANSWERS ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_ANSWERS ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        set_config('reviewoverallfeedback',
 +                0 |
 +                ($CFG->quiz_review & QUIZ_OLD_IMMEDIATELY & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_IMMEDIATELY_AFTER : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_OPEN & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_LATER_WHILE_OPEN : 0) |
 +                ($CFG->quiz_review & QUIZ_OLD_CLOSED & QUIZ_OLD_OVERALLFEEDBACK ? QUIZ_NEW_AFTER_CLOSE : 0),
 +                'quiz');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000217, 'local', 'qedatabase');
 +    }
 +
 +    // Finally drop the old column
 +    if ($oldversion < 2008000220) {
 +        // Define field review to be dropped from quiz
 +        $table = new xmldb_table('quiz');
 +        $field = new xmldb_field('review');
 +
 +        // Launch drop field review
 +        $dbman->drop_field($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000220, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000221) {
 +        unset_config('review', 'quiz');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000221, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000501) {
 +
 +        // Rename field defaultgrade on table question to defaultmark
 +        $table = new xmldb_table('question');
 +        $field = new xmldb_field('defaultgrade');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '1', 'generalfeedback');
 +
 +        // Launch rename field defaultmark
 +        $dbman->rename_field($table, $field, 'defaultmark');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000501, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000505) {
 +
 +        // Rename the question_attempts table to question_usages.
 +        $table = new xmldb_table('question_attempts');
 +        if ($dbman->table_exists($table)) {
 +            $dbman->rename_table($table, 'question_usages');
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000505, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000507) {
 +
 +        // Rename the modulename field to component ...
 +        $table = new xmldb_table('question_usages');
 +        $field = new xmldb_field('modulename');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'contextid');
 +        $dbman->rename_field($table, $field, 'component');
 +
 +        // ... and update its contents.
 +        $DB->set_field('question_usages', 'component', 'mod_quiz', array('component' => 'quiz'));
 +
 +        // Add the contextid field.
 +        $field = new xmldb_field('contextid');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'id');
 +        $dbman->add_field($table, $field);
 +
 +        // And populate it.
 +        $quizmoduleid = $DB->get_field('modules', 'id', array('name' => 'quiz'));
 +        $DB->execute("
 +            UPDATE {question_usages} SET contextid = (
 +                SELECT ctx.id
 +                FROM {context} ctx
 +                JOIN {course_modules} cm ON cm.id = ctx.instanceid AND cm.module = $quizmoduleid
 +                JOIN {quiz_attempts} quiza ON quiza.quiz = cm.instance
 +                WHERE ctx.contextlevel = " . CONTEXT_MODULE . "
 +                AND quiza.uniqueid = {question_usages}.id
 +            )
 +        ");
 +
 +        // Then make it NOT NULL.
 +        $field = new xmldb_field('contextid');
 +        $field->set_attributes(XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'id');
 +        $dbman->change_field_notnull($table, $field);
 +
 +        // Add the preferredbehaviour column. Populate it with a dummy value
 +        // for now. We will fill in the appropriate behaviour name when
 +        // updating all the rest of the attempt data.
 +        $field = new xmldb_field('preferredbehaviour');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'to_be_set_later', 'component');
 +        $dbman->add_field($table, $field);
 +
 +        // Then remove the default value, now the column is populated.
 +        $field = new xmldb_field('preferredbehaviour');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null, 'component');
 +        $dbman->change_field_default($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000507, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000513) {
 +
 +        // Define key contextid (foreign) to be added to question_usages
 +        $table = new xmldb_table('question_usages');
 +        $key = new XMLDBKey('contextid');
 +        $key->set_attributes(XMLDB_KEY_FOREIGN, array('contextid'), 'context', array('id'));
 +
 +        // Launch add key contextid
 +        $dbman->add_key($table, $key);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000513, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000514) {
 +
 +        // Changing precision of field component on table question_usages to (255)
 +        // This was missed during the upgrade from old versions.
 +        $table = new xmldb_table('question_usages');
 +        $field = new xmldb_field('component');
 +        $field->set_attributes(XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'contextid');
 +
 +        // Launch change of precision for field component
 +        $dbman->change_field_precision($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000514, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000520) {
 +
 +        // Define table question_attempts to be created
 +        $table = new xmldb_table('question_attempts');
 +        if (!$dbman->table_exists($table)) {
 +
 +            // Adding fields to table question_attempts
 +            $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 +            $table->add_field('questionusageid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null);
 +            $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, null);
 +            $table->add_field('minfraction', XMLDB_TYPE_NUMBER, '12, 7', null, XMLDB_NOTNULL, null, null);
 +            $table->add_field('flagged', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
 +            $table->add_field('questionsummary', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
 +            $table->add_field('rightanswer', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
 +            $table->add_field('responsesummary', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
 +            $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +
 +            // Adding keys to table question_attempts
 +            $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 +            $table->add_key('questionid', XMLDB_KEY_FOREIGN, array('questionid'), 'question', array('id'));
 +            $table->add_key('questionusageid', XMLDB_KEY_FOREIGN, array('questionusageid'), 'question_usages', array('id'));
 +
 +            // Adding indexes to table question_attempts
 +            $table->add_index('questionusageid-slot', XMLDB_INDEX_UNIQUE, array('questionusageid', 'slot'));
 +
 +            // Launch create table for question_attempts
 +            $dbman->create_table($table);
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000520, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000521) {
 +
 +        // Define table question_attempt_steps to be created
 +        $table = new xmldb_table('question_attempt_steps');
 +        if (!$dbman->table_exists($table)) {
 +
 +            // Adding fields to table question_attempt_steps
 +            $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 +            $table->add_field('questionattemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('sequencenumber', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('state', XMLDB_TYPE_CHAR, '13', null, XMLDB_NOTNULL, null, null);
 +            $table->add_field('fraction', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
 +            $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null);
 +
 +            // Adding keys to table question_attempt_steps
 +            $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 +            $table->add_key('questionattemptid', XMLDB_KEY_FOREIGN, array('questionattemptid'), 'question_attempts_new', array('id'));
 +            $table->add_key('userid', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id'));
 +
 +            // Adding indexes to table question_attempt_steps
 +            $table->add_index('questionattemptid-sequencenumber', XMLDB_INDEX_UNIQUE, array('questionattemptid', 'sequencenumber'));
 +
 +            // Launch create table for question_attempt_steps
 +            $dbman->create_table($table);
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000521, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000522) {
 +
 +        // Define table question_attempt_step_data to be created
 +        $table = new xmldb_table('question_attempt_step_data');
 +        if (!$dbman->table_exists($table)) {
 +
 +            // Adding fields to table question_attempt_step_data
 +            $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 +            $table->add_field('attemptstepid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +            $table->add_field('name', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, null);
 +            $table->add_field('value', XMLDB_TYPE_TEXT, 'small', null, null, null, null);
 +
 +            // Adding keys to table question_attempt_step_data
 +            $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 +            $table->add_key('attemptstepid', XMLDB_KEY_FOREIGN, array('attemptstepid'), 'question_attempt_steps', array('id'));
 +
 +            // Adding indexes to table question_attempt_step_data
 +            $table->add_index('attemptstepid-name', XMLDB_INDEX_UNIQUE, array('attemptstepid', 'name'));
 +
 +            // Launch create table for question_attempt_step_data
 +            $dbman->create_table($table);
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000522, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000600) {
 +
 +        // Define table question_hints to be created
 +        $table = new xmldb_table('question_hints');
 +
 +        // Adding fields to table question_hints
 +        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
 +        $table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null);
 +        $table->add_field('hint', XMLDB_TYPE_TEXT, 'small', null, XMLDB_NOTNULL, null, null);
 +        $table->add_field('hintformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
 +        $table->add_field('shownumcorrect', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, null, null, null);
 +        $table->add_field('clearwrong', XMLDB_TYPE_INTEGER, '1', XMLDB_UNSIGNED, null, null, null);
 +        $table->add_field('options', XMLDB_TYPE_CHAR, '255', null, null, null, null);
 +
 +        // Adding keys to table question_hints
 +        $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
 +        $table->add_key('questionid', XMLDB_KEY_FOREIGN, array('questionid'), 'question', array('id'));
 +
 +        // Conditionally launch create table for question_hints
 +        if (!$dbman->table_exists($table)) {
 +            $dbman->create_table($table);
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000600, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000601) {
 +
 +        // In the past, question_answer fractions were stored with rather
 +        // sloppy rounding. Now update them to the new standard of 7 d.p.
 +        $changes = array(
 +            '-0.66666'  => '-0.6666667',
 +            '-0.33333'  => '-0.3333333',
 +            '-0.16666'  => '-0.1666667',
 +            '-0.142857' => '-0.1428571',
 +             '0.11111'  =>  '0.1111111',
 +             '0.142857' =>  '0.1428571',
 +             '0.16666'  =>  '0.1666667',
 +             '0.33333'  =>  '0.3333333',
 +             '0.333333' =>  '0.3333333',
 +             '0.66666'  =>  '0.6666667',
 +        );
 +        foreach ($changes as $from => $to) {
 +            $DB->set_field('question_answers',
 +                    'fraction', $to, array('fraction' => $from));
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000601, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000602) {
 +
 +        // In the past, question penalties were stored with rather
 +        // sloppy rounding. Now update them to the new standard of 7 d.p.
 +        $DB->set_field('question',
 +                'penalty', 0.3333333, array('penalty' => 33.3));
 +        $DB->set_field_select('question',
 +                'penalty', 0.3333333, 'penalty >= 0.33 AND penalty <= 0.34');
 +        $DB->set_field_select('question',
 +                'penalty', 0.6666667, 'penalty >= 0.66 AND penalty <= 0.67');
 +        $DB->set_field_select('question',
 +                'penalty', 1, 'penalty > 1');
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000602, 'local', 'qedatabase');
 +    }
 +}
index b213e7a,0000000..d12ba96
mode 100755,000000..100755
--- /dev/null
@@@ -1,110 -1,0 +1,110 @@@
-             delete_records_select('question_states',
 +<?php
 +
 +function xmldb_local_qedatabase_upgrade($oldversion) {
 +    global $CFG, $DB;
 +
 +    $dbman = $DB->get_manager();
 +
 +    if ($oldversion < 2008000700) {
 +
 +        // Define field hintformat to be added to question_hints table.
 +        $table = new xmldb_table('question_hints');
 +        $field = new xmldb_field('hintformat', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0');
 +
 +        // Conditionally launch add field partiallycorrectfeedbackformat
 +        if (!$dbman->field_exists($table, $field)) {
 +            $dbman->add_field($table, $field);
 +        }
 +
 +        upgrade_plugin_savepoint(true, 2008000700, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000701) {
 +       // Define table quiz_report to be renamed to quiz_reports
 +        $table = new xmldb_table('quiz_report');
 +
 +        // Launch rename table for quiz_reports
 +        if ($dbman->table_exists($table)) {
 +            $dbman->rename_table($table, 'quiz_reports');
 +        }
 +
 +        upgrade_plugin_savepoint(true, 2008000701, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000702) {
 +        // Define index name (unique) to be added to quiz_reports
 +        $table = new xmldb_table('quiz_reports');
 +        $index = new xmldb_index('name', XMLDB_INDEX_UNIQUE, array('name'));
 +
 +        // Conditionally launch add index name
 +        if (!$dbman->index_exists($table, $index)) {
 +            $dbman->add_index($table, $index);
 +        }
 +
 +        upgrade_plugin_savepoint(true, 2008000702, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000703) {
 +        // Rename the quiz_report table to quiz_reports.
 +        $table = new xmldb_table('quiz_report');
 +        if ($dbman->table_exists($table)) {
 +            $dbman->rename_table($table, 'quiz_reports');
 +        }
 +
 +        upgrade_plugin_savepoint(true, 2008000703, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000704) {
 +
 +        // Changing nullability of field sumgrades on table quiz_attempts to null
 +        $table = new xmldb_table('quiz_attempts');
 +        $field = new xmldb_field('sumgrades', XMLDB_TYPE_NUMBER, '10, 5', null, null, null, null, 'attempt');
 +
 +        // Launch change of nullability for field sumgrades
 +        $dbman->change_field_notnull($table, $field);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000704, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000799) {
 +        // Define field needsupgradetonewqe to be added to quiz_attempts
 +        $table = new xmldb_table('quiz_attempts');
 +        $field = new xmldb_field('needsupgradetonewqe', XMLDB_TYPE_INTEGER, '3', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, '0', 'preview');
 +
 +        // Launch add field needsupgradetonewqe
 +        if (!$dbman->field_exists($table, $field)) {
 +            $dbman->add_field($table, $field);
 +        }
 +
 +        $DB->set_field('quiz_attempts', 'needsupgradetonewqe', 1);
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000800, 'local', 'qedatabase');
 +    }
 +
 +    if ($oldversion < 2008000800) {
 +        $table = new xmldb_table('question_states');
 +        if ($dbman->table_exists($table)) {
 +            // First delete all data from preview attempts.
-             delete_records_select('question_sessions',
++            $DB->delete_records_select('question_states',
 +                    "attempt IN (SELECT uniqueid FROM {quiz_attempts} WHERE preview = 1)");
-             delete_records('quiz_attempts', 'preview', 1);
++            $DB->delete_records_select('question_sessions',
 +                    "attemptid IN (SELECT uniqueid FROM {quiz_attempts} WHERE preview = 1)");
++            $DB->delete_records('quiz_attempts', array('preview' => 1));
 +
 +            // Now update all the old attempt data.
 +            $oldrcachesetting = $CFG->rcache;
 +            $CFG->rcache = false;
 +
 +            require_once($CFG->dirroot . '/question/engine/upgradefromoldqe/upgrade.php');
 +            $upgrader = new question_engine_attempt_upgrader();
 +            $upgrader->convert_all_quiz_attempts();
 +
 +            $CFG->rcache = $oldrcachesetting;
 +        }
 +
 +        // quiz savepoint reached
 +        upgrade_plugin_savepoint(true, 2008000800, 'local', 'qedatabase');
 +    }
 +}
Simple merge
index 3212853,0000000..767ed09
mode 100644,000000..100644
--- /dev/null
@@@ -1,987 -1,0 +1,991 @@@
 +<?php
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 +
 +/**
 + * Defines the renderer for the quiz module.
 + *
 + * @package    mod
 + * @subpackage quiz
 + * @copyright  2011 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +
 +
 +/**
 + * The renderer for the quiz module.
 + *
 + * @copyright  2011 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +class mod_quiz_renderer extends plugin_renderer_base {
 +    /**
 +     * Builds the review page
 +     *
 +     * @param quiz_attempt $attemptobj an instance of quiz_attempt.
 +     * @param array $slots an array of intgers relating to questions.
 +     * @param int $page the current page number
 +     * @param bool $showall whether to show entire attempt on one page.
 +     * @param bool $lastpage if true the current page is the last page.
 +     * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options.
 +     * @param array $summarydata contains all table data
 +     * @return $output containing html data.
 +     */
 +    public function review_page(quiz_attempt $attemptobj, $slots, $page, $showall,
 +                                $lastpage, mod_quiz_display_options $displayoptions,
 +                                $summarydata) {
 +
 +        $output = '';
 +        $output .= $this->header();
 +        $output .= $this->review_summary_table($summarydata, $page);
 +        $output .= $this->review_form($page, $showall, $displayoptions,
 +                $this->questions($attemptobj, true, $slots, $page, $showall, $displayoptions),
 +                $attemptobj, $showall);
 +
 +        $output .= $this->review_next_navigation($attemptobj, $page, $lastpage);
 +        $output .= $this->footer();
 +        return $output;
 +    }
 +
 +    /**
 +     * Filters the summarydata array.
 +     *
 +     * @param array $summarydata contains row data for table
 +     * @param int $page the current page number
 +     * @return $summarydata containing filtered row data
 +     */
 +    protected function filter_summary_table($summarydata, $page) {
 +        if ($page == 0) {
 +            return $summarydata;
 +        }
 +
 +        // Only show some of summary table on subsequent pages.
 +        foreach ($summarydata as $key => $rowdata) {
 +            if (!in_array($key, array('user', 'attemptlist'))) {
 +                unset($summarydata[$key]);
 +            }
 +        }
 +
 +        return $summarydata;
 +    }
 +
 +    /**
 +     * Outputs the table containing data from summary data array
 +     *
 +     * @param array $summarydata contains row data for table
 +     * @param int $page contains the current page number
 +     */
 +    public function review_summary_table($summarydata, $page) {
 +                                         $summarydata = $this->filter_summary_table($summarydata,
 +                                         $page);
 +        if (empty($summarydata)) {
 +            return '';
 +        }
 +
 +        $output = '';
 +        $output .= html_writer::start_tag('table', array(
 +                'class' => 'generaltable generalbox quizreviewsummary'));
 +        $output .= html_writer::start_tag('tbody');
 +        foreach ($summarydata as $rowdata) {
 +            if ($rowdata['title'] instanceof renderable) {
 +                $title = $this->render($rowdata['title']);
 +            } else {
 +                $title = $rowdata['title'];
 +            }
 +
 +            if ($rowdata['content'] instanceof renderable) {
 +                $content = $this->render($rowdata['content']);
 +            } else {
 +                $content = $rowdata['content'];
 +            }
 +
 +            $output .= html_writer::tag('tr',
 +                html_writer::tag('th', $title, array('class' => 'cell', 'scope' => 'row')) .
 +                        html_writer::tag('td', $content, array('class' => 'cell'))
 +            );
 +        }
 +
 +        $output .= html_writer::end_tag('tbody');
 +        $output .= html_writer::end_tag('table');
 +        return $output;
 +    }
 +
 +    /**
 +     * Renders each question
 +     *
 +     * @param quiz_attempt $attemptobj instance of quiz_attempt
 +     * @param bool $reviewing
 +     * @param array $slots array of intgers relating to questions
 +     * @param int $page current page number
 +     * @param bool $showall if true shows attempt on single page
 +     * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
 +     */
 +    public function questions(quiz_attempt $attemptobj, $reviewing, $slots, $page, $showall,
 +                              mod_quiz_display_options $displayoptions) {
 +        $output = '';
 +        foreach ($slots as $slot) {
 +            $output .= $attemptobj->render_question($slot, $reviewing,
 +                    $attemptobj->review_url($slot, $page, $showall));
 +        }
 +        return $output;
 +    }
 +
 +    /**
 +     * Renders the main bit of the review page.
 +     *
 +     * @param array $summarydata contain row data for table
 +     * @param int $page current page number
 +     * @param mod_quiz_display_options $displayoptions instance of mod_quiz_display_options
 +     * @param $content contains each question
 +     * @param quiz_attempt $attemptobj instance of quiz_attempt
 +     * @param bool $showall if true display attempt on one page
 +     */
 +    public function review_form($summarydata, $page, $displayoptions, $content, $attemptobj,
 +                                $showall) {
 +        if ($displayoptions->flags != question_display_options::EDITABLE) {
 +            return $content;
 +        }
 +
 +        $this->page->requires->js_init_call('M.mod_quiz.init_review_form', null, false,
 +                quiz_get_js_module());
 +
 +        $output = '';
 +        $output .= html_writer::start_tag('form', array('action' => $attemptobj->review_url(0,
 +                $page, $showall), 'method' => 'post', 'class' => 'questionflagsaveform'));
 +        $output .= html_writer::start_tag('div');
 +        $output .= $content;
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
 +                'value' => sesskey()));
 +        $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
 +        $output .= html_writer::empty_tag('input', array('type' => 'submit',
 +                'class' => 'questionflagsavebutton', 'name' => 'savingflags',
 +                'value' => get_string('saveflags', 'question')));
 +        $output .= html_writer::end_tag('div');
 +        $output .= html_writer::end_tag('div');
 +        $output .= html_writer::end_tag('form');
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Returns either a liink or button
 +     *
 +     * @param $url contains a url for the review link
 +     */
 +    public function finish_review_link($url) {
 +        if ($this->page->pagelayout == 'popup') {
 +            // In a 'secure' popup window.
 +            $this->page->requires->js_init_call('M.mod_quiz.secure_window.init_close_button',
 +                    array($url), quiz_get_js_module());
 +            return html_writer::empty_tag('input', array('type' => 'button',
 +                    'value' => get_string('finishreview', 'quiz'),
 +                    'id' => 'secureclosebutton'));
 +        } else {
 +            return html_writer::link($url, get_string('finishreview', 'quiz'));
 +        }
 +    }
 +
 +    /**
 +     * Creates a next page arrow or the finishing link
 +     *
 +     * @param quiz_attempt $attemptobj instance of quiz_attempt
 +     * @param int $page the current page
 +     * @param bool $lastpage if true current page is the last page
 +     */
 +    public function review_next_navigation(quiz_attempt $attemptobj, $page, $lastpage) {
 +        if ($lastpage) {
 +            $nav = $this->finish_review_link($attemptobj->view_url());
 +        } else {
 +            $nav = link_arrow_right(get_string('next'), $attemptobj->review_url(0, $page + 1));
 +        }
 +        return html_writer::tag('div', $nav, array('class' => 'submitbtns'));
 +    }
 +
 +    /**
 +     * Return the HTML of the quiz timer.
 +     * @return string HTML content.
 +     */
 +    public function countdown_timer() {
 +        return html_writer::tag('div', get_string('timeleft', 'quiz') .
 +                html_writer::tag('span', '', array('id' => 'quiz-time-left')),
 +                array('id' => 'quiz-timer'));
 +    }
 +
 +    /**
 +     * Create a preview link
 +     *
 +     * @param $url contains a url to the given page
 +     */
 +    public function restart_preview_button($url) {
 +        return $this->single_button($url, get_string('startnewpreview', 'quiz'));
 +    }
 +
 +    /**
 +     * Outputs the navigation block panel
 +     *
 +     * @param quiz_nav_panel_base $panel instance of quiz_nav_panel_base
 +     */
 +    public function navigation_panel(quiz_nav_panel_base $panel) {
 +
 +        $output = '';
 +        $userpicture = $panel->user_picture();
 +        if ($userpicture) {
 +            $output .= html_writer::tag('div', $this->render($userpicture),
 +                    array('id' => 'user-picture', 'class' => 'clearfix'));
 +        }
 +        $output .= $panel->render_before_button_bits($this);
 +
 +        $output = html_writer::start_tag('div', array('class' => 'qn_buttons'));
 +        foreach ($panel->get_question_buttons() as $button) {
 +            $output .= $this->render($button);
 +        }
 +        $output .= html_writer::end_tag('div');
 +
 +        $output .= html_writer::tag('div', $panel->render_end_bits($this),
 +                array('class' => 'othernav'));
 +
 +        $this->page->requires->js_init_call('M.mod_quiz.nav.init', null, false,
 +                quiz_get_js_module());
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Returns the quizzes navigation button
 +     *
 +     * @param quiz_nav_question_button $button
 +     */
 +    protected function render_quiz_nav_question_button(quiz_nav_question_button $button) {
 +        $classes = array('qnbutton', $button->stateclass);
 +        $attributes = array();
 +
 +        if ($button->currentpage) {
 +            $classes[] = 'thispage';
 +            $attributes[] = get_string('onthispage', 'quiz');
 +        }
 +
 +        $attributes[] = $button->statestring;
 +
 +        // Flagged?
 +        if ($button->flagged) {
 +            $classes[] = 'flagged';
 +            $flaglabel = get_string('flagged', 'question');
 +        } else {
 +            $flaglabel = '';
 +        }
 +        $attributes[] = html_writer::tag('span', $flaglabel, array('class' => 'flagstate'));
 +
 +        if (is_numeric($button->number)) {
 +            $qnostring = 'questionnonav';
 +        } else {
 +            $qnostring = 'questionnonavinfo';
 +        }
 +
 +        $a = new stdClass();
 +        $a->number = $button->number;
 +        $a->attributes = implode(' ', $attributes);
 +
 +        return html_writer::link($button->url,
 +                html_writer::tag('span', '', array('class' => 'thispageholder')) .
 +                html_writer::tag('span', '', array('class' => 'trafficlight')) .
 +                get_string($qnostring, 'quiz', $a),
 +                array('class' => implode(' ', $classes), 'id' => $button->id,
 +                        'title' => $button->statestring));
 +    }
 +
 +    /**
 +     * outputs the link the other attempts.
 +     *
 +     * @param mod_quiz_links_to_other_attempts $links
 +     */
 +    protected function render_mod_quiz_links_to_other_attempts(
 +            mod_quiz_links_to_other_attempts $links) {
 +        $attemptlinks = array();
 +        foreach ($links->links as $attempt => $url) {
 +            if ($url) {
 +                $attemptlinks[] = html_writer::link($url, $attempt);
 +            } else {
 +                $attemptlinks[] = html_writer::tag('strong', $attempt);
 +            }
 +        }
 +        return implode(', ', $attemptlinks);
 +    }
 +
 +    /**
 +     * Attempt Page
 +     *
 +     * @param quiz_attempt $attemptobj Instance of quiz_attempt
 +     * @param int $page Current page number
 +     * @param quiz_access_manager $accessmanager Instance of quiz_access_manager
 +     * @param array $messages An array of messages
 +     * @param array $slots Contains an array of integers that relate to questions
 +     * @param int $id The ID of an attempt
 +     * @param int $nextpage The number of the next page
 +     */
 +    public function attempt_page($attemptobj, $page, $accessmanager, $messages, $slots, $id,
 +            $nextpage) {
 +        $output = '';
 +        $output .= $this->quiz_notices($messages);
 +        $output .= $this->attempt_form($attemptobj, $page, $slots, $id, $nextpage);
 +        return $output;
 +    }
 +
 +    /**
 +     * Returns any notices.
 +     *
 +     * @param array $messages
 +     */
 +    public function quiz_notices($messages) {
 +        if (!$messages) {
 +            return '';
 +        }
 +        return $this->box($this->heading(get_string('accessnoticesheader', 'quiz'), 3) .
 +        $this->access_messages($messages), 'quizaccessnotices');
 +    }
 +
 +    /**
 +     * Ouputs the form for making an attempt
 +     *
 +     * @param quiz_attempt $attemptobj
 +     * @param int $page Current page number
 +     * @param array $slots Array of integers relating to questions
 +     * @param int $id ID of the attempt
 +     * @param int $nextpage Next page number
 +     */
 +    public function attempt_form($attemptobj, $page, $slots, $id, $nextpage) {
 +        $output = '';
 +
 +        //Start Form
 +        $output .= html_writer::start_tag('form',
 +                array('action' => $attemptobj->processattempt_url(), 'method' => 'post',
 +                'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
 +                'id' => 'responseform'));
 +        $output .= html_writer::start_tag('div');
 +
 +        // Print all the questions
 +        foreach ($slots as $slot) {
 +            $output .= $attemptobj->render_question($slot, false, $attemptobj->attempt_url($id,
 +                    $page));
 +        }
 +
 +        $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
 +        $output .= html_writer::empty_tag('input', array('type' => 'submit', 'name' => 'next',
 +                'value' => get_string('next')));
 +        $output .= html_writer::end_tag('div');
 +
 +        // Some hidden fields to trach what is going on.
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'attempt',
 +                'value' => $attemptobj->get_attemptid()));
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'thispage',
 +                'value' => $page, 'id' => 'followingpage'));
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'nextpage',
 +                'value' => $nextpage));
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'timeup',
 +                'value' => '0', 'id' => 'timeup'));
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey',
 +                'value' => sesskey()));
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scrollpos',
 +                'value' => '', 'id' => 'scrollpos'));
 +
 +        // Add a hidden field with questionids. Do this at the end of the form, so
 +        // if you navigate before the form has finished loading, it does not wipe all
 +        // the student's answers.
 +        $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'slots',
 +                'value' => implode(',', $slots)));
 +
 +        //Finish form
 +        $output .= html_writer::end_tag('div');
 +        $output .= html_writer::end_tag('form');
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Print each message in an array, surrounded by &lt;p>, &lt;/p> tags.
 +     *
 +     * @param array $messages the array of message strings.
 +     * @param bool $return if true, return a string, instead of outputting.
 +     *
 +     * @return mixed, if $return is true, return the string that would have been output, otherwise
 +     * return null.
 +     */
 +    public function access_messages($messages) {
 +        $output = '';
 +        foreach ($messages as $message) {
 +            $output .= html_writer::tag('p', $message) . "\n";
 +        }
 +        return $output;
 +    }
 +
 +    /*
 +     * Summary Page
 +     */
 +    /**
 +     * Create the summary page
 +     *
 +     * @param quiz_attempt $attemptobj
 +     * @param mod_quiz_display_options $displayoptions
 +     */
 +    public function summary_page($attemptobj, $displayoptions) {
 +        $output = '';
 +        $output .= $this->summary_table($attemptobj, $displayoptions);
 +        $output .= $this->summary_page_controls($attemptobj);
 +        return $output;
 +    }
 +
 +    /**
 +     * Generates the table of summarydata
 +     *
 +     * @param quiz_attempt $attemptobj
 +     * @param mod_quiz_display_options $displayoptions
 +     */
 +    public function summary_table($attemptobj, $displayoptions) {
 +        // Prepare the summary table header
 +        $table = new html_table();
 +        $table->attributes['class'] = 'generaltable quizsummaryofattempt boxaligncenter';
 +        $table->head = array(get_string('question', 'quiz'), get_string('status', 'quiz'));
 +        $table->align = array('left', 'left');
 +        $table->size = array('', '');
 +        $markscolumn = $displayoptions->marks >= question_display_options::MARK_AND_MAX;
 +        if ($markscolumn) {
 +            $table->head[] = get_string('marks', 'quiz');
 +            $table->align[] = 'left';
 +            $table->size[] = '';
 +        }
 +        $table->data = array();
 +
 +        // Get the summary info for each question.
 +        $slots = $attemptobj->get_slots();
 +        foreach ($slots as $slot) {
 +            if (!$attemptobj->is_real_question($slot)) {
 +                continue;
 +            }
 +            $flag = '';
 +            if ($attemptobj->is_question_flagged($slot)) {
 +                $flag = html_writer::empty_tag('img', array('src' => $this->pix_url('i/flagged'),
 +                        'alt' => get_string('flagged', 'question'), 'class' => 'questionflag'));
 +            }
 +            $row = array(html_writer::start_tag('a',
 +                    array('href' => $attemptobj->attempt_url($slot))).
 +                    $attemptobj->get_question_number($slot).$flag.html_writer::end_tag('a'),
 +                    $displayoptions->correctness);
 +            if ($markscolumn) {
 +                $row[] = $attemptobj->get_question_mark($slot);
 +            }
 +            $table->data[] = $row;
 +        }
 +
 +        // Print the summary table.
 +        $output = html_writer::table($table);
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Creates any controls a the page should have.
 +     *
 +     * @param quiz_attempt $attemptobj
 +     */
 +    public function summary_page_controls($attemptobj) {
 +        $output = '';
 +        // countdown timer
 +        $output .= $this->countdown_timer();
 +
 +        // Finish attempt button.
 +        $output .= $this->container_start('submitbtns mdl-align');
 +        $options = array(
 +            'attempt' => $attemptobj->get_attemptid(),
 +            'finishattempt' => 1,
 +            'timeup' => 0,
 +            'slots' => '',
 +            'sesskey' => sesskey(),
 +        );
 +
 +        $button = new single_button(
 +                new moodle_url($attemptobj->processattempt_url(), $options),
 +                get_string('submitallandfinish', 'quiz'));
 +        $button->id = 'responseform';
 +        $button->add_confirm_action(get_string('confirmclose', 'quiz'));
 +
 +        $output .= $this->container_start('controls');
 +        $output .= $this->render($button);
 +        $output .= $this->container_end();
 +        $output .= $this->container_end();
 +
 +        return $output;
 +    }
 +
 +    /*
 +     * View Page
 +     */
 +    /**
 +     * Generates the view page
 +     *
 +     * @param int $course The id of the course
 +     * @param array $quiz Array conting quiz data
 +     * @param int $cm Course Module ID
 +     * @param int $context The page context ID
 +     * @param array $messages Array contining any maeeages
 +     * @param mod_quiz_view_object $viewobj
 +     * @param string $buttontext
 +     */
 +    public function view_page($course, $quiz, $cm, $context, $messages, $viewobj, $buttontext) {
 +        $output = '';
 +        $output .= $this->view_information($course, $quiz, $cm, $context, $messages);
 +        $output .= $this->view_table($quiz, $context, $viewobj);
 +        $output .= $this->view_best_score($viewobj);
 +        $output .= $this->view_result_info($quiz, $context, $cm, $viewobj);
 +        $output .= $this->view_attempt_button($course, $quiz, $cm, $context, $viewobj, $buttontext);
 +        return $output;
 +    }
 +
 +    /**
 +     * Outputs an error message for any guests accessing the quiz
 +     *
 +     * @param int $course The course ID
 +     * @param array $quiz Array contingin quiz data
 +     * @param int $cm Course Module ID
 +     * @param int $context The page contect ID
 +     * @param array $messages Array containing any messages
 +     */
 +    public function view_page_guest($course, $quiz, $cm, $context, $messages) {
 +        $output = '';
 +        $output .= $this->view_information($course, $quiz, $cm, $context, $messages);
 +        $guestno = html_writer::tag('p', get_string('guestsno', 'quiz'));
 +        $liketologin = html_writer::tag('p', get_string('liketologin'));
 +        $output .= $this->confirm($guestno.'\n\n'.$liketologin.'\n', get_login_url(),
 +                get_referer(false));
 +        return $output;
 +    }
 +
 +    /**
 +     * Outputs and error message for anyone who is not enrolle don the course
 +     *
 +     * @param int $course The course ID
 +     * @param array $quiz Array contingin quiz data
 +     * @param int $cm Course Module ID
 +     * @param int $context The page contect ID
 +     * @param array $messages Array containing any messages
 +     */
 +    public function view_page_notenrolled($course, $quiz, $cm, $context, $messages) {
 +        global $CFG;
 +        $output = '';
 +        $output .= $this->view_information($course, $quiz, $cm, $context, $messages);
 +        $youneedtoenrol = html_writer::tag('p', get_string('youneedtoenrol', 'quiz'));
 +        $button = html_writer::tag('p',
 +                $this->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id));
 +        $output .= $this->box($youneedtoenrol.'\n\n'.$button.'\n', 'generalbox', 'notice');
 +        return $output;
 +    }
 +
 +    /**
 +     * Output the page information
 +     *
 +     * @param int $course The course ID
 +     * @param array $quiz Array contingin quiz data
 +     * @param int $cm Course Module ID
 +     * @param int $context The page contect ID
 +     * @param array $messages Array containing any messages
 +     */
 +    public function view_information($course, $quiz, $cm, $context, $messages) {
 +        global $CFG;
 +        $output = '';
 +        // Print quiz name and description
 +        $output .= $this->heading(format_string($quiz->name));
 +        if (trim(strip_tags($quiz->intro))) {
 +            $output .= $this->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox',
 +                    'intro');
 +        }
 +
 +        $output .= $this->box_start('quizinfo');
 +        $this->access_messages($messages);
 +        $output .= $this->box_end();
 +
 +        // Show number of attempts summary to those who can view reports.
 +        if (has_capability('mod/quiz:viewreports', $context)) {
 +            if ($strattemptnum = $this->quiz_attempt_summary_link_to_reports($quiz, $cm,
 +                    $context)) {
 +                $output .= html_writer::tag('div', $strattemptnum,
 +                        array('class' => 'quizattemptcounts'));
 +            }
 +        }
 +        return $output;
 +    }
 +
 +    /**
 +     * Generates the table heading.
 +     */
 +    public function view_table_heading() {
 +        return $this->heading(get_string('summaryofattempts', 'quiz'));
 +    }
 +
 +    /**
 +     * Generates the table of data
 +     *
 +     * @param array $quiz Array contining quiz data
 +     * @param int $context The page context ID
 +     * @param mod_quiz_view_object $viewobj
 +     */
 +    public function view_table($quiz, $context, $viewobj) {
 +        $output = '';
 +        if (!$viewobj->attempts) {
 +            return $output;
 +        }
 +        $output .= $this->view_table_heading();
 +
 +        // Prepare table header
 +        $table = new html_table();
 +        $table->attributes['class'] = 'generaltable quizattemptsummary';
 +        $table->head = array();
 +        $table->align = array();
 +        $table->size = array();
 +        if ($viewobj->attemptcolumn) {
 +            $table->head[] = get_string('attemptnumber', 'quiz');
 +            $table->align[] = 'center';
 +            $table->size[] = '';
 +        }
 +        $table->head[] = get_string('timecompleted', 'quiz');
 +        $table->align[] = 'left';
 +        $table->size[] = '';
 +        if ($viewobj->markcolumn) {
 +            $table->head[] = get_string('marks', 'quiz') . ' / ' .
 +                    quiz_format_grade($quiz, $quiz->sumgrades);
 +            $table->align[] = 'center';
 +            $table->size[] = '';
 +        }
 +        if ($viewobj->gradecolumn) {
 +            $table->head[] = get_string('grade') . ' / ' .
 +                    quiz_format_grade($quiz, $quiz->grade);
 +            $table->align[] = 'center';
 +            $table->size[] = '';
 +        }
 +        if ($viewobj->canreviewmine) {
 +            $table->head[] = get_string('review', 'quiz');
 +            $table->align[] = 'center';
 +            $table->size[] = '';
 +        }
 +        if ($viewobj->feedbackcolumn) {
 +            $table->head[] = get_string('feedback', 'quiz');
 +            $table->align[] = 'left';
 +            $table->size[] = '';
 +        }
 +        if (isset($quiz->showtimetaken)) {
 +            $table->head[] = get_string('timetaken', 'quiz');
 +            $table->align[] = 'left';
 +            $table->size[] = '';
 +        }
 +
 +        // One row for each attempt
 +        foreach ($viewobj->attempts as $attempt) {
 +            $attemptoptions = quiz_get_review_options($quiz, $attempt, $context);
 +            $row = array();
 +
 +            // Add the attempt number, making it a link, if appropriate.
 +            if ($viewobj->attemptcolumn) {
 +                if ($attempt->preview) {
 +                    $row[] = get_string('preview', 'quiz');
 +                } else {
 +                    $row[] = $attempt->attempt;
 +                }
 +            }
 +
 +            // prepare strings for time taken and date completed
 +            $timetaken = '';
 +            $datecompleted = '';
 +            if ($attempt->timefinish > 0) {
 +                // attempt has finished
 +                $timetaken = format_time($attempt->timefinish - $attempt->timestart);
 +                $datecompleted = userdate($attempt->timefinish);
 +            } else if (!$quiz->timeclose || $viewobj->timenow < $quiz->timeclose) {
 +                // The attempt is still in progress.
 +                $timetaken = format_time($viewobj->timenow - $attempt->timestart);
 +                $datecompleted = get_string('inprogress', 'quiz');
 +            } else {
 +                $timetaken = format_time($quiz->timeclose - $attempt->timestart);
 +                $datecompleted = userdate($quiz->timeclose);
 +            }
 +            $row[] = $datecompleted;
 +
 +            if ($viewobj->markcolumn && $attempt->timefinish > 0) {
 +                if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX) {
 +                    $row[] = quiz_format_grade($quiz, $attempt->sumgrades);
 +                } else {
 +                    $row[] = '';
 +                }
 +            }
 +
 +            // Ouside the if because we may be showing feedback but not grades.
 +            $attemptgrade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
 +
 +            if ($viewobj->gradecolumn) {
 +                if ($attemptoptions->marks >= question_display_options::MARK_AND_MAX &&
 +                        $attempt->timefinish > 0) {
 +                    $formattedgrade = quiz_format_grade($quiz, $attemptgrade);
 +                    // highlight the highest grade if appropriate
 +                    if ($viewobj->overallstats && !$attempt->preview
 +                            && $viewobj->numattempts > 1 && !is_null($viewobj->mygrade)
 +                            && $attemptgrade == $viewobj->mygrade
 +                            && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
 +                        $table->rowclasses[$attempt->attempt] = 'bestrow';
 +                    }
 +
 +                    $row[] = $formattedgrade;
 +                } else {
 +                    $row[] = '';
 +                }
 +            }
 +
 +            if ($viewobj->canreviewmine) {
 +                $row[] = $viewobj->accessmanager->make_review_link($attempt,
 +                        $viewobj->canpreview, $attemptoptions);
 +            }
 +
 +            if ($viewobj->feedbackcolumn && $attempt->timefinish > 0) {
 +                if ($attemptoptions->overallfeedback) {
 +                    $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context, $cm);
 +                } else {
 +                    $row[] = '';
 +                }
 +            }
 +
 +            if (isset($quiz->showtimetaken)) {
 +                $row[] = $timetaken;
 +            }
 +
 +            if ($attempt->preview) {
 +                $table->data['preview'] = $row;
 +            } else {
 +                $table->data[$attempt->attempt] = $row;
 +            }
 +        } // End of loop over attempts.
 +        $output .= html_writer::table($table);
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Prints the students best score
 +     *
 +     * @param mod_quiz_view_object $viewobj
 +     */
 +    public function view_best_score($viewobj) {
 +        $output = '';
 +        // Print information about the student's best score for this quiz if possible.
 +        if (!$viewobj->moreattempts) {
 +            $output .= $this->heading(get_string('nomoreattempts', 'quiz'));
 +        }
 +        return $output;
 +    }
 +
 +    /**
 +     * Generates data pertaining to quiz results
 +     *
 +     * @param array $quiz Array containing quiz data
 +     * @param int $context The page context ID
 +     * @param int $cm The Course Module Id
 +     * @param mod_quiz_view_object $viewobj
 +     */
 +    public function view_result_info($quiz, $context, $cm, $viewobj) {
 +        $output = '';
 +        if (!$viewobj->numattempts && !$viewobj->gradecolumn && is_null($viewobj->mygrade)) {
 +            return $output;
 +        }
 +        $resultinfo = '';
 +
 +        if ($viewobj->overallstats) {
 +            if ($viewobj->moreattempts) {
 +                $a = new stdClass();
 +                $a->method = quiz_get_grading_option_name($quiz->grademethod);
 +                $a->mygrade = quiz_format_grade($quiz, $viewobj->mygrade);
 +                $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
 +                $resultinfo .= $this->heading(get_string('gradesofar', 'quiz', $a), 2, 'main');
 +            } else {
 +                $a = new stdClass();
 +                $a->grade = quiz_format_grade($quiz, $viewobj->mygrade);
 +                $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
 +                $a = get_string('outofshort', 'quiz', $a);
 +                $resultinfo .= $this->heading(get_string('yourfinalgradeis', 'quiz', $a), 2,
 +                        'main');
 +            }
 +        }
 +
 +        if ($viewobj->mygradeoverridden) {
 +
 +            $resultinfo .= html_writer::tag('p', get_string('overriddennotice', 'grades'),
 +                    array('class' => 'overriddennotice')).'\n';
 +        }
 +        if ($viewobj->gradebookfeedback) {
 +            $resultinfo .= $this->heading(get_string('comment', 'quiz'), 3, 'main');
 +            $resultinfo .= '<p class="quizteacherfeedback">'.$viewobj->gradebookfeedback.
 +                    "</p>\n";
 +        }
 +        if ($viewobj->feedbackcolumn) {
 +            $resultinfo .= $this->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
 +            $resultinfo .= html_writer::tag('p',
 +                    quiz_feedback_for_grade($viewobj->mygrade, $quiz, $context, $cm),
 +                    array('class' => 'quizgradefeedback')).'\n';
 +        }
 +
 +        if ($resultinfo) {
 +            $output .= $this->box($resultinfo, 'generalbox', 'feedback');
 +        }
 +        return $output;
 +    }
 +
 +    /**
 +     * Generates the view attempt button
 +     *
 +     * @param int $course The course ID
 +     * @param array $quiz Array containging quiz date
 +     * @param int $cm The Course Module ID
 +     * @param int $context The page Context ID
 +     * @param mod_quiz_view_object $viewobj
 +     * @param string $buttontext
 +     */
 +    public function view_attempt_button($course, $quiz, $cm, $context, $viewobj, $buttontext) {
 +        $output = '';
 +        // Determine if we should be showing a start/continue attempt button,
 +        // or a button to go back to the course page.
 +        $output .= $this->box_start('quizattempt');
 +
 +        // Now actually print the appropriate button.
++        if (!quiz_clean_layout($quiz->questions, true)) {
++            $output .= quiz_no_questions_message($quiz, $cm, $context);
++        }
++
 +        if ($buttontext) {
 +            $output .= $viewobj->accessmanager->print_start_attempt_button($viewobj->canpreview,
 +                    $buttontext, $viewobj->unfinished);
 +        } else if ($buttontext === '') {
 +            $output .= $this->single_button(new moodle_url('/course/view.php',
 +                    array('id' => $course->id)), get_string('backtocourse', 'quiz'), 'get',
 +                    array('class' => 'continuebutton'));
 +        }
 +        $output .= $this->box_end();
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Returns the same as {@link quiz_num_attempt_summary()} but wrapped in a link
 +     * to the quiz reports.
 +     *
 +     * @param object $quiz the quiz object. Only $quiz->id is used at the moment.
 +     * @param object $cm the cm object. Only $cm->course, $cm->groupmode and $cm->groupingid
 +     * fields are used at the moment.
 +     * @param object $context the quiz context.
 +     * @param bool $returnzero if false (default), when no attempts have been made '' is returned
 +     * instead of 'Attempts: 0'.
 +     * @param int $currentgroup if there is a concept of current group where this method is being
 +     * called
 +     *         (e.g. a report) pass it in here. Default 0 which means no current group.
 +     * @return string HTML fragment for the link.
 +     */
 +    public function quiz_attempt_summary_link_to_reports($quiz, $cm, $context,
 +                                                          $returnzero = false, $currentgroup = 0) {
 +        global $CFG;
 +        $summary = quiz_num_attempt_summary($quiz, $cm, $returnzero, $currentgroup);
 +        if (!$summary) {
 +            return '';
 +        }
 +
 +        require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
 +        $url = new moodle_url('/mod/quiz/report.php', array(
 +                'id' => $cm->id, 'mode' => quiz_report_default_report($context)));
 +        return html_writer::link($url, $summary);
 +    }
 +}
 +
 +class mod_quiz_links_to_other_attempts implements renderable {
 +    /**
 +     * @var array string attempt number => url, or null for the current attempt.
 +     */
 +    public $links = array();
 +}
 +
 +class mod_quiz_view_object {
 +    /**
 +     * @var array $attempt contains all the user's attempts at this quiz.
 +     */
 +    public $attempts;
 +    /**
 +     * @var object $accessmanager contains various access rules.
 +     */
 +    public $accessmanager;
 +    /**
 +     * @var int $canattempt determins capability for attempting a quiz.
 +     */
 +    public $canattempt;
 +    /**
 +     * @var int $canpreview determins capability for previewing a quiz.
 +     */
 +    public $canpreview;
 +    /**
 +     * @var int $canreviewmine determins capability for reviwing own quiz.
 +     */
 +    public $canreviewmine;
 +    /**
 +     * @var int $attemptcolumn contains the number of attempts done.
 +     */
 +    public $attemptcolumn;
 +    /**
 +     * @var int $gradecolumn contains the grades of any attempts.
 +     */
 +    public $gradecolumn;
 +    /**
 +     * @var int $markcolumn contains the marks of any attempt.
 +     */
 +    public $markcolumn;
 +    /**
 +     * @var int $overallstats contains all marks for any attempt.
 +     */
 +    public $overallstats;
 +    /**
 +     * @var string $feedbackcolumn contains any feedback for and attempt.
 +     */
 +    public $feedbackcolumn;
 +    /**
 +     * @var string $timenow contains a timestamp in string format.
 +     */
 +    public $timenow;
 +    /**
 +     * @var int $numattempts contains the total number of attempts.
 +     */
 +    public $numattempts;
 +    /**
 +     * @var int $mygrade contains the current users final grade for a quiz.
 +     */
 +    public $mygrade;
 +    /**
 +     * @var int $moreattempts total attempts left.
 +     */
 +    public $moreattempts;
 +    /**
 +     * @var int $mygradeoverridden contains an overriden grade.
 +     */
 +    public $mygradeoverridden;
 +    /**
 +     * @var string $gradebookfeedback contains any feedback for a gradebook.
 +     */
 +    public $gradebookfeedback;
 +    /**
 +     * @var int $unfinished contains 1 if an attempt is unfinished.
 +     */
 +    public $unfinished;
 +    /**
 +     * @var int $lastfinishedattempt contains a pointer to the last attempt in the attempts array.
 +     */
 +    public $lastfinishedattempt;
 +}
index d764835,0000000..c09e48e
mode 100644,000000..100644
--- /dev/null
@@@ -1,673 -1,0 +1,673 @@@
-                         $reportstudents, SQL_PARAMS_NAMED, 'u00000');
 +<?php
 +
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 +
 +/**
 + * The file defines some subclasses that can be used when you are building
 + * a report like the overview or responses report, that basically has one
 + * row per attempt.
 + *
 + * @package    mod
 + * @subpackage quiz
 + * @copyright  2010 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +
 +
 +defined('MOODLE_INTERNAL') || die();
 +
 +require_once($CFG->libdir.'/tablelib.php');
 +
 +
 +/**
 + * Base class for quiz reports that are basically a table with one row for each attempt.
 + *
 + * @copyright  2010 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +abstract class quiz_attempt_report extends quiz_default_report {
 +    /** @var object the quiz context. */
 +    protected $context;
 +
 +    /** @var boolean caches the results of {@link should_show_grades()}. */
 +    protected $showgrades = null;
 +
 +    /**
 +     * Should the grades be displayed in this report. That depends on the quiz
 +     * display options, and whether the quiz is graded.
 +     * @param object $quiz the quiz settings.
 +     * @return bool
 +     */
 +    protected function should_show_grades($quiz) {
 +        if (!is_null($this->showgrades)) {
 +            return $this->showgrades;
 +        }
 +
 +        if ($quiz->timeclose && time() > $quiz->timeclose) {
 +            $when = mod_quiz_display_options::AFTER_CLOSE;
 +        } else {
 +            $when = mod_quiz_display_options::LATER_WHILE_OPEN;
 +        }
 +        $reviewoptions = mod_quiz_display_options::make_from_quiz($quiz, $when);
 +
 +        $this->showgrades = quiz_has_grades($quiz) &&
 +                ($reviewoptions->marks >= question_display_options::MARK_AND_MAX ||
 +                has_capability('moodle/grade:viewhidden', $this->context));
 +
 +        return $this->showgrades;
 +    }
 +
 +    /**
 +     * Get information about which students to show in the report.
 +     * @param object $cm the coures module.
 +     * @return an array with four elements:
 +     *      0 => integer the current group id (0 for none).
 +     *      1 => array ids of all the students in this course.
 +     *      2 => array ids of all the students in the current group.
 +     *      3 => array ids of all the students to show in the report. Will be the
 +     *              same as either element 1 or 2.
 +     */
 +    protected function load_relevant_students($cm) {
 +        $currentgroup = groups_get_activity_group($cm, true);
 +
 +        if (!$students = get_users_by_capability($this->context,
 +                array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
 +                'u.id,1', '', '', '', '', '', false)) {
 +            $students = array();
 +        } else {
 +            $students = array_keys($students);
 +        }
 +
 +        if (empty($currentgroup)) {
 +            return array($currentgroup, $students, array(), $students);
 +        }
 +
 +        // We have a currently selected group.
 +        if (!$groupstudents = get_users_by_capability($this->context,
 +                array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
 +                'u.id,1', '', '', '', $currentgroup, '', false)) {
 +            $groupstudents = array();
 +        } else {
 +            $groupstudents = array_keys($groupstudents);
 +        }
 +
 +        return array($currentgroup, $students, $groupstudents, $groupstudents);
 +    }
 +
 +    /**
 +     * Alters $attemptsmode and $pagesize if the current values are inappropriate.
 +     * @param int $attemptsmode what sort of attempts to display (may be updated)
 +     * @param int $pagesize number of records to display per page (may be updated)
 +     * @param object $course the course settings.
 +     * @param int $currentgroup the currently selected group. 0 for none.
 +     */
 +    protected function validate_common_options(&$attemptsmode, &$pagesize, $course, $currentgroup) {
 +        if ($currentgroup) {
 +            //default for when a group is selected
 +            if ($attemptsmode === null  || $attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
 +                $attemptsmode = QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH;
 +            }
 +        } else if (!$currentgroup && $course->id == SITEID) {
 +            //force report on front page to show all, unless a group is selected.
 +            $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
 +        } else if ($attemptsmode === null) {
 +            //default
 +            $attemptsmode = QUIZ_REPORT_ATTEMPTS_ALL;
 +        }
 +
 +        if ($pagesize < 1) {
 +            $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE;
 +        }
 +    }
 +
 +    /**
 +     * Contruct all the parts of the main database query.
 +     * @param object $quiz the quiz settings.
 +     * @param string $qmsubselect SQL fragment from {@link quiz_report_qm_filter_select()}.
 +     * @param bool $qmfilter whether to show all, or only the final grade attempt.
 +     * @param int $attemptsmode which attempts to show. One of the QUIZ_REPORT_ATTEMPTS_... constants.
 +     * @param array $reportstudents list if userids of users to include in the report.
 +     * @return array with 4 elements ($fields, $from, $where, $params) that can be used to
 +     *      build the actual database query.
 +     */
 +    protected function base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $reportstudents) {
 +        global $DB;
 +
 +        $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
 +
 +        if ($qmsubselect) {
 +            $fields .= "\n(CASE WHEN $qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
 +        }
 +
 +        $fields .= '
 +                quiza.uniqueid AS usageid,
 +                quiza.id AS attempt,
 +                u.id AS userid,
 +                u.idnumber,
 +                u.firstname,
 +                u.lastname,
 +                u.picture,
 +                u.imagealt,
 +                u.institution,
 +                u.department,
 +                u.email,
 +                quiza.sumgrades,
 +                quiza.timefinish,
 +                quiza.timestart,
 +                CASE WHEN quiza.timefinish = 0 THEN null
 +                         WHEN quiza.timefinish > quiza.timestart THEN quiza.timefinish - quiza.timestart
 +                         ELSE 0 END AS duration';
 +            // To explain that last bit, in MySQL, qa.timestart and qa.timefinish
 +            // are unsigned. Since MySQL 5.5.5, when they introduced strict mode,
 +            // subtracting a larger unsigned int from a smaller one gave an error.
 +            // Therefore, we avoid doing that. timefinish can be non-zero and less
 +            // than timestart when you have two load-balanced servers with very
 +            // badly synchronised clocks, and a student does a really quick attempt.';
 +
 +        // This part is the same for all cases - join users and quiz_attempts tables
 +        $from = "\n{user} u";
 +        $from .= "\nLEFT JOIN {quiz_attempts} quiza ON quiza.userid = u.id AND quiza.quiz = :quizid";
 +        $params = array('quizid' => $quiz->id);
 +
 +        if ($qmsubselect && $qmfilter) {
 +            $from .= " AND $qmsubselect";
 +        }
 +        switch ($attemptsmode) {
 +            case QUIZ_REPORT_ATTEMPTS_ALL:
 +                // Show all attempts, including students who are no longer in the course
 +                $where = 'quiza.id IS NOT NULL AND quiza.preview = 0';
 +                break;
 +            case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH:
 +                // Show only students with attempts
 +                list($usql, $uparams) = $DB->get_in_or_equal(
-                         $reportstudents, SQL_PARAMS_NAMED, 'u00000');
++                        $reportstudents, SQL_PARAMS_NAMED, 'u');
 +                $params += $uparams;
 +                $where = "u.id $usql AND quiza.preview = 0 AND quiza.id IS NOT NULL";
 +                break;
 +            case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO:
 +                // Show only students without attempts
 +                list($usql, $uparams) = $DB->get_in_or_equal(
-                         $reportstudents, SQL_PARAMS_NAMED, 'u00000');
++                        $reportstudents, SQL_PARAMS_NAMED, 'u');
 +                $params += $uparams;
 +                $where = "u.id $usql AND quiza.id IS NULL";
 +                break;
 +            case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS:
 +                // Show all students with or without attempts
 +                list($usql, $uparams) = $DB->get_in_or_equal(
++                        $reportstudents, SQL_PARAMS_NAMED, 'u');
 +                $params += $uparams;
 +                $where = "u.id $usql AND (quiza.preview = 0 OR quiza.preview IS NULL)";
 +                break;
 +        }
 +
 +        return array($fields, $from, $where, $params);
 +    }
 +
 +    /**
 +     * Add all the user-related columns to the $columns and $headers arrays.
 +     * @param table_sql $table the table being constructed.
 +     * @param array $columns the list of columns. Added to.
 +     * @param array $headers the columns headings. Added to.
 +     */
 +    protected function add_user_columns($table, &$columns, &$headers) {
 +        global $CFG;
 +        if (!$table->is_downloading() && $CFG->grade_report_showuserimage) {
 +            $columns[] = 'picture';
 +            $headers[] = '';
 +        }
 +        if (!$table->is_downloading()) {
 +            $columns[] = 'fullname';
 +            $headers[] = get_string('name');
 +        } else {
 +            $columns[] = 'lastname';
 +            $headers[] = get_string('lastname');
 +            $columns[] = 'firstname';
 +            $headers[] = get_string('firstname');
 +        }
 +
 +        if ($CFG->grade_report_showuseridnumber) {
 +            $columns[] = 'idnumber';
 +            $headers[] = get_string('idnumber');
 +        }
 +
 +        if ($table->is_downloading()) {
 +            $columns[] = 'institution';
 +            $headers[] = get_string('institution');
 +
 +            $columns[] = 'department';
 +            $headers[] = get_string('department');
 +
 +            $columns[] = 'email';
 +            $headers[] = get_string('email');
 +        }
 +    }
 +
 +    /**
 +     * Set the display options for the user-related columns in the table.
 +     * @param table_sql $table the table being constructed.
 +     */
 +    protected function configure_user_columns($table) {
 +        $table->column_suppress('picture');
 +        $table->column_suppress('fullname');
 +        $table->column_suppress('idnumber');
 +
 +        $table->column_class('picture', 'picture');
 +        $table->column_class('lastname', 'bold');
 +        $table->column_class('firstname', 'bold');
 +        $table->column_class('fullname', 'bold');
 +    }
 +
 +    /**
 +     * Add all the time-related columns to the $columns and $headers arrays.
 +     * @param array $columns the list of columns. Added to.
 +     * @param array $headers the columns headings. Added to.
 +     */
 +    protected function add_time_columns(&$columns, &$headers) {
 +        $columns[] = 'timestart';
 +        $headers[] = get_string('startedon', 'quiz');
 +
 +        $columns[] = 'timefinish';
 +        $headers[] = get_string('timecompleted','quiz');
 +
 +        $columns[] = 'duration';
 +        $headers[] = get_string('attemptduration', 'quiz');
 +    }
 +
 +    /**
 +     * Add all the grade and feedback columns, if applicable, to the $columns
 +     * and $headers arrays.
 +     * @param object $quiz the quiz settings.
 +     * @param array $columns the list of columns. Added to.
 +     * @param array $headers the columns headings. Added to.
 +     */
 +    protected function add_grade_columns($quiz, &$columns, &$headers) {
 +        if ($this->should_show_grades($quiz)) {
 +            $columns[] = 'sumgrades';
 +            $headers[] = get_string('grade', 'quiz') . '/' .
 +                    quiz_format_grade($quiz, $quiz->grade);
 +        }
 +
 +        if (quiz_has_feedback($quiz)) {
 +            $columns[] = 'feedbacktext';
 +            $headers[] = get_string('feedback', 'quiz');
 +        }
 +    }
 +
 +    /**
 +     * Set up the table.
 +     * @param table_sql $table the table being constructed.
 +     * @param array $columns the list of columns.
 +     * @param array $headers the columns headings.
 +     * @param moodle_url $reporturl the URL of this report.
 +     * @param array $displayoptions the display options.
 +     * @param bool $collapsible whether to allow columns in the report to be collapsed.
 +     */
 +    protected function set_up_table_columns($table, $columns, $headers, $reporturl, $displayoptions, $collapsible) {
 +        $table->define_columns($columns);
 +        $table->define_headers($headers);
 +        $table->sortable(true, 'uniqueid');
 +
 +        $table->define_baseurl($reporturl->out(false, $displayoptions));
 +
 +        $this->configure_user_columns($table);
 +
 +        $table->no_sorting('feedbacktext');
 +        $table->column_class('sumgrades', 'bold');
 +
 +        $table->set_attribute('id', 'attempts');
 +
 +        $table->collapsible($collapsible);
 +    }
 +
 +    /**
 +     * Delete the quiz attempts
 +     * @param object $quiz the quiz settings. Attempts that don't belong to
 +     * this quiz are not deleted.
 +     * @param object $cm the course_module object.
 +     * @param array $attemptids the list of attempt ids to delete.
 +     * @param array $allowed This list of userids that are visible in the report.
 +     *      Users can only delete attempts that they are allowed to see in the report.
 +     *      Empty means all users.
 +     */
 +    protected function delete_selected_attempts($quiz, $cm, $attemptids, $allowed) {
 +        global $DB;
 +
 +        foreach ($attemptids as $attemptid) {
 +            $attempt = $DB->get_record('quiz_attempts', array('id' => $attemptid));
 +            if (!$attempt || $attempt->quiz != $quiz->id || $attempt->preview != 0) {
 +                // Ensure the attempt exists, and belongs to this quiz. If not skip.
 +                continue;
 +            }
 +            if ($allowed && !array_key_exists($attempt->userid, $allowed)) {
 +                // Ensure the attempt belongs to a student included in the report. If not skip.
 +                continue;
 +            }
 +            add_to_log($quiz->course, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id,
 +                    $attemptid, $cm->id);
 +            quiz_delete_attempt($attempt, $quiz);
 +        }
 +    }
 +}
 +
 +/**
 + * Base class for the table used by {@link quiz_attempt_report}s.
 + *
 + * @copyright  2010 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +abstract class quiz_attempt_report_table extends table_sql {
 +    public $useridfield = 'userid';
 +
 +    /** @var moodle_url the URL of this report. */
 +    protected $reporturl;
 +
 +    /** @var array the display options. */
 +    protected $displayoptions;
 +
 +    /**
 +     * @var array information about the latest step of each question.
 +     * Loaded by {@link load_question_latest_steps()}, if applicable.
 +     */
 +    protected $lateststeps = null;
 +
 +    protected $quiz;
 +    protected $context;
 +    protected $qmsubselect;
 +    protected $groupstudents;
 +    protected $students;
 +    protected $questions;
 +    protected $candelete;
 +
 +    public function __construct($uniqueid, $quiz, $context, $qmsubselect, $groupstudents,
 +            $students, $questions, $candelete, $reporturl, $displayoptions) {
 +        parent::__construct($uniqueid);
 +        $this->quiz = $quiz;
 +        $this->context = $context;
 +        $this->qmsubselect = $qmsubselect;
 +        $this->groupstudents = $groupstudents;
 +        $this->students = $students;
 +        $this->questions = $questions;
 +        $this->candelete = $candelete;
 +        $this->reporturl = $reporturl;
 +        $this->displayoptions = $displayoptions;
 +    }
 +
 +    public function col_checkbox($attempt) {
 +        if ($attempt->attempt) {
 +            return '<input type="checkbox" name="attemptid[]" value="'.$attempt->attempt.'" />';
 +        } else {
 +            return '';
 +        }
 +    }
 +
 +    public function col_picture($attempt) {
 +        global $COURSE, $OUTPUT;
 +        $user = new stdClass();
 +        $user->id = $attempt->userid;
 +        $user->lastname = $attempt->lastname;
 +        $user->firstname = $attempt->firstname;
 +        $user->imagealt = $attempt->imagealt;
 +        $user->picture = $attempt->picture;
 +        $user->email = $attempt->email;
 +        return $OUTPUT->user_picture($user);
 +    }
 +
 +    public function col_fullname($attempt) {
 +        $html = parent::col_fullname($attempt);
 +        if ($this->is_downloading()) {
 +            return $html;
 +        }
 +
 +        return $html . html_writer::empty_tag('br') . html_writer::link(
 +                new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)),
 +                get_string('reviewattempt', 'quiz'), array('class' => 'reviewlink'));
 +    }
 +
 +    public function col_timestart($attempt) {
 +        if ($attempt->attempt) {
 +            return userdate($attempt->timestart, $this->strtimeformat);
 +        } else {
 +            return  '-';
 +        }
 +    }
 +
 +    public function col_timefinish($attempt) {
 +        if ($attempt->attempt && $attempt->timefinish) {
 +            return userdate($attempt->timefinish, $this->strtimeformat);
 +        } else {
 +            return  '-';
 +        }
 +    }
 +
 +    public function col_duration($attempt) {
 +        if ($attempt->timefinish) {
 +            return format_time($attempt->timefinish - $attempt->timestart);
 +        } elseif ($attempt->timestart) {
 +            return get_string('unfinished', 'quiz');
 +        } else {
 +            return '-';
 +        }
 +    }
 +
 +    public function col_feedbacktext($attempt) {
 +        if (!$attempt->timefinish) {
 +            return '-';
 +        }
 +
 +        if (!$this->is_downloading()) {
 +            return quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false),
 +                    $this->quiz->id, $this->context);
 +        } else {
 +            return strip_tags(quiz_report_feedback_for_grade(quiz_rescale_grade($attempt->sumgrades, $this->quiz, false), $this->quiz->id));
 +        }
 +    }
 +
 +    public function get_row_class($attempt) {
 +        if ($this->qmsubselect && $attempt->gradedattempt) {
 +            return 'gradedattempt';
 +        } else {
 +            return '';
 +        }
 +    }
 +
 +    /**
 +     * Make a link to review an individual question in a popup window.
 +     *
 +     * @param string $data HTML fragment. The text to make into the link.
 +     * @param object $attempt data for the row of the table being output.
 +     * @param int $slot the number used to identify this question within this usage.
 +     */
 +    public function make_review_link($data, $attempt, $slot) {
 +        global $OUTPUT;
 +
 +        $stepdata = $this->lateststeps[$attempt->usageid][$slot];
 +        $state = question_state::get($stepdata->state);
 +
 +        $flag = '';
 +        if ($stepdata->flagged) {
 +            $flag = ' ' . $OUTPUT->pix_icon('i/flagged', get_string('flagged', 'question'),
 +                    'moodle', array('class' => 'questionflag'));
 +        }
 +
 +        $feedbackimg = '';
 +        if ($state->is_finished() && $state != question_state::$needsgrading) {
 +            $feedbackimg = ' ' . $this->icon_for_fraction($stepdata->fraction);
 +        }
 +
 +        $output = html_writer::tag('span', html_writer::tag('span',
 +                $data . $feedbackimg . $flag,
 +                array('class' => $state->get_state_class(true))), array('class' => 'que'));
 +
 +        $url = new moodle_url('/mod/quiz/reviewquestion.php',
 +                array('attempt' => $attempt->attempt, 'slot' => $slot));
 +        $output = $OUTPUT->action_link($url, $output,
 +                new popup_action('click', $url, 'reviewquestion', array('height' => 450, 'width' => 650)),
 +                array('title' => get_string('reviewresponse', 'quiz')));
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Return an appropriate icon (green tick, red cross, etc.) for a grade.
 +     * @param float $fraction grade on a scale 0..1.
 +     * @return string html fragment.
 +     */
 +    protected function icon_for_fraction($fraction) {
 +        global $OUTPUT;
 +
 +        $state = question_state::graded_state_for_fraction($fraction);
 +        if ($state == question_state::$gradedright) {
 +            $icon = 'i/tick_green_big';
 +        } else if ($state == question_state::$gradedpartial) {
 +            $icon = 'i/tick_amber_big';
 +        } else {
 +            $icon = 'i/cross_red_big';
 +        }
 +
 +        return $OUTPUT->pix_icon($icon, get_string($state->get_feedback_class(), 'question'),
 +                'moodle', array('class' => 'icon'));
 +    }
 +
 +    /**
 +     * Load information about the latest state of selected questions in selected attempts.
 +     *
 +     * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
 +     *
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @param array $slots A list of slots for the questions you want to konw about.
 +     * @return array of records. See the SQL in this function to see the fields available.
 +     */
 +    protected function load_question_latest_steps(qubaid_condition $qubaids) {
 +        $dm = new question_engine_data_mapper();
 +        $latesstepdata = $dm->load_questions_usages_latest_steps(
 +                $qubaids, array_keys($this->questions));
 +
 +        $lateststeps = array();
 +        foreach ($latesstepdata as $step) {
 +            $lateststeps[$step->questionusageid][$step->slot] = $step;
 +        }
 +
 +        return $lateststeps;
 +    }
 +
 +    /**
 +     * @return bool should {@link query_db()} call {@link load_question_latest_steps}?
 +     */
 +    protected function requires_latest_steps_loaded() {
 +        return false;
 +    }
 +
 +    /**
 +     * Is this a column that depends on joining to the latest state information?
 +     * If so, return the corresponding slot. If not, return false.
 +     * @param string $column a column name
 +     * @return int false if no, else a slot.
 +     */
 +    protected function is_latest_step_column($column) {
 +        return false;
 +    }
 +
 +    /**
 +     * Get any fields that might be needed when sorting on date for a particular slot.
 +     * @param int $slot the slot for the column we want.
 +     * @param string $alias the table alias for latest state information relating to that slot.
 +     */
 +    protected function get_required_latest_state_fields($slot, $alias) {
 +        return '';
 +    }
 +
 +    /**
 +     * Add the information about the latest state of the question with slot
 +     * $slot to the query.
 +     *
 +     * The extra information is added as a join to a
 +     * 'table' with alias qa$slot, with columns that are a union of
 +     * the columns of the question_attempts and question_attempts_states tables.
 +     *
 +     * @param int $slot the question to add information for.
 +     */
 +    protected function add_latest_state_join($slot) {
 +        $alias = 'qa' . $slot;
 +
 +        $fields = $this->get_required_latest_state_fields($slot, $alias);
 +        if (!$fields) {
 +            return;
 +        }
 +
 +        $dm = new question_engine_data_mapper();
 +        $inlineview = $dm->question_attempt_latest_state_view($alias);
 +
 +        $this->sql->fields .= ",\n$fields";
 +        $this->sql->from .= "\nLEFT JOIN $inlineview ON " .
 +                "$alias.questionusageid = quiza.uniqueid AND $alias.slot = :{$alias}slot";
 +        $this->sql->params[$alias . 'slot'] = $slot;
 +    }
 +
 +    /**
 +     * Get an appropriate qubaid_condition for loading more data about the
 +     * attempts we are displaying.
 +     * @return qubaid_condition
 +     */
 +    protected function get_qubaids_condition() {
 +        if (is_null($this->rawdata)) {
 +            throw new coding_exception(
 +                    'Cannot call get_qubaids_condition until the main data has been loaded.');
 +        }
 +
 +        if ($this->is_downloading()) {
 +            // We want usages for all attempts.
 +            return new qubaid_join($this->sql->from, 'quiza.uniqueid',
 +                    $this->sql->where, $this->sql->params);
 +        }
 +
 +        $qubaids = array();
 +        foreach ($this->rawdata as $attempt) {
 +            if ($attempt->usageid > 0) {
 +                $qubaids[] = $attempt->usageid;
 +            }
 +        }
 +
 +        return new qubaid_list($qubaids);
 +    }
 +
 +    public function query_db($pagesize, $useinitialsbar = true) {
 +        $doneslots = array();
 +        foreach ($this->get_sort_columns() as $column => $notused) {
 +            $slot = $this->is_latest_step_column($column);
 +            if ($slot && !in_array($slot, $doneslots)) {
 +                $this->add_latest_state_join($slot);
 +                $doneslots[] = $slot;
 +            }
 +        }
 +
 +        parent::query_db($pagesize, $useinitialsbar);
 +
 +        if ($this->requires_latest_steps_loaded()) {
 +            $qubaids = $this->get_qubaids_condition();
 +            $this->lateststeps = $this->load_question_latest_steps($qubaids);
 +        }
 +    }
 +
 +    public function get_sort_columns() {
 +        // Add attemptid as a final tie-break to the sort. This ensures that
 +        // Attempts by the same student appear in order when just sorting by name.
 +        $sortcolumns = parent::get_sort_columns();
 +        $sortcolumns['quiza.id'] = SORT_ASC;
 +        return $sortcolumns;
 +    }
 +}
 +
@@@ -140,416 -216,256 +140,416 @@@ class quiz_grading_report extends quiz_
          return true;
      }
  
 -    /**
 -     * Prints a table with users and their attempts
 -     *
 -     * @return void
 -     * @todo Add current grade to the table
 -     *       Finnish documenting
 -     **/
 -    function view_question($quiz, $question, $totalattempts, $ungraded) {
 -        global $CFG, $DB, $OUTPUT;
 +    protected function get_qubaids_condition() {
 +        global $DB;
 +
 +        $where = "quiza.quiz = :mangrquizid AND
 +                quiza.preview = 0 AND
 +                quiza.timefinish <> 0";
 +        $params = array('mangrquizid' => $this->cm->instance);
 +
 +        if ($this->currentgroup) {
 +            list($usql, $uparam) = $DB->get_in_or_equal(array_keys($this->users),
-                     SQL_PARAMS_NAMED, 'mangru00000');
++                    SQL_PARAMS_NAMED, 'mangru');
 +            $where .= ' AND quiza.userid ' . $usql;
 +            $params += $uparam;
 +        }
 +
 +        return new qubaid_join('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
 +    }
 +
 +    protected function load_attempts_by_usage_ids($qubaids) {
 +        global $DB;
  
 -        $usercount = count($this->users);
 +        list($asql, $params) = $DB->get_in_or_equal($qubaids);
 +        $params[] = $this->quiz->id;
  
 -        // set up table
 -        $tablecolumns = array('picture', 'fullname', 'timefinish', 'grade');
 -        $tableheaders = array('', get_string('name'), get_string("completedon", "quiz"), '');
 +        $attemptsbyid = $DB->get_records_sql("
 +                SELECT quiza.*, u.firstname, u.lastname, u.idnumber
 +                FROM {quiz_attempts} quiza
 +                JOIN {user} u ON u.id = quiza.userid
 +                WHERE quiza.uniqueid $asql AND quiza.timefinish <> 0 AND quiza.quiz = ?",
 +                $params);
 +
 +        $attempts = array();
 +        foreach ($attemptsbyid as $attempt) {
 +            $attempts[$attempt->uniqueid] = $attempt;
 +        }
 +        return $attempts;
 +    }
 +
 +    /**
 +     * Get the URL of the front page of the report that lists all the questions.
 +     * @param $includeauto if not given, use the current setting, otherwise,
 +     *      force a paricular value of includeauto in the URL.
 +     * @return string the URL.
 +     */
 +    protected function base_url() {
 +        return new moodle_url('/mod/quiz/report.php',
 +                array('id' => $this->cm->id, 'mode' => 'grading'));
 +    }
  
 -        $table = new flexible_table('mod-quiz-report-grading');
 +    /**
 +     * Get the URL of the front page of the report that lists all the questions.
 +     * @param $includeauto if not given, use the current setting, otherwise,
 +     *      force a paricular value of includeauto in the URL.
 +     * @return string the URL.
 +     */
 +    protected function list_questions_url($includeauto = null) {
 +        $url = $this->base_url();
  
 -        $table->define_columns($tablecolumns);
 -        $table->define_headers($tableheaders);
 -        $table->define_baseurl($this->viewurl->out());
 +        $url->params($this->viewoptions);
  
 -        $table->sortable(true);
 -        $table->initialbars($usercount>20);  // will show initialbars if there are more than 20 users
 -        $table->pageable(true);
 -        $table->collapsible(true);
 +        if (!is_null($includeauto)) {
 +            $url->param('includeauto', $includeauto);
 +        }
  
 -        $table->column_suppress('fullname');
 -        $table->column_suppress('picture');
 -        $table->column_suppress('grade');
 +        return $url;
 +    }
  
 -        $table->column_class('picture', 'picture');
 +    /**
 +     * @param int $slot
 +     * @param int $questionid
 +     * @param string $grade
 +     * @param mixed $page = true, link to current page. false = omit page.
 +     *      number = link to specific page.
 +     */
 +    protected function grade_question_url($slot, $questionid, $grade, $page = true) {
 +        $url = $this->base_url();
 +        $url->params(array('slot' => $slot, 'qid' => $questionid, 'grade' => $grade));
 +        $url->params($this->viewoptions);
 +
 +        $options = $this->viewoptions;
 +        if (!$page) {
 +            $url->remove_params('page');
 +        } else if (is_integer($page)) {
 +            $url->param('page', $page);
 +        }
  
 -        // attributes in the table tag
 -        $table->set_attribute('cellspacing', '0');
 -        $table->set_attribute('id', 'attempts');
 -        $table->set_attribute('class', 'generaltable generalbox');
 -        $table->set_attribute('align', 'center');
 -        //$table->set_attribute('width', '50%');
 +        return $url;
 +    }
  
 -        // get it ready!
 -        $table->setup();
 +    protected function format_count_for_table($counts, $type, $gradestring) {
 +        $result = $counts->$type;
 +        if ($counts->$type > 0) {
 +            $result .= ' ' . html_writer::link($this->grade_question_url(
 +                    $counts->slot, $counts->questionid, $type),
 +                    get_string($gradestring, 'quiz_grading'),
 +                    array('class' => 'gradetheselink'));
 +        }
 +        return $result;
 +    }
  
 -        list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, true, $question->id);
 +    protected function display_index($includeauto) {
 +        global $OUTPUT;
  
 -        list($twhere, $tparams) = $table->get_sql_where();
 -        if ($twhere) {
 -            $where .= ' AND '.$twhere; //initial bar
 -            $params = array_merge($params, $tparams);
 +        if ($groupmode = groups_get_activity_groupmode($this->cm)) {
 +            // Groups are being used
 +            groups_print_activity_menu($this->cm, $this->list_questions_url());
          }
  
 -        // sorting of the table
 -        if ($sort = $table->get_sql_sort()) {
 -            $sort = 'ORDER BY '.$sort;  // seems like I would need to have u. or qa. infront of the ORDER BY attribues... but seems to work..
 +        echo $OUTPUT->heading(get_string('questionsthatneedgrading', 'quiz_grading'));
 +        if ($includeauto) {
 +            $linktext = get_string('hideautomaticallygraded', 'quiz_grading');
          } else {
 -            // my default sort rule
 -            $sort = 'ORDER BY u.firstname, u.lastname, qa.timefinish ASC';
 -        }
 -
 -        // set up the pagesize
 -        $table->pagesize(QUIZ_REPORT_DEFAULT_PAGE_SIZE, $totalattempts);
 -
 -        // get the attempts and process them
 -        echo '<div id="tablecontainer">';
 -        if ($attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size())) {
 -            // grade all link
 -            $links = "<strong><a href=\"report.php?mode=grading&amp;gradeall=1&amp;q=$quiz->id&amp;questionid=$question->id\">".get_string('gradeall', 'quiz_grading', $totalattempts).'</a></strong>';
 -            if ($ungraded>0){
 -                $links .="<br /><strong><a href=\"report.php?mode=grading&amp;gradeungraded=1&amp;q=$quiz->id&amp;questionid=$question->id\">".get_string('gradeungraded', 'quiz_grading', $ungraded).'</a></strong>';
 -                if ($ungraded>QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE){
 -                    $links .="<br /><strong><a href=\"report.php?mode=grading&amp;gradenextungraded=1&amp;q=$quiz->id&amp;questionid=$question->id\">".get_string('gradenextungraded', 'quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE).'</a></strong>';
 -                }
 +            $linktext = get_string('alsoshowautomaticallygraded', 'quiz_grading');
 +        }
 +        echo html_writer::tag('p', html_writer::link($this->list_questions_url(!$includeauto),
 +                $linktext), array('class' => 'toggleincludeauto'));
 +
 +        $statecounts = $this->get_question_state_summary(array_keys($this->questions));
 +
 +        $data = array();
 +        foreach ($statecounts as $counts) {
 +            if ($counts->all == 0) {
 +                continue;
 +            }
 +            if (!$includeauto && $counts->needsgrading == 0 && $counts->manuallygraded == 0) {
 +                continue;
              }
 -            $table->add_data_keyed(array('grade'=> $links));
 -            $table->add_separator();
 -            foreach($attempts as $attempt) {
  
 -                $user = clone($attempt);
 -                $user->id = $user->userid;
 -                $picture = $OUTPUT->user_picture($user, array('courseid'=>$quiz->course));
 +            $row = array();
  
 -                // link to student profile
 -                $userlink = "<a href=\"$CFG->wwwroot/user/view.php?id=$attempt->userid&amp;course=$quiz->course\">".
 -                            fullname($attempt, true).'</a>';
 +            $row[] = $this->questions[$counts->slot]->number;
  
 -                $gradedclass = question_state_is_graded($attempt)?' class="highlightgraded" ':'';
 -                $gradedstring = question_state_is_graded($attempt)?(' '.get_string('graded','quiz_grading')):'';
 +            $row[] = format_string($counts->name);
  
 -                // link for the attempt
 -                $attemptlink = "<a {$gradedclass}href=\"report.php?mode=grading&amp;q=$quiz->id&amp;questionid=$question->id&amp;attemptid=$attempt->attemptid\">".
 -                        userdate($attempt->timefinish, get_string('strftimedatetime')).
 -                        $gradedstring.'</a>';
 +            $row[] = $this->format_count_for_table($counts, 'needsgrading', 'grade');
  
 -                // grade all attempts for this user
 -                $gradelink = "<a href=\"report.php?mode=grading&amp;q=$quiz->id&amp;questionid=$question->id&amp;userid=$attempt->userid\">".
 -                        get_string('grade').'</a>';
 +            $row[] = $this->format_count_for_table($counts, 'manuallygraded', 'updategrade');
  
 -                $table->add_data( array($picture, $userlink, $attemptlink, $gradelink) );
 +            if ($includeauto) {
 +                $row[] = $this->format_count_for_table($counts, 'autograded', 'updategrade');
              }
 -            $table->add_separator();
 -            $table->add_data_keyed(array('grade'=> $links));
 +
 +            $row[] = $this->format_count_for_table($counts, 'all', 'gradeall');
 +
 +            $data[] = $row;
 +        }
 +
 +        if (empty($data)) {
 +            echo $OUTPUT->heading(get_string('noquestionsfound', 'quiz_grading'));
 +            return;
 +        }
 +
 +        $table = new html_table();
 +        $table->class = 'generaltable';
 +        $table->id = 'questionstograde';
 +
 +        $table->head[] = get_string('qno', 'quiz_grading');
 +        $table->head[] = get_string('questionname', 'quiz_grading');
 +        $table->head[] = get_string('tograde', 'quiz_grading');
 +        $table->head[] = get_string('alreadygraded', 'quiz_grading');
 +        if ($includeauto) {
 +            $table->head[] = get_string('automaticallygraded', 'quiz_grading');
          }
 -        // print everything here
 -        $table->print_html();
 -        echo '</div>';
 +        $table->head[] = get_string('total', 'quiz_grading');
 +
 +        $table->data = $data;
 +        echo html_writer::table($table);
      }
  
 +    protected function display_grading_interface($slot, $questionid, $grade,
 +            $pagesize, $page, $shownames, $showidnumbers, $order) {
 +        global $OUTPUT;
  
 -    /**
 -     * Prints questions with comment and grade form underneath each question
 -     *
 -     * @return void
 -     * @todo Finish documenting this function
 -     **/
 -    function print_questions_and_form($quiz, $question, $userid, $attemptid, $gradeungraded, $gradenextungraded, $ungraded) {
 -        global $CFG, $DB, $OUTPUT;
 +        // Make sure there is something to do.
 +        $statecounts = $this->get_question_state_summary(array($slot));
  
 -        $context = get_context_instance(CONTEXT_MODULE, $this->cm->id);
 +        $counts = null;
 +        foreach ($statecounts as $record) {
 +            if ($record->questionid == $questionid) {
 +                $counts = $record;
 +                break;
 +            }
 +        }
  
 -        $questions[$question->id] = &$question;
 -        $usehtmleditor = can_use_html_editor();
 +        // If not, redirect back to the list.
 +        if (!$counts || $counts->$grade == 0) {
 +            redirect($this->list_questions_url(), get_string('alldoneredirecting', 'quiz_grading'));
 +        }
  
 -        list($select, $from, $where, $params) = $this->attempts_sql($quiz->id, false, $question->id, $userid, $attemptid, $gradeungraded, $gradenextungraded);
 +        if ($pagesize * $page >= $counts->$grade) {
 +            $page = 0;
 +        }
  
 -        $sort = 'ORDER BY u.firstname, u.lastname, qa.attempt ASC';
 +        list($qubaids, $count) = $this->get_usage_ids_where_question_in_state(
 +                $grade, $slot, $questionid, $order, $page, $pagesize);
 +        $attempts = $this->load_attempts_by_usage_ids($qubaids);
 +
 +        // Prepare the form.
 +        $hidden = array(
 +            'id' => $this->cm->id,
 +            'mode' => 'grading',
 +            'slot' => $slot,
 +            'qid' => $questionid,
 +            'page' => $page,
 +        );
 +        if (array_key_exists('includeauto', $this->viewoptions)) {
 +            $hidden['includeauto'] = $this->viewoptions['includeauto'];
 +        }
 +        $mform = new quiz_grading_settings($hidden, $counts, $shownames, $showidnumbers);
 +
 +        // Tell the form the current settings.
 +        $settings = new stdClass();
 +        $settings->grade = $grade;
 +        $settings->pagesize = $pagesize;
 +        $settings->order = $order;
 +        $mform->set_data($settings);
 +
 +        // Print the heading and form.
 +        echo question_engine::initialise_js();
 +
 +        $a = new stdClass();
 +        $a->number = $this->questions[$slot]->number;
 +        $a->questionname = format_string($counts->name);
 +        echo $OUTPUT->heading(get_string('gradingquestionx', 'quiz_grading', $a));
 +        echo html_writer::tag('p', html_writer::link($this->list_questions_url(),
 +                get_string('backtothelistofquestions', 'quiz_grading')),
 +                array('class' => 'mdl-align'));
 +
 +        $mform->display();
 +
 +        // Paging info.
 +        $a = new stdClass();
 +        $a->from = $page * $pagesize + 1;
 +        $a->to = min(($page + 1) * $pagesize, $count);
 +        $a->of = $count;
 +        echo $OUTPUT->heading(get_string('gradingattemptsxtoyofz', 'quiz_grading', $a), 3);
 +
 +        if ($count > $pagesize && $order != 'random') {
 +            echo $OUTPUT->paging_bar($count, $page, $pagesize,
 +                    $this->grade_question_url($slot, $questionid, $grade, false));
 +        }
  
 -        if ($gradenextungraded){
 -            $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, 0, QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE);
 -        } else {
 -            $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params);
 -        }
 -        if ($attempts){
 -            $firstattempt = current($attempts);
 -            $fullname = fullname($firstattempt);
 -            if ($gradeungraded) { // getting all ungraded attempts
 -                echo $OUTPUT->heading(get_string('gradingungraded','quiz_grading', $ungraded), 3);
 -            } else if ($gradenextungraded) { // getting next ungraded attempts
 -                echo $OUTPUT->heading(get_string('gradingnextungraded','quiz_grading', QUIZ_REPORT_DEFAULT_GRADING_PAGE_SIZE), 3);
 -            } else if ($userid){
 -                echo $OUTPUT->heading(get_string('gradinguser','quiz_grading', $fullname), 3);
 -            } else if ($attemptid){
 -                $a = new stdClass();
 -                $a->fullname = $fullname;
 -                $a->attempt = $firstattempt->attempt;
 -                echo $OUTPUT->heading(get_string('gradingattempt','quiz_grading', $a), 3);
 -            } else {
 -                echo $OUTPUT->heading(get_string('gradingall','quiz_grading', count($attempts)), 3);
 +        // Display the form with one section for each attempt.
 +        $usehtmleditor = can_use_html_editor();
 +        $sesskey = sesskey();
 +        $qubaidlist = implode(',', $qubaids);
 +        echo html_writer::start_tag('form', array('method' => 'post',
 +                'action' => $this->grade_question_url($slot, $questionid, $grade, $page),
 +                'class' => 'mform', 'id' => 'manualgradingform')) .
 +                html_writer::start_tag('div') .
 +                html_writer::input_hidden_params(new moodle_url('', array(
 +                'qubaids' => $qubaidlist, 'slots' => $slot, 'sesskey' => $sesskey)));
 +
 +        foreach ($qubaids as $qubaid) {
 +            $attempt = $attempts[$qubaid];
 +            $quba = question_engine::load_questions_usage_by_activity($qubaid);
 +            $displayoptions = quiz_get_review_options($this->quiz, $attempt, $this->context);
 +            $displayoptions->hide_all_feedback();
 +            $displayoptions->history = question_display_options::HIDDEN;
 +            $displayoptions->manualcomment = question_display_options::EDITABLE;
 +
 +            $heading = $this->get_question_heading($attempt, $shownames, $showidnumbers);
 +            if ($heading) {
 +                echo $OUTPUT->heading($heading, 4);
              }
 +            echo $quba->render_question($slot, $displayoptions, $this->questions[$slot]->number);
 +        }
  
 -            // Display the form with one part for each selected attempt
 +        echo html_writer::tag('div', html_writer::empty_tag('input', array(
 +                'type' => 'submit', 'value' => get_string('saveandnext', 'quiz_grading'))),
 +                array('class' => 'mdl-align')) .
 +                html_writer::end_tag('div') . html_writer::end_tag('form');
 +    }
  
 -            echo '<form method="post" action="report.php" class="mform" id="manualgradingform">'.
 -                '<input type="hidden" name="mode" value="grading" />'.
 -                '<input type="hidden" name="q" value="'.$quiz->id.'" />'.
 -                '<input type="hidden" name="sesskey" value="'.sesskey().'" />'.
 -                '<input type="hidden" name="questionid" value="'.$question->id.'" />';
 +    protected function get_question_heading($attempt, $shownames, $showidnumbers) {
 +        $a = new stdClass();
 +        $a->attempt = $attempt->attempt;
 +        $a->fullname = fullname($attempt);
 +        $a->idnumber = $attempt->idnumber;
 +
 +        $showidnumbers &= !empty($attempt->idnumber);
 +
 +        if ($shownames && $showidnumbers) {
 +            return get_string('gradingattemptwithidnumber', 'quiz_grading', $a);
 +        } else if ($shownames) {
 +            return get_string('gradingattempt', 'quiz_grading', $a);
 +        } else if ($showidnumbers) {
 +            $a->fullname = $attempt->idnumber;
 +            return get_string('gradingattempt', 'quiz_grading', $a);
 +        } else {
 +            return '';
 +        }
 +    }
  
 -            foreach ($attempts as $attempt) {
 +    protected function validate_submitted_marks() {
  
 -                // Load the state for this attempt (The questions array was created earlier)
 -                $states = get_question_states($questions, $quiz, $attempt);
 -                // The $states array is indexed by question id but because we are dealing
 -                // with only one question there is only one entry in this array
 -                $state = &$states[$question->id];
 +        $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
 +        if (!$qubaids) {
 +            return false;
 +        }
 +        $qubaids = clean_param(explode(',', $qubaids), PARAM_INT);
  
 -                $options = quiz_get_reviewoptions($quiz, $attempt, $context);
 -                unset($options->questioncommentlink);
 -                $options->readonly = 1;
 +        $slots = optional_param('slots', '', PARAM_SEQUENCE);
 +        if (!$slots) {
 +            $slots = array();
 +        } else {
 +            $slots = explode(',', $slots);
 +        }
  
 -                if (question_state_is_graded($state)) {
 -                    $gradedclass = 'main highlightgraded';
 -                    $gradedstring = ' ' . get_string('graded','quiz_grading');
 -                } else {
 -                    $gradedclass = 'main';
 -                    $gradedstring = '';
 +        foreach ($qubaids as $qubaid) {
 +            foreach ($slots as $slot) {
 +                $prefix = 'q' . $qubaid . ':' . $slot . '_';
 +                $mark = optional_param($prefix . '-mark', null, PARAM_NUMBER);
 +                $maxmark = optional_param($prefix . '-maxmark', null, PARAM_NUMBER);
 +                $minfraction = optional_param($prefix . ':minfraction', null, PARAM_NUMBER);
 +                if (!is_null($mark) && ($mark < $minfraction * $maxmark || $mark > $maxmark)) {
 +                    return false;
                  }
 -                $a = new stdClass();
 -                $a->fullname = fullname($attempt, true);
 -                $a->attempt = $attempt->attempt;
 -
 -                // print the user name, attempt count, the question, and some more hidden fields
 -                echo '<div class="boxaligncenter" width="80%" style="clear:left;padding:15px;">';
 -                echo $OUTPUT->heading(get_string('gradingattempt', 'quiz_grading', $a) . $gradedstring, 3, $gradedclass);
 -
 -                // Print the question, without showing any previous comment.
 -                $copy = $state->manualcomment;
 -                $state->manualcomment = '';
 -                $options->noeditlink = true;
 -                print_question($question, $state, '', $quiz, $options);
 -
 -                // The print the comment and grade fields, putting back the previous comment.
 -                $state->manualcomment = $copy;
 -                question_print_comment_fields($question, $state, 'manualgrades[' . $attempt->uniqueid . ']',
 -                        $quiz, get_string('manualgrading', 'quiz'));
 -
 -                echo '</div>';
              }
 -            echo '<div class="boxaligncenter"><input type="submit" value="'.get_string('savechanges').'" /></div>'.
 -                '</form>';
 -        } else {
 -            echo $OUTPUT->notification(get_string('noattemptstoshow', 'quiz'));
          }
 +
 +        return true;
      }
  
 -    function attempts_sql($quizid, $wantstateevent=false, $questionid=0, $userid=0, $attemptid=0, $gradeungraded=0, $gradenextungraded=0){
 -        global $CFG, $DB;
 -        // this sql joins the attempts table and the user table
 -        $select = 'SELECT qa.id AS attemptid, qa.uniqueid, qa.attempt, qa.timefinish, qa.preview,
 -                    u.id AS userid, u.firstname, u.lastname, u.picture, u.imagealt, u.email ';
 -        if ($wantstateevent && $questionid){
 -            $select .= ', qs.event ';
 +    protected function process_submitted_data() {
 +        global $DB;
 +
 +        $qubaids = optional_param('qubaids', null, PARAM_SEQUENCE);
 +        if (!$qubaids) {
 +            return;
          }
 -        $from   = 'FROM {user} u, ' .
 -                '{quiz_attempts} qa ';
 -        $params = array();
  
 -        $from .= "LEFT JOIN {question_sessions} qns " .
 -                "ON (qns.attemptid = qa.uniqueid AND qns.questionid = ?) ";
 -        $params[] = $questionid;
 -        $from .=  "LEFT JOIN  {question_states} qs " .
 -                "ON (qs.id = qns.newest AND qs.question = ?) ";
 -        $params[] = $questionid;
 -
 -        list($usql, $u_params) = $DB->get_in_or_equal(array_keys($this->users));
 -        if ($gradenextungraded || $gradeungraded) { // get ungraded attempts
 -            $where = "WHERE u.id $usql AND qs.event NOT IN (".QUESTION_EVENTS_GRADED.")";
 -            $params = array_merge($params, $u_params);
 -        } else if ($userid) { // get all the attempts for a specific user
 -            $where = 'WHERE u.id=?';
 -            $params[] = $userid;
 -        } else if ($attemptid) { // get a specific attempt
 -            $where = 'WHERE qa.id=? ';
 -            $params[] = $attemptid;
 -        } else { // get all user attempts
 -            $where  = "WHERE u.id $usql ";
 -            $params = array_merge($params, $u_params);
 -        }
 -
 -        $where .= ' AND qs.event IN ('.QUESTION_EVENTS_CLOSED_OR_GRADED.')';
 -
 -        $where .= ' AND u.id = qa.userid AND qa.quiz = ?';
 -        $params[] = $quizid;
 -        // ignore previews
 -        $where .= ' AND preview = 0 ';
 -
 -        $where .= ' AND qa.timefinish != 0 ';
 -
 -        return array($select, $from, $where, $params);
 +        $qubaids = clean_param(explode(',', $qubaids), PARAM_INT);
 +        $attempts = $this->load_attempts_by_usage_ids($qubaids);
 +
 +        $transaction = $DB->start_delegated_transaction();
 +        foreach ($qubaids as $qubaid) {
 +            $attempt = $attempts[$qubaid];
 +            $quba = question_engine::load_questions_usage_by_activity($qubaid);
 +            $attemptobj = new quiz_attempt($attempt, $this->quiz, $this->cm, $this->course);
 +            $attemptobj->process_all_actions(time());
 +        }
 +        $transaction->allow_commit();
      }
  
 -}
 +    /**
 +     * Load information about the number of attempts at various questions in each
 +     * summarystate.
 +     *
 +     * The results are returned as an two dimensional array $qubaid => $slot => $dataobject
 +     *
 +     * @param array $slots A list of slots for the questions you want to konw about.
 +     * @return array The array keys are slot,qestionid. The values are objects with
 +     * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
 +     * $manuallygraded and $all.
 +     */
 +    protected function get_question_state_summary($slots) {
 +        $dm = new question_engine_data_mapper();
 +        return $dm->load_questions_usages_question_state_summary(
 +                $this->get_qubaids_condition(), $slots);
 +    }
  
 +    /**
 +     * Get a list of usage ids where the question with slot $slot, and optionally
 +     * also with question id $questionid, is in summary state $summarystate. Also
 +     * return the total count of such states.
 +     *
 +     * Only a subset of the ids can be returned by using $orderby, $limitfrom and
 +     * $limitnum. A special value 'random' can be passed as $orderby, in which case
 +     * $limitfrom is ignored.
 +     *
 +     * @param int $slot The slot for the questions you want to konw about.
 +     * @param int $questionid (optional) Only return attempts that were of this specific question.
 +     * @param string $summarystate 'all', 'needsgrading', 'autograded' or 'manuallygraded'.
 +     * @param string $orderby 'random', 'date', 'student' or 'idnumber'.
 +     * @param int $page implements paging of the results.
 +     *      Ignored if $orderby = random or $pagesize is null.
 +     * @param int $pagesize implements paging of the results. null = all.
 +     */
 +    protected function get_usage_ids_where_question_in_state($summarystate, $slot,
 +            $questionid = null, $orderby = 'random', $page = 0, $pagesize = null) {
 +        global $CFG;
 +        $dm = new question_engine_data_mapper();
 +
 +        if ($pagesize && $orderby != 'random') {
 +            $limitfrom = $page * $pagesize;
 +        } else {
 +            $limitfrom = 0;
 +        }
 +
 +        $qubaids = $this->get_qubaids_condition();
  
 +        $params = array();
 +        if ($orderby == 'date') {
 +            list($statetest, $params) = $dm->in_summary_state_test(
 +                    'manuallygraded', false, 'mangrstate');
 +            $orderby = "(
 +                    SELECT MAX(sortqas.timecreated)
 +                    FROM {question_attempt_steps} sortqas
 +                    WHERE sortqas.questionattemptid = qa.id
 +                        AND sortqas.state $statetest
 +                    )";
 +        } else if ($orderby == 'student' || $orderby == 'idnumber') {
 +            $qubaids->from .= " JOIN {user} u ON quiza.userid = u.id ";
 +            if ($orderby == 'student') {
 +                $orderby = sql_fullname('u.firstname', 'u.lastname');
 +            }
 +        }
 +
 +        return $dm->load_questions_usages_where_question_in_state($qubaids, $summarystate,
 +                $slot, $questionid, $orderby, $params, $limitfrom, $pagesize);
 +    }
 +}
@@@ -272,64 -287,59 +272,64 @@@ class quiz_report_overview_table extend
              return get_string('done', 'quiz_overview');
          }
      }
 -    function query_db($pagesize, $useinitialsbar=true){
 -        // Add table joins so we can sort by question grade
 -        // unfortunately can't join all tables necessary to fetch all grades
 -        // to get the state for one question per attempt row we must join two tables
 -        // and there is a limit to how many joins you can have in one query. In MySQL it
 -        // is 61. This means that when having more than 29 questions the query will fail.
 -        // So we join just the tables needed to sort the attempts.
 -        if($sort = $this->get_sql_sort()) {
 -            if ($this->detailedmarks) {
 -                $this->sql->from .= ' ';
 -                $sortparts    = explode(',', $sort);
 -                $matches = array();
 -                foreach($sortparts as $sortpart) {
 -                    $sortpart = trim($sortpart);
 -                    if (preg_match('/^qsgrade([0-9]+)/', $sortpart, $matches)){
 -                        $qid = intval($matches[1]);
 -                        $this->sql->fields .=  ", qs$qid.grade AS qsgrade$qid, qs$qid.event AS qsevent$qid, qs$qid.id AS qsid$qid";
 -                        $this->sql->from .= "LEFT JOIN {question_sessions} qns$qid ON qns$qid.attemptid = qa.uniqueid AND qns$qid.questionid = :qid$qid ";
 -                        $this->sql->from .=  "LEFT JOIN  {question_states} qs$qid ON qs$qid.id = qns$qid.newgraded ";
 -                        $this->sql->params['qid'.$qid] = $qid;
 -                    }
 -                }
 -            } else {
 -                //unset any sort columns that sort on question grade as the
 -                //grades are not being fetched as fields
 -                $sess = &$this->sess;
 -                foreach($sess->sortby as $column => $order) {
 -                    if (preg_match('/^qsgrade([0-9]+)/', trim($column))){
 -                        unset($sess->sortby[$column]);
 -                    }
 -                }
 -            }
 +
 +    protected function requires_latest_steps_loaded() {
 +        return $this->detailedmarks;
 +    }
 +
 +    protected function is_latest_step_column($column) {
 +        if (preg_match('/^qsgrade([0-9]+)/', $column, $matches)) {
 +            return $matches[1];
          }
 +        return false;
 +    }
 +
 +    protected function get_required_latest_state_fields($slot, $alias) {
 +        return "$alias.fraction * $alias.maxmark AS qsgrade$slot";
 +    }
 +
 +    public function query_db($pagesize, $useinitialsbar = true) {
          parent::query_db($pagesize, $useinitialsbar);
 -        //get all the attempt ids we want to display on this page
 -        //or to export for download.
 -        if (!$this->is_downloading()) {
 -            $attemptids = array();
 -            foreach ($this->rawdata as $attempt){
 -                if ($attempt->attemptuniqueid > 0){
 -                    $attemptids[] = $attempt->attemptuniqueid;
 -                }
 -            }
 -            $this->gradedstatesbyattempt = quiz_get_newgraded_states($attemptids, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt');
 -            if (has_capability('mod/quiz:regrade', $this->context)){
 -                $this->regradedqs = quiz_get_regraded_qs($attemptids);
 -            }
 -        } else {
 -            $this->gradedstatesbyattempt = quiz_get_newgraded_states($this->sql, true, 'qs.id, qs.grade, qs.event, qs.question, qs.attempt');
 -            if (has_capability('mod/quiz:regrade', $this->context)){
 -                $this->regradedqs = quiz_get_regraded_qs($this->sql);
 -            }
 +
 +        if ($this->detailedmarks && has_capability('mod/quiz:regrade', $this->context)) {
 +            $this->regradedqs = $this->get_regraded_questions();
          }
      }
 -}
  
-         list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u0000');
 +    /**
 +     * Load the average grade for each question, averaged over particular users.
 +     * @param array $userids the user ids to average over.
 +     */
 +    protected function load_average_question_grades($userids) {
 +        global $DB;
 +
 +        $qmfilter = '';
 +        if ($this->quiz->attempts != 1) {
 +            $qmfilter = '(' . quiz_report_qm_filter_select($this->quiz, 'quiza') . ') AND ';
 +        }
 +
++        list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
 +        $params['quizid'] = $this->quiz->id;
 +        $qubaids = new qubaid_join(
 +                '{quiz_attempts} quiza',
 +                'quiza.uniqueid',
 +                "quiza.userid $usql AND quiza.quiz = :quizid",
 +                $params);
 +
 +        $dm = new question_engine_data_mapper();
 +        return $dm->load_average_marks($qubaids, array_keys($this->questions));
 +    }
 +
 +    /**
 +     * Get all the questions in all the attempts being displayed that need regrading.
 +     * @return array A two dimensional array $questionusageid => $slot => $regradeinfo.
 +     */
 +    protected function get_regraded_questions() {
 +        global $DB;
 +
 +        $qubaids = $this->get_qubaids_condition();
 +        $regradedqs = $DB->get_records_select('quiz_overview_regrades',
 +                'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params());
 +        return quiz_report_index_by_keys($regradedqs, array('questionusageid', 'slot'));
 +    }
 +}
@@@ -141,83 -214,69 +141,83 @@@ WHER
   * one attempt that will be graded for each user. Or return
   * empty string if all attempts contribute to final grade.
   */
 -function quiz_report_qm_filter_select($quiz){
 -    if ($quiz->attempts == 1) {//only one attempt allowed on this quiz
 +function quiz_report_qm_filter_select($quiz, $quizattemptsalias = 'quiza') {
 +    if ($quiz->attempts == 1) { // Only one attempt allowed on this quiz
          return '';
      }
 -    $useridsql = 'qa.userid';
 -    $quizidsql = 'qa.quiz';
 -    $qmfilterattempts = true;
 +
      switch ($quiz->grademethod) {
      case QUIZ_GRADEHIGHEST :
 -        $field1 = 'sumgrades';
 -        $field2 = 'timestart';
 -        $aggregator1 = 'MAX';
 -        $aggregator2 = 'MIN';
 -        $qmselectpossible = true;
 -        break;
 +        return "$quizattemptsalias.id = (
 +                SELECT MIN(qa2.id)
 +                FROM {quiz_attempts} qa2
 +                WHERE qa2.quiz = $quizattemptsalias.quiz AND
 +                    qa2.userid = $quizattemptsalias.userid AND
 +                    COALESCE(qa2.sumgrades, 0) = (
 +                        SELECT MAX(COALESCE(qa3.sumgrades, 0))
 +                        FROM {quiz_attempts} qa3
 +                        WHERE qa3.quiz = $quizattemptsalias.quiz AND
 +                            qa3.userid = $quizattemptsalias.userid
 +                    )
 +                )";
 +
      case QUIZ_GRADEAVERAGE :
 -        $qmselectpossible = false;
 -        break;
 +        return '';
 +
      case QUIZ_ATTEMPTFIRST :
 -        $field1 = 'timestart';
 -        $field2 = 'id';
 -        $aggregator1 = 'MIN';
 -        $aggregator2 = 'MIN';
 -        $qmselectpossible = true;
 -        break;
 +        return "$quizattemptsalias.id = (
 +                SELECT MIN(qa2.id)
 +                FROM {quiz_attempts} qa2
 +                WHERE qa2.quiz = $quizattemptsalias.quiz AND
 +                    qa2.userid = $quizattemptsalias.userid)";
 +
      case QUIZ_ATTEMPTLAST :
 -        $field1 = 'timestart';
 -        $field2 = 'id';
 -        $aggregator1 = 'MAX';
 -        $aggregator2 = 'MAX';
 -        $qmselectpossible = true;
 -        break;
 +        return "$quizattemptsalias.id = (
 +                SELECT MAX(qa2.id)
 +                FROM {quiz_attempts} qa2
 +                WHERE qa2.quiz = $quizattemptsalias.quiz AND
 +                    qa2.userid = $quizattemptsalias.userid)";
      }
 -    if ($qmselectpossible){
 -        $qmselect = "qa.$field1 = (SELECT $aggregator1(qa2.$field1) FROM {quiz_attempts} qa2 WHERE qa2.quiz = $quizidsql AND qa2.userid = $useridsql) AND " .
 -                    "qa.$field2 = (SELECT $aggregator2(qa3.$field2) FROM {quiz_attempts} qa3 WHERE qa3.quiz = $quizidsql AND qa3.userid = $useridsql AND qa3.$field1 = qa.$field1)";
 -    } else {
 -        $qmselect = '';
 -    }
 -
 -    return $qmselect;
  }
  
 -function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids=array()){
 -    global $CFG, $DB;
 -    if ($userids){
 -        list($usql, $params) = $DB->get_in_or_equal($userids);
 +/**
 + * Get the nuber of students whose score was in a particular band for this quiz.
 + * @param number $bandwidth the width of each band.
 + * @param int $bands the number of bands
 + * @param int $quizid the quiz id.
 + * @param array $userids list of user ids.
 + * @return array band number => number of users with scores in that band.
 + */
 +function quiz_report_grade_bands($bandwidth, $bands, $quizid, $userids = array()) {
 +    global $DB;
 +
 +    if ($userids) {
-         list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u000000');
++        list($usql, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'u');
 +        $usql = "qg.userid $usql AND";
      } else {
 -        $usql ='';
 +        $usql = '';
          $params = array();
      }
 -    $sql = "SELECT
 -        FLOOR(qg.grade/$bandwidth) AS band,
 -        COUNT(1) AS num
 -    FROM
 -        {quiz_grades} qg,  {quiz} q
 -    WHERE qg.quiz = q.id " .
 -            ($usql?"AND qg.userid $usql ":'') .
 -            "AND qg.quiz = ?
 -    GROUP BY FLOOR(qg.grade/$bandwidth)
 -    ORDER BY band";
 -    $params[] = $quizid;
 +    $sql = "
 +SELECT band, COUNT(1)
 +
 +FROM (
 +    SELECT FLOOR(qg.grade / :bandwidth) AS band
 +      FROM {quiz_grades} qg
 +     WHERE $usql qg.quiz = :quizid
 +) subquery
 +
 +GROUP BY
 +    band
 +
 +ORDER BY
 +    band";
 +
 +    $params['quizid'] = $quizid;
 +    $params['bandwidth'] = $bandwidth;
 +
      $data = $DB->get_records_sql_menu($sql, $params);
 +
      //need to create array elements with values 0 at indexes where there is no element
      $data =  $data + array_fill(0, $bands+1, 0);
      ksort($data);
        <FIELDS>
          <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" NEXT="quizstatisticsid"/>
          <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="id" NEXT="questionid"/>
 -        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="quizstatisticsid" NEXT="subquestion"/>
 -        <FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="questionid" NEXT="s"/>
 +        <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="quizstatisticsid" NEXT="slot"/>
 +        <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" COMMENT="The position in the quiz where this question appears" PREVIOUS="questionid" NEXT="subquestion"/>
 +        <FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="slot" NEXT="s"/>
          <FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="subquestion" NEXT="effectiveweight"/>
-         <FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" DECIMALS="5" PREVIOUS="s" NEXT="negcovar"/>
+         <FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="5" PREVIOUS="s" NEXT="negcovar"/>
          <FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" PREVIOUS="effectiveweight" NEXT="discriminationindex"/>
          <FIELD NAME="discriminationindex" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="5" PREVIOUS="negcovar" NEXT="discriminativeefficiency"/>
          <FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="5" PREVIOUS="discriminationindex" NEXT="sd"/>
-         <FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" DECIMALS="10" PREVIOUS="discriminativeefficiency" NEXT="facility"/>
-         <FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" DECIMALS="10" PREVIOUS="sd" NEXT="subquestions"/>
-         <FIELD NAME="subquestions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" PREVIOUS="facility" NEXT="maxmark"/>
-         <FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" DECIMALS="7" PREVIOUS="subquestions" NEXT="positions"/>
-         <FIELD NAME="positions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions." PREVIOUS="maxmark" NEXT="randomguessscore"/>
+         <FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="10" PREVIOUS="discriminativeefficiency" NEXT="facility"/>
+         <FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="10" PREVIOUS="sd" NEXT="subquestions"/>
+         <FIELD NAME="subquestions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" PREVIOUS="facility" NEXT="maxgrade"/>
+         <FIELD NAME="maxgrade" TYPE="number" LENGTH="12" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="7" PREVIOUS="subquestions" NEXT="positions"/>
 -        <FIELD NAME="positions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions." PREVIOUS="maxgrade"/>
++        <FIELD NAME="positions" TYPE="text" LENGTH="medium" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions." PREVIOUS="maxgrade" NEXT="randomguessscore"/>
 +        <FIELD NAME="randomguessscore" TYPE="number" LENGTH="12" NOTNULL="false" UNSIGNED="false" SEQUENCE="false" DECIMALS="7" COMMENT="An estimate of the score a student would get by guessing randomly." PREVIOUS="positions"/>
        </FIELDS>
        <KEYS>
          <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
          <FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="id" NEXT="questionid"/>
          <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="quizstatisticsid" NEXT="subqid"/>
          <FIELD NAME="subqid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="questionid" NEXT="aid"/>
 -        <FIELD NAME="aid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" PREVIOUS="subqid" NEXT="response"/>
 +        <FIELD NAME="aid" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" PREVIOUS="subqid" NEXT="response"/>
          <FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false" PREVIOUS="aid" NEXT="rcount"/>
          <FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" UNSIGNED="true" SEQUENCE="false" PREVIOUS="response" NEXT="credit"/>
-         <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" DECIMALS="5" PREVIOUS="rcount"/>
+         <FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" UNSIGNED="false" SEQUENCE="false" DECIMALS="5" PREVIOUS="rcount"/>
        </FIELDS>
        <KEYS>
          <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@@ -188,96 -157,77 +188,164 @@@ function xmldb_quiz_statistics_upgrade(
          $DB->set_field('quiz_report', 'capability', 'quizreport/statistics:view', array('name'=>'statistics'));
  
      /// statistics savepoint reached
 -        upgrade_plugin_savepoint(true, 2008112100, 'quizreport', 'statistics');
 +        upgrade_plugin_savepoint(true, 2008112100, 'quiz', 'statistics');
      }
  
+     if ($oldversion < 2008112101) {
+         // Removed UNSIGNED from all NUMBER columns in the quiz_statistics table.
+         $table = new xmldb_table('quiz_statistics');
+         // Change of sign for field firstattemptsavg
+         $field = new xmldb_field('firstattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'allattemptscount');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field allattemptsavg
+         $field = new xmldb_field('allattemptsavg', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'firstattemptsavg');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field median
+         $field = new xmldb_field('median', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'allattemptsavg');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field standarddeviation
+         $field = new xmldb_field('standarddeviation', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 'median');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field errorratio
+         $field = new xmldb_field('errorratio', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'cic');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field standarderror
+         $field = new xmldb_field('standarderror', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'errorratio');
+         $dbman->change_field_unsigned($table, $field);
+         // statistics savepoint reached
+         upgrade_plugin_savepoint(true, 2008112101, 'quiz', 'statistics');
+     }
+     if ($oldversion < 2008112102) {
+         // Removed UNSIGNED from all NUMBER columns in the quiz_question_statistics table.
+         $table = new xmldb_table('quiz_question_statistics');
+         // Change of sign for field effectiveweight
+         $field = new xmldb_field('effectiveweight', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null, 's');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field sd
+         $field = new xmldb_field('sd', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'discriminativeefficiency');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field facility
+         $field = new xmldb_field('facility', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null, 'sd');
+         $dbman->change_field_unsigned($table, $field);
+         // Change of sign for field maxgrade
+         $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'subquestions');
+         $dbman->change_field_unsigned($table, $field);
+         // statistics savepoint reached
+         upgrade_plugin_savepoint(true, 2008112102, 'quiz', 'statistics');
+     }
+     if ($oldversion < 2008112103) {
+         // Removed UNSIGNED from all NUMBER columns in the quiz_question_response_stats table.
+         $table = new xmldb_table('quiz_question_response_stats');
+         // Change of sign for field credit
+         $field = new xmldb_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null, XMLDB_NOTNULL, null, null, 'rcount');
+         $dbman->change_field_unsigned($table, $field);
+         // statistics savepoint reached
+         upgrade_plugin_savepoint(true, 2008112103, 'quiz', 'statistics');
+     }
 +    if ($oldversion < 2010031700) {
 +
 +        // Define field randomguessscore to be added to quiz_question_statistics
 +        $table = new xmldb_table('quiz_question_statistics');
 +        $field = new xmldb_field('randomguessscore', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'positions');
 +
 +        // Conditionally launch add field randomguessscore
 +        if (!$dbman->field_exists($table, $field)) {
 +            $dbman->add_field($table, $field);
 +        }
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2010031700, 'quiz', 'statistics');
 +    }
 +
 +    if ($oldversion < 2010032400) {
 +
 +        // Define field slot to be added to quiz_question_statistics
 +        $table = new xmldb_table('quiz_question_statistics');
 +        $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'questionid');
 +
 +        // Conditionally launch add field slot
 +        if (!$dbman->field_exists($table, $field)) {
 +            $dbman->add_field($table, $field);
 +        }
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2010032400, 'quiz', 'statistics');
 +    }
 +
 +    if ($oldversion < 2010032401) {
 +
 +    /// Delete all cached data
 +        $DB->delete_records('quiz_question_response_stats');
 +        $DB->delete_records('quiz_question_statistics');
 +        $DB->delete_records('quiz_statistics');
 +
 +        // Rename field maxgrade on table quiz_question_statistics to maxmark
 +        $table = new xmldb_table('quiz_question_statistics');
 +        $field = new xmldb_field('maxgrade', XMLDB_TYPE_NUMBER, '12, 7', XMLDB_UNSIGNED, null, null, null, 'subquestions');
 +
 +        // Launch rename field maxmark
 +        $dbman->rename_field($table, $field, 'maxmark');
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2010032401, 'quiz', 'statistics');
 +    }
 +
 +    if ($oldversion < 2010062200) {
 +
 +        // Changing nullability of field aid on table quiz_question_response_stats to null
 +        $table = new xmldb_table('quiz_question_response_stats');
 +        $field = new xmldb_field('aid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, null, null, null, 'subqid');
 +
 +        // Launch change of nullability for field aid
 +        $dbman->change_field_notnull($table, $field);
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2010062200, 'quiz', 'statistics');
 +    }
 +
 +    if ($oldversion < 2010070301) {
 +
 +        // Changing type of field maxmark on table quiz_question_statistics to number
 +        $table = new xmldb_table('quiz_question_statistics');
 +        $field = new xmldb_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', XMLDB_UNSIGNED, null, null, null, 'subquestions');
 +
 +        // Launch change of type for field maxmark
 +        $dbman->change_field_type($table, $field);
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2010070301, 'quiz', 'statistics');
 +    }
 +
 +    if ($oldversion < 2011021500) {
 +        if ($dbman->table_exists('quiz_reports')) {
 +            $DB->set_field('quiz_reports', 'capability', 'quiz/statistics:view',
 +                    array('name' => 'statistics'));
 +        } else {
 +            $DB->set_field('quiz_report', 'capability', 'quiz/statistics:view',
 +                    array('name' => 'statistics'));
 +        }
 +
 +        // statistics savepoint reached
 +        upgrade_plugin_savepoint(true, 2011021500, 'quiz', 'statistics');
 +    }
 +
      return true;
  }
  
  <?php
 -class qstats{
 -    /**
 -     * @var mixed states from which to calculate stats - iteratable.
 -     */
 -    var $states;
  
 -    var $sumofgradevariance = 0;
 -    var $questions;
 -    var $subquestions = array();
 -    var $randomselectors = array();
 -    var $responses = array();
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  
 -    function qstats($questions, $s, $sumgradesavg){
 -        $this->s = $s;
 -        $this->sumgradesavg = $sumgradesavg;
 +/**
 + * Quiz statistics report calculations class.
 + *
 + * @package    quiz
 + * @subpackage statistics
 + * @copyright  2008 Jamie Pratt
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
  
 -        foreach (array_keys($questions) as $qid){
 -            $questions[$qid]->_stats = $this->stats_init_object();
 -        }
 -        $this->questions = $questions;
 -    }
 -    function stats_init_object(){
 -        $statsinit = new stdClass();
 -        $statsinit->s = 0;
 -        $statsinit->totalgrades = 0;
 -        $statsinit->totalothergrades = 0;
 -        $statsinit->gradevariancesum = 0;
 -        $statsinit->othergradevariancesum = 0;
 -        $statsinit->covariancesum = 0;
 -        $statsinit->covariancemaxsum = 0;
 -        $statsinit->subquestion = false;
 -        $statsinit->subquestions = '';
 -        $statsinit->covariancewithoverallgradesum = 0;
 -        $statsinit->gradearray = array();
 -        $statsinit->othergradesarray = array();
 -        return $statsinit;
 -    }
 -    function get_records($quizid, $currentgroup, $groupstudents, $allattempts){
 -        global $DB;
 -        list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions), SQL_PARAMS_NAMED, 'q');
 -        list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts);
 -        $sql = 'SELECT qs.id, ' .
 -            'qs.question, ' .
 -            'qa.sumgrades, ' .
 -            'qs.grade, ' .
 -            'qs.answer ' .
 -            'FROM ' .
 -            '{question_sessions} qns, ' .
 -            '{question_states} qs, '.
 -            $fromqa.' '.
 -            'WHERE ' .$whereqa.
 -            'AND qs.question '.$qsql.' '.
 -            'AND qns.attemptid = qa.uniqueid '.
 -            'AND qns.newgraded = qs.id';
 -        $this->states = $DB->get_records_sql($sql, $qaparams + $qparams);
 -        if ($this->states === false){
 -            print_error('errorstatisticsquestions', 'quiz_statistics');
 -        }
 -    }
  
 -    function _initial_states_walker($state, &$stats, $positionstat = true){
 -        $stats->s++;
 -        $stats->totalgrades += $state->grade;
 -        if ($positionstat){
 -            $stats->totalothergrades += $state->sumgrades - $state->grade;
 -        } else {
 -            $stats->totalothergrades += $state->sumgrades;
 -        }
 -        //need to sort these to calculate max covariance :
 -        $stats->gradearray[] = $state->grade;
 -        if ($positionstat){
 -            $stats->othergradesarray[] = $state->sumgrades - $state->grade;
 -        } else {
 -            $stats->othergradesarray[] = $state->sumgrades;
 -        }
 +defined('MOODLE_INTERNAL') || die();
  
 -    }
  
 -    function _secondary_states_walker($state, &$stats){
 -        $gradedifference = ($state->grade - $stats->gradeaverage);
 -        if ($stats->subquestion){
 -            $othergradedifference = $state->sumgrades - $stats->othergradeaverage;
 -        } else {
 -            $othergradedifference = (($state->sumgrades - $state->grade) - $stats->othergradeaverage);
 -        }
 -        $overallgradedifference = $state->sumgrades - $this->sumgradesavg;
 -        $sortedgradedifference = (array_shift($stats->gradearray) - $stats->gradeaverage);
 -        $sortedothergradedifference = (array_shift($stats->othergradesarray) - $stats->othergradeaverage);
 -        $stats->gradevariancesum += pow($gradedifference,2);
 -        $stats->othergradevariancesum += pow($othergradedifference,2);
 -        $stats->covariancesum += $gradedifference * $othergradedifference;
 -        $stats->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference;
 -        $stats->covariancewithoverallgradesum += $gradedifference * $overallgradedifference;
 +/**
 + * This class has methods to compute the question statistics from the raw data.
 + *
 + * @copyright  2008 Jamie Pratt
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +class quiz_statistics_question_stats {
 +    public $questions;
 +    public $subquestions = array();
  
 -    }
 +    protected $s;
 +    protected $summarksavg;
 +    protected $allattempts;
  
 -    function add_response_detail_to_array($responsedetail){
 -        $responsedetail->rcount = 1;
 -        if (isset($this->responses[$responsedetail->subqid])){
 -            if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid])){
 -                if (isset($this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response])){
 -                    $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response]->rcount++;
 -                } else {
 -                    $this->responses[$responsedetail->subqid][$responsedetail->aid][$responsedetail->response] = $responsedetail;
 -                }
 -            } else {
 -                $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
 -            }
 -        } else {
 -            $this->responses[$responsedetail->subqid] = array();
 -            $this->responses[$responsedetail->subqid][$responsedetail->aid] = array($responsedetail->response => $responsedetail);
 -        }
 -    }
 +    /** @var mixed states from which to calculate stats - iteratable. */
 +    protected $lateststeps;
 +
 +    protected $sumofmarkvariance = 0;
 +    protected $randomselectors = array();
  
      /**
 -     * Get the data for the individual question response analysis table.
 +     * Constructor.
 +     * @param $questions the questions.
 +     * @param $s the number of attempts included in the stats.
 +     * @param $summarksavg the average attempt summarks.
       */
 -    function _process_actual_responses($question, $state){
 -        global $QTYPES;
 -        if ($question->qtype != 'random' &&
 -                $QTYPES[$question->qtype]->show_analysis_of_responses()){
 -            $restoredstate = clone($state);
 -            restore_question_state($question, $restoredstate);
 -            $responsedetails = $QTYPES[$question->qtype]->get_actual_response_details($question, $restoredstate);
 -            foreach ($responsedetails as $responsedetail){
 -                $responsedetail->questionid = $question->id;
 -                $this->add_response_detail_to_array($responsedetail);
 -            }
 +    public function __construct($questions, $s, $summarksavg) {
 +        $this->s = $s;
 +        $this->summarksavg = $summarksavg;
 +
 +        foreach ($questions as $slot => $question) {
 +            $question->_stats = $this->make_blank_question_stats();
 +            $question->_stats->questionid = $question->id;
 +            $question->_stats->slot = $slot;
          }
 +
 +        $this->questions = $questions;
      }
  
 -    function _initial_question_walker(&$stats){
 -        $stats->gradeaverage = $stats->totalgrades / $stats->s;
 -        if ($stats->maxgrade!=0){
 -            $stats->facility = $stats->gradeaverage / $stats->maxgrade;
 -        } else {
 -            $stats->facility = null;
 -        }
 -        $stats->othergradeaverage = $stats->totalothergrades / $stats->s;
 -        sort($stats->gradearray, SORT_NUMERIC);
 -        sort($stats->othergradesarray, SORT_NUMERIC);
 +    /**
 +     * @return object ready to hold all the question statistics.
 +     */
 +    protected function make_blank_question_stats() {
 +        $stats = new stdClass();
 +        $stats->slot = null;
 +        $stats->s = 0;
 +        $stats->totalmarks = 0;
 +        $stats->totalothermarks = 0;
 +        $stats->markvariancesum = 0;
 +        $stats->othermarkvariancesum = 0;
 +        $stats->covariancesum = 0;
 +        $stats->covariancemaxsum = 0;
 +        $stats->subquestion = false;
 +        $stats->subquestions = '';
 +        $stats->covariancewithoverallmarksum = 0;
 +        $stats->randomguessscore = null;
 +        $stats->markarray = array();
 +        $stats->othermarksarray = array();
 +        return $stats;
      }
 -    function _secondary_question_walker(&$stats){
 -        if ($stats->s > 1){
 -            $stats->gradevariance = $stats->gradevariancesum / ($stats->s -1);
 -            $stats->othergradevariance = $stats->othergradevariancesum / ($stats->s -1);
 -            $stats->covariance = $stats->covariancesum / ($stats->s -1);
 -            $stats->covariancemax = $stats->covariancemaxsum / ($stats->s -1);
 -            $stats->covariancewithoverallgrade = $stats->covariancewithoverallgradesum / ($stats->s-1);
 -            $stats->sd = sqrt($stats->gradevariancesum / ($stats->s -1));
 -        } else {
 -            $stats->gradevariance = null;
 -            $stats->othergradevariance = null;
 -            $stats->covariance = null;
 -            $stats->covariancemax = null;
 -            $stats->covariancewithoverallgrade = null;
 -            $stats->sd = null;
 -        }
 -        //avoid divide by zero
 -        if ($stats->gradevariance * $stats->othergradevariance){
 -            $stats->discriminationindex = 100*$stats->covariance
 -                        / sqrt($stats->gradevariance * $stats->othergradevariance);
 -        } else {
 -            $stats->discriminationindex = null;
 -        }
 -        if ($stats->covariancemax){
 -            $stats->discriminativeefficiency = 100*$stats->covariance / $stats->covariancemax;
 -        } else {
 -            $stats->discriminativeefficiency = null;
 -        }
 +
 +    /**
 +     * Load the data that will be needed to perform the calculations.
 +     *
 +     * @param int $quizid the quiz id.
 +     * @param int $currentgroup the current group. 0 for none.
 +     * @param array $groupstudents students in this group.
 +     * @param bool $allattempts use all attempts, or just first attempts.
 +     */
 +    public function load_step_data($quizid, $currentgroup, $groupstudents, $allattempts) {
 +        global $DB;
 +
 +        $this->allattempts = $allattempts;
 +
-         list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions), SQL_PARAMS_NAMED, 'q0000');
++        list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions),
++                SQL_PARAMS_NAMED, 'q');
 +        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
 +                $quizid, $currentgroup, $groupstudents, $allattempts, false);
 +
 +        $this->lateststeps = $DB->get_records_sql("
 +                SELECT
 +                    qas.id,
 +                    quiza.sumgrades,
 +                    qa.questionid,
 +                    qa.slot,
 +                    qa.maxmark,
 +                    qas.fraction * qa.maxmark as mark
 +
 +                FROM $fromqa
 +                JOIN {question_attempts} qa ON qa.questionusageid = quiza.uniqueid
 +                JOIN (
 +                    SELECT questionattemptid, MAX(id) AS latestid FROM {question_attempt_steps} GROUP BY questionattemptid
 +                ) lateststepid ON lateststepid.questionattemptid = qa.id
 +                JOIN {question_attempt_steps} qas ON qas.id = lateststepid.latestid
 +
 +                WHERE
 +                    qa.slot $qsql AND
 +                    $whereqa", $qparams + $qaparams);
      }
  
 -    function process_states(){
 -        global $DB, $OUTPUT;
 +    public function compute_statistics() {
          set_time_limit(0);
 +
          $subquestionstats = array();
 -        foreach ($this->states as $state){
 -            $this->_initial_states_walker($state, $this->questions[$state->question]->_stats);
 -            //if this is a random question what is the real item being used?
 -            if ($this->questions[$state->question]->qtype == 'random'){
 -                if ($realstate = question_get_real_state($state)){
 -                    if (!isset($subquestionstats[$realstate->question])){
 -                        $subquestionstats[$realstate->question] = $this->stats_init_object();
 -                        $subquestionstats[$realstate->question]->usedin = array();
 -                        $subquestionstats[$realstate->question]->subquestion = true;
 -                        $subquestionstats[$realstate->question]->differentweights = false;
 -                        $subquestionstats[$realstate->question]->maxgrade = $this->questions[$state->question]->maxgrade;
 -                    } else if ($subquestionstats[$realstate->question]->maxgrade != $this->questions[$state->question]->maxgrade){
 -                        $subquestionstats[$realstate->question]->differentweights = true;
 -                    }
 -                    $this->_initial_states_walker($realstate, $subquestionstats[$realstate->question], false);
 -                    $number = $this->questions[$state->question]->number;
 -                    $subquestionstats[$realstate->question]->usedin[$number] = $number;
 -                    $randomselectorstring = $this->questions[$state->question]->category.'/'.$this->questions[$state->question]->questiontext;
 -                    if (!isset($this->randomselectors[$randomselectorstring])){
 -                        $this->randomselectors[$randomselectorstring] = array();
 -                    }
 -                    $this->randomselectors[$randomselectorstring][$realstate->question] = $realstate->question;
 +
 +        // Compute the statistics of position, and for random questions, work
 +        // out which questions appear in which positions.
 +        foreach ($this->lateststeps as $step) {
 +            $this->initial_steps_walker($step, $this->questions[$step->slot]->_stats);
 +
 +            // If this is a random question what is the real item being used?
 +            if ($step->questionid != $this->questions[$step->slot]->id) {
 +                if (!isset($subquestionstats[$step->questionid])) {
 +                    $subquestionstats[$step->questionid] = $this->make_blank_question_stats();
 +                    $subquestionstats[$step->questionid]->questionid = $step->questionid;
 +                    $subquestionstats[$step->questionid]->allattempts = $this->allattempts;
 +                    $subquestionstats[$step->questionid]->usedin = array();
 +                    $subquestionstats[$step->questionid]->subquestion = true;
 +                    $subquestionstats[$step->questionid]->differentweights = false;
 +                    $subquestionstats[$step->questionid]->maxmark = $step->maxmark;
 +                } else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
 +                    $subquestionstats[$step->questionid]->differentweights = true;
 +                }
 +
 +                $this->initial_steps_walker($step,
 +                        $subquestionstats[$step->questionid], false);
 +
 +                $number = $this->questions[$step->slot]->number;
 +                $subquestionstats[$step->questionid]->usedin[$number] = $number;
 +
 +                $randomselectorstring = $this->questions[$step->slot]->category .
 +                        '/' . $this->questions[$step->slot]->questiontext;
 +                if (!isset($this->randomselectors[$randomselectorstring])) {
 +                    $this->randomselectors[$randomselectorstring] = array();
                  }
 +                $this->randomselectors[$randomselectorstring][$step->questionid] =
 +                        $step->questionid;
              }
          }
 -        foreach ($this->randomselectors as $key => $randomselector){
 +
 +        foreach ($this->randomselectors as $key => $notused) {
              ksort($this->randomselectors[$key]);
          }
 +
 +        // Compute the statistics of question id, if we need any.
          $this->subquestions = question_load_questions(array_keys($subquestionstats));
 -        foreach (array_keys($this->subquestions) as $qid){
 -            $this->subquestions[$qid]->_stats = $subquestionstats[$qid];
 -            $this->subquestions[$qid]->_stats->questionid = $qid;
 -            $this->subquestions[$qid]->maxgrade = $this->subquestions[$qid]->_stats->maxgrade;
 -            $this->_initial_question_walker($this->subquestions[$qid]->_stats);
 -            if ($subquestionstats[$qid]->differentweights){
 -                echo $OUTPUT->notification(get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $this->subquestions[$qid]->name));
 +        foreach ($this->subquestions as $qid => $subquestion) {
 +            $subquestion->_stats = $subquestionstats[$qid];
 +            $subquestion->maxmark = $subquestion->_stats->maxmark;
 +            $subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
 +
 +            $this->initial_question_walker($subquestion->_stats);
 +
 +            if ($subquestionstats[$qid]->differentweights) {
 +                // TODO output here really sucks, but throwing is too severe.
 +                global $OUTPUT;
 +                echo $OUTPUT->notification(get_string('erroritemappearsmorethanoncewithdifferentweight',
 +                        'quiz_statistics', $this->subquestions[$qid]->name));
              }
 -            if ($this->subquestions[$qid]->_stats->usedin){
 -                sort($this->subquestions[$qid]->_stats->usedin, SORT_NUMERIC);
 -                $this->subquestions[$qid]->_stats->positions = join($this->subquestions[$qid]->_stats->usedin, ',');
 +
 +            if ($subquestion->_stats->usedin) {
 +                sort($subquestion->_stats->usedin, SORT_NUMERIC);
 +                $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
              } else {
 -                $this->subquestions[$qid]->_stats->positions = '';
 +                $subquestion->_stats->positions = '';
              }
          }
 +
 +        // Finish computing the averages, and put the subquestion data into the
 +        // corresponding questions.
 +
 +        // This cannot be a foreach loop because we need to have both
 +        // $question and $nextquestion available, but apart from that it is
 +        // foreach ($this->questions as $qid => $question) {
          reset($this->questions);
 -        do{
 -            list($qid, $question) = each($this->questions);
 +        while (list($slot, $question) = each($this->questions)) {
              $nextquestion = current($this->questions);
 -            $this->questions[$qid]->_stats->questionid = $qid;
 -            $this->questions[$qid]->_stats->positions = $this->questions[$qid]->number;
 -            $this->questions[$qid]->_stats->maxgrade = $question->maxgrade;
 -            $this->_initial_question_walker($this->questions[$qid]->_stats);
 -            if ($question->qtype == 'random'){
 +            $question->_stats->allattempts = $this->allattempts;
 +            $question->_stats->positions = $question->number;
 +            $question->_stats->maxmark = $question->maxmark;
 +            $question->_stats->randomguessscore = $this->get_random_guess_score($question);
 +
 +            $this->initial_question_walker($question->_stats);
 +
 +            if ($question->qtype == 'random') {
                  $randomselectorstring = $question->category.'/'.$question->questiontext;
 -                if ($nextquestion){
 +                if ($nextquestion && $nextquestion->qtype == 'random') {
                      $nextrandomselectorstring = $nextquestion->category.'/'.$nextquestion->questiontext;
 -                    if ($nextquestion->qtype == 'random' && $randomselectorstring == $nextrandomselectorstring){
 -                        continue;//next loop iteration
 +                    if ($randomselectorstring == $nextrandomselectorstring) {
 +                        continue; // Next loop iteration
                      }
                  }
 -                if (isset($this->randomselectors[$randomselectorstring])){
 -                    $question->_stats->subquestions = join($this->randomselectors[$randomselectorstring], ',');
 +                if (isset($this->randomselectors[$randomselectorstring])) {
 +                    $question->_stats->subquestions = implode(',', $this->randomselectors[$randomselectorstring]);
                  }
              }
 -        } while ($nextquestion);
 -        //go through the records one more time
 -        foreach ($this->states as $state){
 -            $this->_secondary_states_walker($state, $this->questions[$state->question]->_stats);
 -            if ($this->questions[$state->question]->qtype == 'random'){
 -                if ($realstate = question_get_real_state($state)){
 -                    $this->_secondary_states_walker($realstate, $this->subquestions[$realstate->question]->_stats);
 -                }
 +        }
 +
 +        // Go through the records one more time
 +        foreach ($this->lateststeps as $step) {
 +            $this->secondary_steps_walker($step,
 +                    $this->questions[$step->slot]->_stats);
 +
 +            if ($this->questions[$step->slot]->qtype == 'random') {
 +                $this->secondary_steps_walker($step,
 +                        $this->subquestions[$step->questionid]->_stats);
              }
          }
 -        $sumofcovariancewithoverallgrade = 0;
 -        foreach (array_keys($this->questions) as $qid){
 -            $this->_secondary_question_walker($this->questions[$qid]->_stats);
 -            $this->sumofgradevariance += $this->questions[$qid]->_stats->gradevariance;
 -            if ($this->questions[$qid]->_stats->covariancewithoverallgrade >= 0){
 -                $sumofcovariancewithoverallgrade += sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade);
 -                $this->questions[$qid]->_stats->negcovar = 0;
 +
 +        $sumofcovariancewithoverallmark = 0;
 +        foreach ($this->questions as $slot => $question) {
 +            $this->secondary_question_walker($question->_stats);
 +
 +            $this->sumofmarkvariance += $question->_stats->markvariance;
 +
 +            if ($question->_stats->covariancewithoverallmark >= 0) {
 +                $sumofcovariancewithoverallmark +=
 +                        sqrt($question->_stats->covariancewithoverallmark);
 +                $question->_stats->negcovar = 0;
              } else {
 -                $this->questions[$qid]->_stats->negcovar = 1;
 +                $question->_stats->negcovar = 1;
              }
          }
 -        foreach (array_keys($this->subquestions) as $qid){
 -            $this->_secondary_question_walker($this->subquestions[$qid]->_stats);
 +
 +        foreach ($this->subquestions as $subquestion) {
 +            $this->secondary_question_walker($subquestion->_stats);
          }
 -        foreach (array_keys($this->questions) as $qid){
 -            if ($sumofcovariancewithoverallgrade){
 -                if ($this->questions[$qid]->_stats->negcovar){
 -                    $this->questions[$qid]->_stats->effectiveweight = null;
 +
 +        foreach ($this->questions as $question) {
 +            if ($sumofcovariancewithoverallmark) {
 +                if ($question->_stats->negcovar) {
 +                    $question->_stats->effectiveweight = null;
                  } else {
 -                    $this->questions[$qid]->_stats->effectiveweight = 100 * sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade)
 -                                /   $sumofcovariancewithoverallgrade;
 +                    $question->_stats->effectiveweight = 100 *
 +                            sqrt($question->_stats->covariancewithoverallmark) /
 +                            $sumofcovariancewithoverallmark;
                  }
              } else {
 -                $this->questions[$qid]->_stats->effectiveweight = null;
 +                $question->_stats->effectiveweight = null;
              }
          }
      }
@@@ -246,792 -173,499 +246,793 @@@ class quiz_statistics_report extends qu
          return true;
      }
  
 -    function sort_response_details($detail1, $detail2){
 -        if ($detail1->credit == $detail2->credit){
 -            return strcmp($detail1->answer, $detail2->answer);
 -        }
 -        return ($detail1->credit > $detail2->credit) ? -1 : 1;
 -    }
 -    function sort_answers($answer1, $answer2){
 -        if ($answer1->rcount == $answer2->rcount){
 -            return strcmp($answer1->response, $answer2->response);
 -        } else {
 -            return ($answer1->rcount > $answer2->rcount)? -1 : 1;
 +    /**
 +     * Display the statistical and introductory information about a question.
 +     * Only called when not downloading.
 +     * @param object $quiz the quiz settings.
 +     * @param object $question the question to report on.
 +     * @param moodle_url $reporturl the URL to resisplay this report.
 +     * @param object $quizstats Holds the quiz statistics.
 +     */
 +    protected function output_individual_question_data($quiz, $question) {
 +        global $OUTPUT;
 +
 +        // On-screen display. Show a summary of the question's place in the quiz,
 +        // and the question statistics.
 +        $datumfromtable = $this->table->format_row($question);
 +
 +        // Set up the question info table.
 +        $questioninfotable = new html_table();
 +        $questioninfotable->align = array('center', 'center');
 +        $questioninfotable->width = '60%';
 +        $questioninfotable->attributes['class'] = 'generaltable titlesleft';
 +
 +        $questioninfotable->data = array();
 +        $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
 +        $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
 +                $question->name.'&nbsp;'.$datumfromtable['actions']);
 +        $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
 +                $datumfromtable['icon'] . '&nbsp;' .
 +                question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
 +                $datumfromtable['icon']);
 +        $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
 +                $question->_stats->positions);
 +
 +        // Set up the question statistics table.
 +        $questionstatstable = new html_table();
 +        $questionstatstable->align = array('center', 'center');
 +        $questionstatstable->width = '60%';
 +        $questionstatstable->attributes['class'] = 'generaltable titlesleft';
 +
 +        unset($datumfromtable['number']);
 +        unset($datumfromtable['icon']);
 +        $actions = $datumfromtable['actions'];
 +        unset($datumfromtable['actions']);
 +        unset($datumfromtable['name']);
 +        $labels = array('s' => get_string('attempts', 'quiz_statistics'),
 +                        'facility' => get_string('facility', 'quiz_statistics'),
 +                        'sd' => get_string('standarddeviationq', 'quiz_statistics'),
 +                        'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
 +                        'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
 +                        'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
 +                        'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
 +                        'discriminative_efficiency' => get_string('discriminative_efficiency', 'quiz_statistics'));
 +        foreach ($datumfromtable as $item => $value) {
 +            $questionstatstable->data[] = array($labels[$item], $value);
          }
 +
 +        // Display the various bits.
 +        echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
 +        echo html_writer::table($questioninfotable);
 +
 +        echo $OUTPUT->box(format_text($question->questiontext, $question->questiontextformat, array('overflowdiv'=>true)).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
 +
 +        echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
 +        echo html_writer::table($questionstatstable);
      }
  
 -    function output_individual_question_data($quiz, $question, $reporturl, $quizstats){
 -        global $CFG, $DB, $QTYPES, $OUTPUT;
 -        require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_question_table.php');
 -        $this->qtable = new quiz_report_statistics_question_table($question->id);
 -        $downloadtype = $this->table->is_downloading();
 -        if (!$this->table->is_downloading()){
 -            $datumfromtable = $this->table->format_row($question);
 -
 -            $questioninfotable = new html_table();
 -            $questioninfotable->align = array('center', 'center');
 -            $questioninfotable->width = '60%';
 -            $questioninfotable->attributes['class'] = 'generaltable titlesleft';
 -
 -            $questioninfotable->data = array();
 -            $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
 -            $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'), $question->name.'&nbsp;'.$datumfromtable['actions']);
 -            $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].'&nbsp;'.get_string($question->qtype,'quiz').'&nbsp;'.$datumfromtable['icon']);
 -            $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'), $question->_stats->positions);
 -
 -            $questionstatstable = new html_table();
 -            $questionstatstable->align = array('center', 'center');
 -            $questionstatstable->width = '60%';
 -            $questionstatstable->attributes['class'] = 'generaltable titlesleft';
 -
 -            unset($datumfromtable['number']);
 -            unset($datumfromtable['icon']);
 -            $actions = $datumfromtable['actions'];
 -            unset($datumfromtable['actions']);
 -            unset($datumfromtable['name']);
 -            $labels = array('s' => get_string('attempts', 'quiz_statistics'),
 -                            'facility' => get_string('facility', 'quiz_statistics'),
 -                            'sd' => get_string('standarddeviationq', 'quiz_statistics'),
 -                            'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
 -                            'intended_weight'=> get_string('intended_weight', 'quiz_statistics'),
 -                            'effective_weight'=> get_string('effective_weight', 'quiz_statistics'),
 -                            'discrimination_index'=> get_string('discrimination_index', 'quiz_statistics'),
 -                            'discriminative_efficiency'=> get_string('discriminative_efficiency', 'quiz_statistics'));
 -            foreach ($datumfromtable as $item => $value){
 -                $questionstatstable->data[] = array($labels[$item], $value);
 -            }
 -            echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
 -            echo html_writer::table($questioninfotable);
 +    /**
 +     * Display the response analysis for a question.
 +     * @param object $question the question to report on.
 +     * @param moodle_url $reporturl the URL to resisplay this report.
 +     * @param object $quizstats Holds the quiz statistics.
 +     */
 +    protected function output_individual_question_response_analysis($question, $reporturl, $quizstats) {
 +        global $OUTPUT;
  
 -            echo $OUTPUT->box(format_text($question->questiontext, $question->questiontextformat, array('overflowdiv'=>true)).$actions, 'boxaligncenter generalbox boxwidthnormal mdl-align');
 +        if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
 +            return;
 +        }
  
 -            echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
 -            echo html_writer::table($questionstatstable);
 +        $qtable = new quiz_report_statistics_question_table($question->id);
 +        $exportclass = $this->table->export_class_instance();
 +        $qtable->export_class_instance($exportclass);
 +        if (!$this->table->is_downloading()) {
 +            // Output an appropriate title.
 +            echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
  
          } else {
 -            $this->qtable->export_class_instance($this->table->export_class_instance());
 -            $questiontabletitle = !empty($question->number)?'('.$question->number.') ':'';
 -            $questiontabletitle .= "\"{$question->name}\"";
 -            $questiontabletitle = "<em>$questiontabletitle</em>";
 -            if ($downloadtype == 'xhtml'){
 +            // Work out an appropriate title.
 +            $questiontabletitle = '"' . $question->name . '"';
 +            if (!empty($question->number)) {
 +                $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
 +            }
 +            if ($this->table->is_downloading() == 'xhtml') {
                  $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
              }
 -            $exportclass =& $this->table->export_class_instance();
 +
 +            // Set up the table.
              $exportclass->start_table($questiontabletitle);
          }
 -        if ($QTYPES[$question->qtype]->show_analysis_of_responses()){
 -            if (!$this->table->is_downloading()){
 -                echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
 -            }
 -            $teacherresponses = $QTYPES[$question->qtype]->get_possible_responses($question);
 -            $this->qtable->setup($reporturl, $question, count($teacherresponses)>1);
 -            if ($this->table->is_downloading()){
 -                $exportclass->output_headers($this->qtable->headers);
 -            }
  
 -            $responses = $DB->get_records('quiz_question_response_stats', array('quizstatisticsid' => $quizstats->id, 'questionid' => $question->id), 'credit DESC, subqid ASC, aid ASC, rcount DESC');
 -            $responses = quiz_report_index_by_keys($responses, array('subqid', 'aid'), false);
 -            foreach ($responses as $subqid => $response){
 -                foreach (array_keys($responses[$subqid]) as $aid){
 -                    uasort($responses[$subqid][$aid], array('quiz_statistics_report', 'sort_answers'));
 -                }
 -                if (isset($responses[$subqid]['0'])){
 -                    $wildcardresponse = new stdClass();
 -                    $wildcardresponse->answer = '*';
 -                    $wildcardresponse->credit = 0;
 -                    $teacherresponses[$subqid][0] = $wildcardresponse;
 -                }
 -            }
 -            $first = true;
 -            $subq = 0;
 -            foreach ($teacherresponses as $subqid => $tresponsesforsubq){
 -                $subq++;
 -                $qhaswildcards = $QTYPES[$question->qtype]->has_wildcards_in_responses($question, $subqid);
 -                if (!$first){
 -                    $this->qtable->add_separator();
 -                }
 -                uasort($tresponsesforsubq, array('quiz_statistics_report', 'sort_response_details'));
 -                foreach ($tresponsesforsubq as $aid => $teacherresponse){
 -                    $teacherresponserow = new stdClass();
 -                    $teacherresponserow->response = $teacherresponse->answer;
 -                    $teacherresponserow->indent = '';
 -                    $teacherresponserow->rcount = 0;
 -                    $teacherresponserow->subq = $subq;
 -                    $teacherresponserow->credit = $teacherresponse->credit;
 -                    if (isset($responses[$subqid][$aid])){
 -                        $singleanswer = count($responses[$subqid][$aid])==1 &&
 -                                        ($responses[$subqid][$aid][0]->response == $teacherresponserow->response);
 -                        if (!$singleanswer && $qhaswildcards){
 -                            $this->qtable->add_separator();
 -                        }
 -                        foreach ($responses[$subqid][$aid] as $response){
 -                            $teacherresponserow->rcount += $response->rcount;
 -                        }
 -                        if ($aid!=0 || $qhaswildcards){
 -                            $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
 -                        }
 -                        if (!$singleanswer){
 -                            foreach ($responses[$subqid][$aid] as $response){
 -                                if (!$downloadtype || $downloadtype=='xhtml'){
 -                                    $indent = '&nbsp;&nbsp;&nbsp;&nbsp;';
 -                                } else {
 -                                    $indent = '    ';
 -                                }
 -                                $response->response = $response->response;
 -                                $response->indent = $qhaswildcards ? $indent : '';
 -                                $response->subq = $subq;
 -                                if ((count($responses[$subqid][$aid])<2) || ($response->rcount > ($teacherresponserow->rcount / 10))){
 -                                    $this->qtable->add_data_keyed($this->qtable->format_row($response));
 -                                }
 -                            }
 -                        }
 +        $responesstats = new quiz_statistics_response_analyser($question);
 +        $responesstats->load_cached($quizstats->id);
 +
 +        $qtable->setup($reporturl, $question, $responesstats);
 +        if ($this->table->is_downloading()) {
 +            $exportclass->output_headers($qtable->headers);
 +        }
 +
 +        foreach ($responesstats->responseclasses as $partid => $partclasses) {
 +            $rowdata = new stdClass();
 +            $rowdata->part = $partid;
 +            foreach ($partclasses as $responseclassid => $responseclass) {
 +                $rowdata->responseclass = $responseclass->responseclass;
 +
 +                $responsesdata = $responesstats->responses[$partid][$responseclassid];
 +                if (empty($responsesdata)) {
 +                    if (!array_key_exists('responseclass', $qtable->columns)) {
 +                        $rowdata->response = $responseclass->responseclass;
                      } else {
 -                        $this->qtable->add_data_keyed($this->qtable->format_row($teacherresponserow));
 +                        $rowdata->response = '';
                      }
 +                    $rowdata->fraction = $responseclass->fraction;
 +                    $rowdata->count = 0;
 +                    $qtable->add_data_keyed($qtable->format_row($rowdata));
 +                    continue;
 +                }
 +
 +                foreach ($responsesdata as $response => $data) {
 +                    $rowdata->response = $response;
 +                    $rowdata->fraction = $data->fraction;
 +                    $rowdata->count = $data->count;
 +                    $qtable->add_data_keyed($qtable->format_row($rowdata));
                  }
 -                $first = false;
              }
 -            $this->qtable->finish_output(!$this->table->is_downloading());
 -        }
 -        if (!$this->table->is_downloading()){
 -            $url = $reporturl->out();
 -            $text = get_string('backtoquizreport', 'quiz_statistics');
 -            echo $OUTPUT->box("<a href=\"$url\">$text</a>", 'boxaligncenter generalbox boxwidthnormal mdl-align');
          }
 +
 +        $qtable->finish_output(!$this->table->is_downloading());
      }
  
 -    function output_quiz_structure_analysis_table($s, $questions, $subquestions){
 -        global $OUTPUT;
 -        if ($s){
 -            if (!$this->table->is_downloading()){
 -                echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
 -            }
 -            foreach ($questions as $question){
 -                $this->table->add_data_keyed($this->table->format_row($question));
 -                if (!empty($question->_stats->subquestions)){
 -                    $subitemstodisplay = explode(',', $question->_stats->subquestions);
 -                    foreach ($subitemstodisplay as $subitemid){
 -                        $subquestions[$subitemid]->maxgrade = $question->maxgrade;
 -                        $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
 -                    }
 -                }
 +    /**
 +     * Output the table that lists all the questions in the quiz with their statistics.
 +     * @param int $s number of attempts.
 +     * @param array $questions the questions in the quiz.
 +     * @param array $subquestions the subquestions of any random questions.
 +     */
 +    protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
 +        if (!$s) {
 +            return;
 +        }
 +
 +        foreach ($questions as $question) {
 +            // Output the data for this questions.
 +            $this->table->add_data_keyed($this->table->format_row($question));
 +
 +            if (empty($question->_stats->subquestions)) {
 +                continue;
              }
  
 -            $this->table->finish_output(!$this->table->is_downloading());
 +            // And its subquestions, if it has any.
 +            $subitemstodisplay = explode(',', $question->_stats->subquestions);
 +            foreach ($subitemstodisplay as $subitemid) {
 +                $subquestions[$subitemid]->maxmark = $question->maxmark;
 +                $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
 +            }
          }
 +
 +        $this->table->finish_output(!$this->table->is_downloading());
      }
  
 -    function output_quiz_info_table($course, $cm, $quiz, $quizstats, $usingattemptsstring,
 -                    $currentgroup, $groupstudents, $useallattempts, $download, $reporturl, $everything){
 -        global $DB, $OUTPUT;
 -        // Print information on the number of existing attempts
 -        $quizinformationtablehtml = $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 2, 'main');
 -        $quizinformationtable = new html_table();
 -        $quizinformationtable->align = array('center', 'center');
 -        $quizinformationtable->width = '60%';
 -        $quizinformationtable->attributes['class'] = 'generaltable titlesleft boxaligncenter';
 -        $quizinformationtable->data = array();
 -        $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name);
 -        $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname);
 -        if ($cm->idnumber){
 -            $quizinformationtable->data[] = array(get_string('idnumbermod'), $cm->idnumber);
 -        }
 -        if ($quiz->timeopen){
 -            $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen));
 -        }
 -        if ($quiz->timeclose){
 -            $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose));
 -        }
 -        if ($quiz->timeopen && $quiz->timeclose){
 -            $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen));
 -        }
 -        $format = array('firstattemptscount' => '',
 -                    'allattemptscount' => '',
 -                    'firstattemptsavg' => 'sumgrades_as_percentage',
 -                    'allattemptsavg' => 'sumgrades_as_percentage',
 -                    'median' => 'sumgrades_as_percentage',
 -                    'standarddeviation' => 'sumgrades_as_percentage',
 -                    'skewness' => '',
 -                    'kurtosis' => '',
 -                    'cic' => 'number_format',
 -                    'errorratio' => 'number_format',
 -                    'standarderror' => 'sumgrades_as_percentage');
 -        foreach ($quizstats as $property => $value){
 -            if (!isset($format[$property])){
 +    protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
 +
 +        // You can edit this array to control which statistics are displayed.
 +        $todisplay = array('firstattemptscount' => 'number',
 +                    'allattemptscount' => 'number',
 +                    'firstattemptsavg' => 'summarks_as_percentage',
 +                    'allattemptsavg' => 'summarks_as_percentage',
 +                    'median' => 'summarks_as_percentage',
 +                    'standarddeviation' => 'summarks_as_percentage',
 +                    'skewness' => 'number_format',
 +                    'kurtosis' => 'number_format',
 +                    'cic' => 'number_format_percent',
 +                    'errorratio' => 'number_format_percent',
 +                    'standarderror' => 'summarks_as_percentage');
 +
 +        // General information about the quiz.
 +        $quizinfo = array();
 +        $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
 +        $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
 +        if ($cm->idnumber) {
 +            $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
 +        }
 +        if ($quiz->timeopen) {
 +            $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
 +        }
 +        if ($quiz->timeclose) {
 +            $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
 +        }
 +        if ($quiz->timeopen && $quiz->timeclose) {
 +            $quizinfo[get_string('duration', 'quiz_statistics')] = format_time($quiz->timeclose - $quiz->timeopen);
 +        }
 +
 +        // The statistics.
 +        foreach ($todisplay as $property => $format) {
 +            if (!isset($quizstats->$property) || empty($format[$property])) {
                  continue;
              }
 -            if (!is_null($value)){
 -                switch ($format[$property]){
 -                    case 'sumgrades_as_percentage' :
 -                        $formattedvalue = quiz_report_scale_sumgrades_as_percentage($value, $quiz);
 -                        break;
 -                    case 'number_format' :
 -                        $formattedvalue = quiz_format_grade($quiz, $value).'%';
 -                        break;
 -                    default :
 -                        $formattedvalue = $value;
 -                }
 -                $quizinformationtable->data[] = array(get_string($property, 'quiz_statistics', $usingattemptsstring), $formattedvalue);
 +            $value = $quizstats->$property;
 +
 +            switch ($format) {
 +                case 'summarks_as_percentage':
 +                    $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
 +                    break;
 +                case 'number_format_percent':
 +                    $formattedvalue = quiz_format_grade($quiz, $value) . '%';
 +                    break;
 +                case 'number_format':
 +                    // + 2 decimal places, since not a percentage, and we want the same number of sig figs.
 +                    $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
 +                    break;
 +                case 'number':
 +                    $formattedvalue = $value + 0;
 +                    break;
 +                default:
 +                    $formattedvalue = $value;
              }
 +
 +            $quizinfo[get_string($property, 'quiz_statistics',
 +                    $this->using_attempts_string(!empty($quizstats->allattempts)))] =
 +                    $formattedvalue;
          }
 -        if (!$this->table->is_downloading()){
 -            if (isset($quizstats->timemodified)){
 -                list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quiz->id, $currentgroup, $groupstudents, $useallattempts);
 -                $sql = 'SELECT COUNT(1) ' .
 -                    'FROM ' .$fromqa.' '.
 -                    'WHERE ' .$whereqa.' AND qa.timefinish > :time';
 -                $a = new stdClass();
 -                $a->lastcalculated = format_time(time() - $quizstats->timemodified);
 -                if (!$a->count = $DB->count_records_sql($sql, array('time'=>$quizstats->timemodified)+$qaparams)){
 -                    $a->count = 0;
 -                }
 -                $quizinformationtablehtml .= $OUTPUT->box_start('boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
 -                $quizinformationtablehtml .= get_string('lastcalculated', 'quiz_statistics', $a);
 -                $aurl = new moodle_url($reporturl->out_omit_querystring(), $reporturl->params() + array('recalculate' => 1, 'sesskey' => sesskey()));
 -                $quizinformationtablehtml .= $OUTPUT->single_button($aurl, get_string('recalculatenow', 'quiz_statistics'));
 -                $quizinformationtablehtml .= $OUTPUT->box_end();
 -            }
 -            $downloadoptions = $this->table->get_download_menu();
 -            $quizinformationtablehtml .= '<form action="'. $this->table->baseurl .'" method="post">';
 -            $quizinformationtablehtml .= '<div class="mdl-align">';
 -            $quizinformationtablehtml .= '<input type="hidden" name="everything" value="1"/>';
 -            $quizinformationtablehtml .= '<input type="submit" value="'.get_string('downloadeverything', 'quiz_statistics').'"/>';
 -            $quizinformationtablehtml .= html_writer::select($downloadoptions, 'download', $this->table->defaultdownloadformat, false);
 -            $quizinformationtablehtml .= '</div></form>';
 -        }
 -        $quizinformationtablehtml .= html_writer::table($quizinformationtable);
 -        if (!$this->table->is_downloading()){
 -            echo $quizinformationtablehtml;
 -        } elseif ($everything) {
 -            $exportclass =& $this->table->export_class_instance();
 -            if ($download == 'xhtml'){
 -                echo $quizinformationtablehtml;
 -            } else {
 -                $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
 -                $headers = array();
 -                $row = array();
 -                foreach ($quizinformationtable->data as $data){
 -                    $headers[]= $data[0];
 -                    $row[] = $data[1];
 -                }
 -                $exportclass->output_headers($headers);
 -                $exportclass->add_data($row);
 -                $exportclass->finish_table();
 -            }
 +
 +        return $quizinfo;
 +    }
 +
 +    /**
 +     * Output the table of overall quiz statistics.
 +     * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
 +     * @return string the HTML.
 +     */
 +    protected function output_quiz_info_table($quizinfo) {
 +
 +        $quizinfotable = new html_table();
 +        $quizinfotable->align = array('center', 'center');
 +        $quizinfotable->width = '60%';
 +        $quizinfotable->attributes['class'] = 'generaltable titlesleft';
 +        $quizinfotable->data = array();
 +
 +        foreach ($quizinfo as $heading => $value) {
 +             $quizinfotable->data[] = array($heading, $value);
          }
 +
 +        return html_writer::table($quizinfotable);
      }
  
 -    function quiz_stats($nostudentsingroup, $quizid, $currentgroup, $groupstudents, $questions, $useallattempts){
 -        global $CFG, $DB;
 -        if (!$nostudentsingroup){
 -            //Calculating_MEAN_of_grades_for_all_attempts_by_students
 -            //http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Calculating_MEAN_of_grades_for_all_attempts_by_students
 +    /**
 +     * Download the table of overall quiz statistics.
 +     * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
 +     */
 +    protected function download_quiz_info_table($quizinfo) {
 +        global $OUTPUT;
  
 -            list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents);
 +        // XHTML download is a special case.
 +        if ($this->table->is_downloading() == 'xhtml') {
 +            echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
 +            echo $this->output_quiz_info_table($quizinfo);
 +            return;
 +        }
  
 -            $sql = 'SELECT (CASE WHEN attempt=1 THEN 1 ELSE 0 END) AS isfirst, COUNT(1) AS countrecs, SUM(sumgrades) AS total ' .
 -                    'FROM '.$fromqa.
 -                    'WHERE ' .$whereqa.
 -                    'GROUP BY (attempt=1)';
 +        // Reformat the data ready for output.
 +        $headers = array();
 +        $row = array();
 +        foreach ($quizinfo as $heading => $value) {
 +            $headers[] = $heading;
 +            $row[] = $value;
 +        }
  
 -            if (!$attempttotals = $DB->get_records_sql($sql, $qaparams)){
 -                $s = 0;
 -                $usingattemptsstring = '';
 -            } else {
 -                $firstattempt = $attempttotals[1];
 -                $allattempts = new stdClass();
 -                $allattempts->countrecs = $firstattempt->countrecs +
 -                                (isset($attempttotals[0])?$attempttotals[0]->countrecs:0);
 -                $allattempts->total = $firstattempt->total +
 -                                (isset($attempttotals[0])?$attempttotals[0]->total:0);
 -                if ($useallattempts){
 -                    $usingattempts = $allattempts;
 -                    $usingattempts->attempts = get_string('allattempts', 'quiz_statistics');
 -                    $usingattempts->sql = '';
 -                } else {
 -                    $usingattempts = $firstattempt;
 -                    $usingattempts->attempts = get_string('firstattempts', 'quiz_statistics');
 -                    $usingattempts->sql = 'AND qa.attempt=1 ';
 -                }
 -                $usingattemptsstring = $usingattempts->attempts;
 -                $s = $usingattempts->countrecs;
 -                $sumgradesavg = $usingattempts->total / $usingattempts->countrecs;
 -            }
 +        // Do the output.
 +        $exportclass = $this->table->export_class_instance();
 +        $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
 +        $exportclass->output_headers($headers);
 +        $exportclass->add_data($row);
 +        $exportclass->finish_table();
 +    }
 +
 +    /**
 +     * Output the HTML needed to show the statistics graph.
 +     * @param int $quizstatsid the id of the statistics to show in the graph.
 +     */
 +    protected function output_statistics_graph($quizstatsid, $s) {
 +        global $OUTPUT;
 +
 +        if ($s == 0) {
 +            return;
 +        }
 +
 +        $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
 +                array('id' => $quizstatsid));
 +        $OUTPUT->heading(get_string('statisticsreportgraph', 'quiz_statistics'));
 +        echo html_writer::tag('div', html_writer::empty_tag('img',
 +                array('src' => $imageurl, 'alt' => get_string('statisticsreportgraph', 'quiz_statistics'))),
 +                array('class' => 'graph'));
 +    }
 +
 +    /**
 +     * Return the stats data for when there are no stats to show.
 +     *
 +     * @param array $questions question definitions.
 +     * @param int $firstattemptscount number of first attempts (optional).
 +     * @param int $firstattemptscount total number of attempts (optional).
 +     * @return array with three elements:
 +     *      - integer $s Number of attempts included in the stats (0).
 +     *      - array $quizstats The statistics for overall attempt scores.
 +     *      - array $qstats The statistics for each question.
 +     */
 +    protected function get_emtpy_stats($questions, $firstattemptscount = 0,
 +            $allattemptscount = 0) {
 +        $quizstats = new stdClass();
 +        $quizstats->firstattemptscount = $firstattemptscount;
 +        $quizstats->allattemptscount = $allattemptscount;
 +
 +        $qstats = new stdClass();
 +        $qstats->questions = $questions;
 +        $qstats->subquestions = array();
 +        $qstats->responses = array();
 +
 +        return array(0, $quizstats, false);
 +    }
 +
 +    /**
 +     * Compute the quiz statistics.
 +     *
 +     * @param object $quizid the quiz id.
 +     * @param int $currentgroup the current group. 0 for none.
 +     * @param bool $nostudentsingroup true if there a no students.
 +     * @param bool $useallattempts use all attempts, or just first attempts.
 +     * @param array $groupstudents students in this group.
 +     * @param array $questions question definitions.
 +     * @return array with three elements:
 +     *      - integer $s Number of attempts included in the stats.
 +     *      - array $quizstats The statistics for overall attempt scores.
 +     *      - array $qstats The statistics for each question.
 +     */
 +    protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
 +            $useallattempts, $groupstudents, $questions) {
 +        global $DB;
 +
 +        // Calculating MEAN of marks for all attempts by students
 +        // http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Calculating_MEAN_of_grades_for_all_attempts_by_students
 +        if ($nostudentsingroup) {
 +            return $this->get_emtpy_stats($questions);
 +        }
 +
 +        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
 +                $quizid, $currentgroup, $groupstudents, true);
 +
 +        $attempttotals = $DB->get_records_sql("
 +                SELECT
 +                    CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
 +                    COUNT(1) AS countrecs,
 +                    SUM(sumgrades) AS total
 +                FROM $fromqa
 +                WHERE $whereqa
 +                GROUP BY attempt = 1", $qaparams);
 +
 +        if (!$attempttotals) {
 +            return $this->get_emtpy_stats($questions);
 +        }
 +
 +        if (isset($attempttotals[1])) {
 +            $firstattempts = $attempttotals[1];
 +            $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
 +        } else {
 +            $firstattempts = new stdClass();
 +            $firstattempts->countrecs = 0;
 +            $firstattempts->total = 0;
 +            $firstattempts->average = '-';
 +        }
 +
 +        $allattempts = new stdClass();
 +        if (isset($attempttotals[0])) {
 +            $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
 +            $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
          } else {
 -            $s = 0;
 +            $allattempts->countrecs = $firstattempts->countrecs;
 +            $allattempts->total = $firstattempts->total;
 +        }
 +
 +        if ($useallattempts) {
 +            $usingattempts = $allattempts;
 +            $usingattempts->sql = '';
 +        } else {
 +            $usingattempts = $firstattempts;
 +            $usingattempts->sql = 'AND quiza.attempt = 1 ';
 +        }
 +
 +        $s = $usingattempts->countrecs;
 +        if ($s == 0) {
 +            return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
 +                    $allattempts->countrecs);
          }
 +        $summarksavg = $usingattempts->total / $usingattempts->countrecs;
 +
          $quizstats = new stdClass();
 -        if ($s == 0){
 -            $quizstats->firstattemptscount = 0;
 -            $quizstats->allattemptscount = 0;
 +        $quizstats->allattempts = $useallattempts;
 +        $quizstats->firstattemptscount = $firstattempts->countrecs;
 +        $quizstats->allattemptscount = $allattempts->countrecs;
 +        $quizstats->firstattemptsavg = $firstattempts->average;
 +        $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
 +
 +        // Recalculate sql again this time possibly including test for first attempt.
 +        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
 +                $quizid, $currentgroup, $groupstudents, $useallattempts);
 +
 +        // Median
 +        if ($s % 2 == 0) {
 +            //even number of attempts
 +            $limitoffset = $s/2 - 1;
 +            $limit = 2;
          } else {
 -            $quizstats->firstattemptscount = $firstattempt->countrecs;
 -            $quizstats->allattemptscount = $allattempts->countrecs;
 -            $quizstats->firstattemptsavg = $firstattempt->total / $firstattempt->countrecs;
 -            $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
 +            $limitoffset = floor($s/2);
 +            $limit = 1;
          }
 -        //recalculate sql again this time possibly including test for first attempt.
 -        list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
 +        $sql = "SELECT id, sumgrades
 +                FROM $fromqa
 +                WHERE $whereqa
 +                ORDER BY sumgrades";
  
 -        //get the median
 -        if ($s) {
 +        $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
  
 -            if (($s%2)==0){
 -                //even number of attempts
 -                $limitoffset = ($s/2) - 1;
 -                $limit = 2;
 -            } else {
 -                $limitoffset = (floor($s/2));
 -                $limit = 1;
 -            }
 -            $sql = 'SELECT id, sumgrades ' .
 -                'FROM ' .$fromqa.
 -                'WHERE ' .$whereqa.
 -                'ORDER BY sumgrades';
 -            if (!$mediangrades = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit)){
 -                print_error('errormedian', 'quiz_statistics');
 -            }
 -            $quizstats->median = array_sum($mediangrades) / count($mediangrades);
 -            if ($s>1){
 -                //fetch sum of squared, cubed and power 4d
 -                //differences between grades and mean grade
 -                $mean = $usingattempts->total / $s;
 -                $sql = "SELECT " .
 -                    "SUM(POWER((qa.sumgrades - :mean1),2)) AS power2, " .
 -                    "SUM(POWER((qa.sumgrades - :mean2),3)) AS power3, ".
 -                    "SUM(POWER((qa.sumgrades - :mean3),4)) AS power4 ".
 -                    'FROM ' .$fromqa.
 -                    'WHERE ' .$whereqa;
 -                $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
 -                if (!$powers = $DB->get_record_sql($sql, $params)){
 -                    print_error('errorpowers', 'quiz_statistics');
 -                }
 +        $quizstats->median = array_sum($medianmarks) / count($medianmarks);
 +        if ($s > 1) {
 +            //fetch sum of squared, cubed and power 4d
 +            //differences between marks and mean mark
 +            $mean = $usingattempts->total / $s;
 +            $sql = "SELECT
 +                    SUM(POWER((quiza.sumgrades - $mean),2)) AS power2,
 +                    SUM(POWER((quiza.sumgrades - $mean),3)) AS power3,
 +                    SUM(POWER((quiza.sumgrades - $mean),4)) AS power4
 +                    FROM $fromqa
 +                    WHERE $whereqa";
 +            $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
  
 -                //Standard_Deviation
 -                //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Standard_Deviation
 +            $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
  
 -                $quizstats->standarddeviation = sqrt($powers->power2 / ($s -1));
 +            // Standard_Deviation
 +            //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Standard_Deviation
  
 +            $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
  
 +            // Skewness
 +            if ($s > 2) {
 +                //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
 +                $m2= $powers->power2 / $s;
 +                $m3= $powers->power3 / $s;
 +                $m4= $powers->power4 / $s;
  
 -                //Skewness_and_Kurtosis
 -                if ($s>2){
 -                    //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
 -                    $m2= $powers->power2 / $s;
 -                    $m3= $powers->power3 / $s;
 -                    $m4= $powers->power4 / $s;
 +                $k2= $s*$m2/($s-1);
 +                $k3= $s*$s*$m3/(($s-1)*($s-2));
 +                if ($k2) {
 +                    $quizstats->skewness = $k3 / (pow($k2, 3/2));
 +                }
 +            }
  
 -                    $k2= $s*$m2/($s-1);
 -                    $k3= $s*$s*$m3/(($s-1)*($s-2));
 -                    if ($k2){
 -                        $quizstats->skewness = $k3 / (pow($k2, 3/2));
 -                    }
 +            // Kurtosis
 +            if ($s > 3) {
 +                $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
 +                if ($k2) {
 +                    $quizstats->kurtosis = $k4 / ($k2*$k2);
                  }
 +            }
 +        }
  
 +        $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
 +        $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
 +        $qstats->compute_statistics();
  
 -                if ($s>3){
 -                    $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
 -                    if ($k2){
 -                        $quizstats->kurtosis = $k4 / ($k2*$k2);
 -                    }
 -                }
 +        if ($s > 1) {
 +            $p = count($qstats->questions); // No of positions
 +            if ($p > 1 && isset($k2)) {
 +                $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->get_sum_of_mark_variance())/$k2);
 +                $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
 +                $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
              }
          }
 -        if ($s){
 -            require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php");
 -            $qstats = new qstats($questions, $s, $sumgradesavg);
 -            $qstats->get_records($quizid, $currentgroup, $groupstudents, $useallattempts);
 -            $qstats->process_states();
 -            $qstats->process_responses();
 +
 +        return array($s, $quizstats, $qstats);
 +    }
 +
 +    /**
 +     * Load the cached statistics from the database.
 +     *
 +     * @param object $quiz the quiz settings
 +     * @param int $currentgroup the current group. 0 for none.
 +     * @param bool $nostudentsingroup true if there a no students.
 +     * @param bool $useallattempts use all attempts, or just first attempts.
 +     * @param array $groupstudents students in this group.
 +     * @param array $questions question definitions.
 +     * @return array with 4 elements:
 +     *     - $quizstats The statistics for overall attempt scores.
 +     *     - $questions The questions, with an additional _stats field.
 +     *     - $subquestions The subquestions, if any, with an additional _stats field.
 +     *     - $s Number of attempts included in the stats.
 +     * If there is no cached data in the database, returns an array of four nulls.
 +     */
 +    protected function try_loading_cached_stats($quiz, $currentgroup,
 +            $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
 +        global $DB;
 +
 +        $timemodified = time() - self::TIME_TO_CACHE_STATS;
 +        $quizstats = $DB->get_record_select('quiz_statistics',
 +                'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
 +                array($quiz->id, $currentgroup, $useallattempts, $timemodified));
 +
 +        if (!$quizstats) {
 +            // No cached data found.
 +            return array(null, $questions, null, null);
 +        }
 +
 +        if ($useallattempts) {
 +            $s = $quizstats->allattemptscount;
          } else {
 -            $qstats = false;
 -        }
 -        if ($s>1){
 -            $p = count($qstats->questions);//no of positions
 -            if ($p > 1){
 -                if (isset($k2)){
 -                    $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2);
 -                    $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
 -                    $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
 -                }
 +            $s = $quizstats->firstattemptscount;
 +        }
 +
 +        $subquestions = array();
 +        $questionstats = $DB->get_records('quiz_question_statistics',
 +                array('quizstatisticsid' => $quizstats->id));
 +
 +        $subquestionstats = array();
 +        foreach ($questionstats as $stat) {
 +            if ($stat->slot) {
 +                $questions[$stat->slot]->_stats = $stat;
 +            } else {
 +                $subquestionstats[$stat->questionid] = $stat;
              }
          }
 -        return array($s, $usingattemptsstring, $quizstats, $qstats);
 +
 +        if (!empty($subquestionstats)) {
 +            $subqstofetch = array_keys($subquestionstats);
 +            $subquestions = question_load_questions($subqstofetch);
 +            foreach ($subquestions as $subqid => $subq) {
 +                $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
 +                $subquestions[$subqid]->maxmark = $subq->defaultmark;
 +            }
 +        }
 +
 +        return array($quizstats, $questions, $subquestions, $s);
      }
  
 -    function quiz_questions_stats($quiz, $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions){
 +    /**
 +     * Store the statistics in the cache tables in the database.
 +     *
 +     * @param object $quizid the quiz id.
 +     * @param int $currentgroup the current group. 0 for none.
 +     * @param bool $useallattempts use all attempts, or just first attempts.
 +     * @param object $quizstats The statistics for overall attempt scores.
 +     * @param array $questions The questions, with an additional _stats field.
 +     * @param array $subquestions The subquestions, if any, with an additional _stats field.
 +     */
 +    protected function cache_stats($quizid, $currentgroup,
 +            $quizstats, $questions, $subquestions) {
          global $DB;
 -        $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS;
 -        $params = array('quizid'=>$quiz->id, 'groupid'=>(int)$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified);
 -        if (!$quizstats = $DB->get_record_select('quiz_statistics', 'quizid = :quizid  AND groupid = :groupid AND allattempts = :allattempts AND timemodified > :timemodified', $params, '*', true)){
 -            list($s, $usingattemptsstring, $quizstats, $qstats) = $this->quiz_stats($nostudentsingroup, $quiz->id, $currentgroup, $groupstudents, $questions, $useallattempts);
 -            if ($s){
 -                $toinsert = (object)((array)$quizstats + $params);
 -                if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
 -                    $toinsert->errorratio = NULL;
 -                }
 -                if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
 -                    $toinsert->standarderror = NULL;
 -                }
 -                $toinsert->timemodified = time();
 -                $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
 -                foreach ($qstats->questions as $question){
 -                    $question->_stats->quizstatisticsid = $quizstats->id;
 -                    $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
 -                }
 -                foreach ($qstats->subquestions as $subquestion){
 -                    $subquestion->_stats->quizstatisticsid = $quizstats->id;
 -                    $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
 -                }
 -                foreach ($qstats->responses as $response){
 -                    $response->quizstatisticsid = $quizstats->id;
 -                    $DB->insert_record('quiz_question_response_stats', $response, false);
 -                }
 -            }
 -            if ($qstats){
 +
 +        $toinsert = clone($quizstats);
 +        $toinsert->quizid = $quizid;
 +        $toinsert->groupid = $currentgroup;
 +        $toinsert->timemodified = time();
 +
 +        // Fix up some dodgy data.
 +        if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
 +            $toinsert->errorratio = null;
 +        }
 +        if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
 +            $toinsert->standarderror = null;
 +        }
 +
 +        // Store the data.
 +        $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
 +
 +        foreach ($questions as $question) {
 +            $question->_stats->quizstatisticsid = $quizstats->id;
 +            $DB->insert_record('quiz_question_statistics', $question->_stats, false);
 +        }
 +
 +        foreach ($subquestions as $subquestion) {
 +            $subquestion->_stats->quizstatisticsid = $quizstats->id;
 +            $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
 +        }
 +
 +        return $quizstats->id;
 +    }
 +
 +    /**
 +     * Get the quiz and question statistics, either by loading the cached results,
 +     * or by recomputing them.
 +     *
 +     * @param object $quiz the quiz settings.
 +     * @param int $currentgroup the current group. 0 for none.
 +     * @param bool $nostudentsingroup true if there a no students.
 +     * @param bool $useallattempts use all attempts, or just first attempts.
 +     * @param array $groupstudents students in this group.
 +     * @param array $questions question definitions.
 +     * @return array with 4 elements:
 +     *     - $quizstats The statistics for overall attempt scores.
 +     *     - $questions The questions, with an additional _stats field.
 +     *     - $subquestions The subquestions, if any, with an additional _stats field.
 +     *     - $s Number of attempts included in the stats.
 +     */
 +    protected function get_quiz_and_questions_stats($quiz, $currentgroup,
 +            $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
 +
 +        list($quizstats, $questions, $subquestions, $s) =
 +                $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
 +                        $useallattempts, $groupstudents, $questions);
 +
 +        if (is_null($quizstats)) {
 +            list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
 +                    $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
 +
 +            if ($s) {
                  $questions = $qstats->questions;
                  $subquestions = $qstats->subquestions;
 -            } else {
 -                $questions = array();
 -                $subquestions = array();
 -            }
 -        } else {
 -            //use cached results
 -            if ($useallattempts){
 -                $usingattemptsstring = get_string('allattempts', 'quiz_statistics');
 -                $s = $quizstats->allattemptscount;
 -            } else {
 -                $usingattemptsstring = get_string('firstattempts', 'quiz_statistics');
 -                $s = $quizstats->firstattemptscount;
 +
 +                $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
 +                        $quizstats, $questions, $subquestions);
 +
 +                $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
 +                        $nostudentsingroup, $useallattempts, $groupstudents,
 +                        $questions, $subquestions);
              }
 -            $subquestions = array();
 -            $questionstats = $DB->get_records('quiz_question_statistics', array('quizstatisticsid'=>$quizstats->id), 'subquestion ASC');
 -            $questionstats = quiz_report_index_by_keys($questionstats, array('subquestion', 'questionid'));
 -            if (1 < count($questionstats)){
 -                list($mainquestionstats, $subquestionstats) = $questionstats;
 -                $subqstofetch = array_keys($subquestionstats);
 -                $subquestions = question_load_questions($subqstofetch);
 -                foreach (array_keys($subquestions) as $subqid){
 -                    $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
 -                }
 -            } elseif (count($questionstats)) {
 -                $mainquestionstats = $questionstats[0];
 +        }
 +
 +        return array($quizstats, $questions, $subquestions, $s);
 +    }
 +
 +    protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
 +            $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
 +
 +        $qubaids = quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts);
 +
 +        $done = array();
 +        foreach ($questions as $question) {
 +            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
 +                continue;
              }
 -            if (count($questionstats)) {
 -                foreach (array_keys($questions) as $qid){
 -                    $questions[$qid]->_stats = $mainquestionstats[$qid];
 -                }
 +            $done[$question->id] = 1;
 +
 +            $responesstats = new quiz_statistics_response_analyser($question);
 +            $responesstats->analyse($qubaids);
 +            $responesstats->store_cached($quizstatisticsid);
 +        }
 +
 +        foreach ($subquestions as $question) {
 +            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
 +                    isset($done[$question->id])) {
 +                continue;
              }
 +            $done[$question->id] = 1;
 +
 +            $responesstats = new quiz_statistics_response_analyser($question);
 +            $responesstats->analyse($qubaids);
 +            $responesstats->store_cached($quizstatisticsid);
 +        }
 +    }
 +
 +    /**
 +     * @return string HTML snipped for the Download full report as UI.
 +     */
 +    protected function everything_download_options() {
 +        $downloadoptions = $this->table->get_download_menu();
 +
 +        $output = '<form action="'. $this->table->baseurl .'" method="post">';
 +        $output .= '<div class="mdl-align">';
 +        $output .= '<input type="hidden" name="everything" value="1"/>';
 +        $output .= '<input type="submit" value="'.get_string('downloadeverything', 'quiz_statistics').'"/>';
 +        $output .= html_writer::select($downloadoptions, 'download', $this->table->defaultdownloadformat, false);
 +        $output .= '</div></form>';
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Generate the snipped of HTML that says when the stats were last caculated,
 +     * with a recalcuate now button.
 +     * @param object $quizstats the overall quiz statistics.
 +     * @param int $quizid the quiz id.
 +     * @param int $currentgroup the id of the currently selected group, or 0.
 +     * @param array $groupstudents ids of students in the group.
 +     * @param bool $useallattempts whether to use all attempts, instead of just first attempts.
 +     * @return string a HTML snipped saying when the stats were last computed, or blank if that is not appropriate.
 +     */
 +    protected function output_caching_info($quizstats, $quizid, $currentgroup,
 +            $groupstudents, $useallattempts, $reporturl) {
 +        global $DB, $OUTPUT;
 +
 +        if (empty($quizstats->timemodified)) {
 +            return '';
 +        }
 +
 +        // Find the number of attempts since the cached statistics were computed.
 +        list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
 +                $quizid, $currentgroup, $groupstudents, $useallattempts, true);
 +        $count = $DB->count_records_sql("
 +                SELECT COUNT(1)
 +                FROM $fromqa
 +                WHERE $whereqa
 +                AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
 +
 +        if (!$count) {
 +            $count = 0;
 +        }
 +
 +        // Generate the output.
 +        $a = new stdClass();
 +        $a->lastcalculated = format_time(time() - $quizstats->timemodified);
 +        $a->count = $count;
 +
 +        $recalcualteurl = new moodle_url($reporturl, array('recalculate' => 1, 'sesskey' => sesskey()));
 +        $output = '';
 +        $output .= $OUTPUT->box_start('boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
 +        $output .= get_string('lastcalculated', 'quiz_statistics', $a);
 +        $output .= $OUTPUT->single_button($recalcualteurl, get_string('recalculatenow', 'quiz_statistics'));
 +        $output .= $OUTPUT->box_end(true);
 +
 +        return $output;
 +    }
 +
 +    /**
 +     * Clear the cached data for a particular report configuration. This will
 +     * trigger a re-computation the next time the report is displayed.
 +     * @param int $quizid the quiz id.
 +     * @param int $currentgroup a group id, or 0.
 +     * @param bool $useallattempts whether all attempts, or just first attempts are included.
 +     */
 +    protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
 +        global $DB;
 +
 +        $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
 +                'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id,1');
 +
 +        if (!$todelete) {
 +            return;
 +        }
 +
 +        list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
 +
 +        $DB->delete_records_select('quiz_question_statistics', 'quizstatisticsid ' . $todeletesql, $todeleteparams);
 +        $DB->delete_records_select('quiz_question_response_stats', 'quizstatisticsid ' . $todeletesql, $todeleteparams);
 +        $DB->delete_records_select('quiz_statistics', 'id ' . $todeletesql, $todeleteparams);
 +    }
 +
 +    /**
 +     * @param bool $useallattempts whether we are using all attempts.
 +     * @return the appropriate lang string to describe this option.
 +     */
 +    protected function using_attempts_string($useallattempts) {
 +        if ($useallattempts) {
 +            return get_string('allattempts', 'quiz_statistics');
 +        } else {
 +            return get_string('firstattempts', 'quiz_statistics');
          }
 -        return array($quizstats, $questions, $subquestions, $s, $usingattemptsstring);
      }
  }
 -function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts = true){
 -    global $DB;
 -    $fromqa = '{quiz_attempts} qa ';
 -    $whereqa = 'qa.quiz = :quizid AND qa.preview=0 AND qa.timefinish !=0 ';
 -    $qaparams = array('quizid'=>$quizid);
 +
 +function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
 +        $allattempts = true, $includeungraded = false) {
 +    global $CFG;
 +
 +    $fromqa = '{quiz_attempts} quiza ';
 +
 +    $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.timefinish <> 0';
 +    $qaparams = array('quizid' => $quizid);
 +
      if (!empty($currentgroup) && $groupstudents) {
-         list($grpsql, $grpparams) = get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u0000');
 -        list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u');
 -        $whereqa .= 'AND qa.userid '.$grpsql.' ';
++        list($grpsql, $grpparams) = get_in_or_equal(array_keys($groupstudents),
++                SQL_PARAMS_NAMED, 'u');
 +        $whereqa .= " AND quiza.userid $grpsql";
          $qaparams += $grpparams;
      }
 -    if (!$allattempts){
 -        $whereqa .= 'AND qa.attempt=1 ';
 +
 +    if (!$allattempts) {
 +        $whereqa .= ' AND quiza.attempt = 1';
 +    }
 +
 +    if (!$includeungraded) {
 +        $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
      }
 +
      return array($fromqa, $whereqa, $qaparams);
  }
  
@@@ -103,8 -53,7 +103,9 @@@ div.editq div.question div.content .sin
  #page-mod-quiz-view #page .quizgradefeedback,
  #page-mod-quiz-view #page .quizattempt {text-align: center;}
  #page-mod-quiz-view #page .quizattemptsummary td p {margin-top: 0;}
 +table.quizattemptsummary .bestrow td {background-color: #e8e8e8;}
 +table.quizattemptsummary .noreviewmessage {color: gray;}
+ #page-mod-quiz-view .generaltable.quizattemptsummary {margin-left:auto;margin-right:auto;}
  #page-mod-quiz-view .generalbox#feedback {width:70%;margin-left:auto;margin-right:auto;padding-bottom:15px;}
  #page-mod-quiz-view .generalbox#feedback h2 {margin: 0;}
  #page-mod-quiz-view .generalbox#feedback h3 {text-align: left;}
  <?php
  
 -/// This page prints a particular instance of quiz
 -
 -    require_once(dirname(__FILE__) . '/../../config.php');
 -    require_once($CFG->libdir.'/gradelib.php');
 -    require_once($CFG->dirroot.'/mod/quiz/locallib.php');
 -    require_once($CFG->libdir . '/completionlib.php');
 -
 -    $id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
 -    $q = optional_param('q',  0, PARAM_INT);  // quiz ID
 -
 -    if ($id) {
 -        if (! $cm = get_coursemodule_from_id('quiz', $id)) {
 -            print_error('invalidcoursemodule');
 -        }
 -        if (! $course = $DB->get_record('course', array('id' => $cm->course))) {
 -            print_error('coursemisconf');
 -        }
 -        if (! $quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
 -            print_error('invalidcoursemodule');
 -        }
 -    } else {
 -        if (! $quiz = $DB->get_record('quiz', array('id' => $q))) {
 -            print_error('invalidquizid', 'quiz');
 -        }
 -        if (! $course = $DB->get_record('course', array('id' => $quiz->course))) {
 -            print_error('invalidcourseid');
 -        }
 -        if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
 -            print_error('invalidcoursemodule');
 -        }
 -    }
 -
 -/// Check login and get context.
 -    require_login($course->id, false, $cm);
 -    $context = get_context_instance(CONTEXT_MODULE, $cm->id);
 -    require_capability('mod/quiz:view', $context);
 -
 -/// Cache some other capabilities we use several times.
 -    $canattempt = has_capability('mod/quiz:attempt', $context);
 -    $canreviewmine = has_capability('mod/quiz:reviewmyattempts', $context);
 -    $canpreview = has_capability('mod/quiz:preview', $context);
 -
 -/// Create an object to manage all the other (non-roles) access rules.
 -    $timenow = time();
 -    $accessmanager = new quiz_access_manager(quiz::create($quiz->id, $USER->id), $timenow,
 -            has_capability('mod/quiz:ignoretimelimits', $context, NULL, false));
 -
 -/// If no questions have been set up yet redirect to edit.php
 -    if (!$quiz->questions && has_capability('mod/quiz:manage', $context)) {
 -        redirect($CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $cm->id);
 -    }
 -
 -/// Log this request.
 -    add_to_log($course->id, "quiz", "view", "view.php?id=$cm->id", $quiz->id, $cm->id);
 -
 -    // Mark module as viewed
 -    $completion = new completion_info($course);
 -    $completion->set_module_viewed($cm);
 -
 -/// Initialize $PAGE, compute blocks
 -    $PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
 -
 -    $edit = optional_param('edit', -1, PARAM_BOOL);
 -    if ($edit != -1 && $PAGE->user_allowed_editing()) {
 -        $USER->editing = $edit;
 -    }
 -
 -    $PAGE->requires->yui2_lib('event');
 -
 -    // Note: MDL-19010 there will be further changes to printing header and blocks.
 -    // The code will be much nicer than this eventually.
 -    $title = $course->shortname . ': ' . format_string($quiz->name);
 -
 -    if ($PAGE->user_allowed_editing()) {
 -        $buttons = '<table><tr><td><form method="get" action="view.php"><div>'.
 -            '<input type="hidden" name="id" value="'.$cm->id.'" />'.
 -            '<input type="hidden" name="edit" value="'.($PAGE->user_is_editing()?'off':'on').'" />'.
 -            '<input type="submit" value="'.get_string($PAGE->user_is_editing()?'blockseditoff':'blocksediton').'" /></div></form></td></tr></table>';
 -        $PAGE->set_button($buttons);
 -    }
 -
 -    $PAGE->set_title($title);
 -    $PAGE->set_heading($course->fullname);
 -
 -    echo $OUTPUT->header();
 -
 -/// Print quiz name and description
 -    echo $OUTPUT->heading(format_string($quiz->name));
 -    if (trim(strip_tags($quiz->intro))) {
 -        echo $OUTPUT->box(format_module_intro('quiz', $quiz, $cm->id), 'generalbox', 'intro');
 -    }
 -
 -/// Display information about this quiz.
 -    $messages = $accessmanager->describe_rules();
 -    if ($quiz->attempts != 1) {
 -        $messages[] = get_string('gradingmethod', 'quiz', quiz_get_grading_option_name($quiz->grademethod));
 -    }
 -    echo $OUTPUT->box_start('quizinfo');
 -    $accessmanager->print_messages($messages);
 -    echo $OUTPUT->box_end();
 -
 -/// Show number of attempts summary to those who can view reports.
 -    if (has_capability('mod/quiz:viewreports', $context)) {
 -        if ($strattemptnum = quiz_attempt_summary_link_to_reports($quiz, $cm, $context)) {
 -            echo '<div class="quizattemptcounts">' . $strattemptnum . "</div>\n";
 -        }
 -    }
 -
 -/// Guests can't do a quiz, so offer them a choice of logging in or going back.
 -    if (isguestuser()) {
 -        echo $OUTPUT->confirm('<p>' . get_string('guestsno', 'quiz') . "</p>\n\n<p>" .
 -                get_string('liketologin') . "</p>\n", get_login_url(), get_referer(false));
 -        echo $OUTPUT->footer();
 -        exit;
 -    }
 -
 -/// If they are not enrolled in this course in a good enough role, tell them to enrol.
 -    if (!($canattempt || $canpreview || $canreviewmine)) {
 -        echo $OUTPUT->box('<p>' . get_string('youneedtoenrol', 'quiz') . "</p>\n\n<p>" .
 -                $OUTPUT->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id) .
 -                "</p>\n", 'generalbox', 'notice');
 -        echo $OUTPUT->footer();
 -        exit;
 -    }
 -
 -/// Update the quiz with overrides for the current user
 -    $quiz = quiz_update_effective_access($quiz, $USER->id);
 -
 -/// Get this user's attempts.
 -    $attempts = quiz_get_user_attempts($quiz->id, $USER->id);
 -    $lastfinishedattempt = end($attempts);
 -    $unfinished = false;
 -    if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
 -        $attempts[] = $unfinishedattempt;
 -        $unfinished = true;
 -    }
 -    $numattempts = count($attempts);
 -
 -/// Work out the final grade, checking whether it was overridden in the gradebook.
 -    $mygrade = quiz_get_best_grade($quiz, $USER->id);
 -    $mygradeoverridden = false;
 -    $gradebookfeedback = '';
 -
 -    $grading_info = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $USER->id);
 -    if (!empty($grading_info->items)) {
 -        $item = $grading_info->items[0];
 -        if (isset($item->grades[$USER->id])) {
 -            $grade = $item->grades[$USER->id];
 -
 -            if ($grade->overridden) {
 -                $mygrade = $grade->grade + 0; // Convert to number.
 -                $mygradeoverridden = true;
 -            }
 -            if (!empty($grade->str_feedback)) {
 -                $gradebookfeedback = $grade->str_feedback;
 -            }
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 +
 +/**
 + * This page is the entry page into the quiz UI. Displays information about the
 + * quiz to students and teachers, and lets students see their previous attempts.
 + *
 + * @package    mod
 + * @subpackage quiz
 + * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +
 +
 +require_once(dirname(__FILE__) . '/../../config.php');
 +require_once($CFG->libdir.'/gradelib.php');
 +require_once($CFG->dirroot.'/mod/quiz/locallib.php');
 +require_once($CFG->libdir . '/completionlib.php');
 +
 +$id = optional_param('id', 0, PARAM_INT); // Course Module ID, or
 +$q = optional_param('q',  0, PARAM_INT);  // quiz ID
 +
 +if ($id) {
 +    if (!$cm = get_coursemodule_from_id('quiz', $id)) {
 +        print_error('invalidcoursemodule');
 +    }
 +    if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
 +        print_error('coursemisconf');
 +    }
 +    if (!$quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
 +        print_error('invalidcoursemodule');
 +    }
 +} else {
 +    if (!$quiz = $DB->get_record('quiz', array('id' => $q))) {
 +        print_error('invalidquizid', 'quiz');
 +    }
 +    if (!$course = $DB->get_record('course', array('id' => $quiz->course))) {
 +        print_error('invalidcourseid');
 +    }
 +    if (!$cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
 +        print_error('invalidcoursemodule');
 +    }
 +}
 +
 +// Check login and get context.
 +require_login($course->id, false, $cm);
 +$context = get_context_instance(CONTEXT_MODULE, $cm->id);
 +require_capability('mod/quiz:view', $context);
 +
 +// Cache some other capabilities we use several times.
 +$canattempt = has_capability('mod/quiz:attempt', $context);
 +$canreviewmine = has_capability('mod/quiz:reviewmyattempts', $context);
 +$canpreview = has_capability('mod/quiz:preview', $context);
 +
 +// Create an object to manage all the other (non-roles) access rules.
 +$timenow = time();
 +$accessmanager = new quiz_access_manager(quiz::create($quiz->id, $USER->id), $timenow,
 +        has_capability('mod/quiz:ignoretimelimits', $context, null, false));
 +
 +// Log this request.
 +add_to_log($course->id, 'quiz', 'view', 'view.php?id=' . $cm->id, $quiz->id, $cm->id);
 +
++$completion = new completion_info($course);
++$completion->set_module_viewed($cm);
++
 +// Initialize $PAGE, compute blocks
 +$PAGE->set_url('/mod/quiz/view.php', array('id' => $cm->id));
 +
 +$edit = optional_param('edit', -1, PARAM_BOOL);
 +if ($edit != -1 && $PAGE->user_allowed_editing()) {
 +    $USER->editing = $edit;
 +}
 +
 +// Update the quiz with overrides for the current user
 +$quiz = quiz_update_effective_access($quiz, $USER->id);
 +
 +// Get this user's attempts.
 +$attempts = quiz_get_user_attempts($quiz->id, $USER->id, 'finished', true);
 +$lastfinishedattempt = end($attempts);
 +$unfinished = false;
 +if ($unfinishedattempt = quiz_get_user_attempt_unfinished($quiz->id, $USER->id)) {
 +    $attempts[] = $unfinishedattempt;
 +    $unfinished = true;
 +}
 +$numattempts = count($attempts);
 +
 +// Work out the final grade, checking whether it was overridden in the gradebook.
 +$mygrade = quiz_get_best_grade($quiz, $USER->id);
 +$mygradeoverridden = false;
 +$gradebookfeedback = '';
 +
 +$grading_info = grade_get_grades($course->id, 'mod', 'quiz', $quiz->id, $USER->id);
 +if (!empty($grading_info->items)) {
 +    $item = $grading_info->items[0];
 +    if (isset($item->grades[$USER->id])) {
 +        $grade = $item->grades[$USER->id];
 +
 +        if ($grade->overridden) {
 +            $mygrade = $grade->grade + 0; // Convert to number.
 +            $mygradeoverridden = true;
 +        }
 +        if (!empty($grade->str_feedback)) {
 +            $gradebookfeedback = $grade->str_feedback;
 +        }
 +    }
 +}
 +
 +$title = $course->shortname . ': ' . format_string($quiz->name);
 +$PAGE->set_title($title);
 +$PAGE->set_heading($course->fullname);
 +$output = $PAGE->get_renderer('mod_quiz');
 +
 +/*
 + * Create view object for use within renderers file
 + */
 +$viewobj = new mod_quiz_view_object();
 +$viewobj->attempts = $attempts;
 +$viewobj->accessmanager = $accessmanager;
 +$viewobj->canattempt = $canattempt;
 +$viewobj->canpreview = $canpreview;
 +$viewobj->canreviewmine = $canreviewmine;
 +
 +// Print table with existing attempts
 +if ($attempts) {
 +    // Work out which columns we need, taking account what data is available in each attempt.
 +    list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
 +
 +    $viewobj->attemptcolumn = $quiz->attempts != 1;
 +
 +    $viewobj->gradecolumn = $someoptions->marks >= question_display_options::MARK_AND_MAX &&
 +            quiz_has_grades($quiz);
 +    $viewobj->markcolumn = $viewobj->gradecolumn && ($quiz->grade != $quiz->sumgrades);
 +    $viewobj->overallstats = $alloptions->marks >= question_display_options::MARK_AND_MAX;
 +
 +    $viewobj->feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback;
 +} else {
 +    $viewobj->attemptcolumn = 1;
 +}
 +
 +$moreattempts = $unfinished || !$accessmanager->is_finished($numattempts, $lastfinishedattempt);
 +
 +$viewobj->timenow = $timenow;
 +$viewobj->numattempts = $numattempts;
 +$viewobj->mygrade = $mygrade;
 +$viewobj->moreattempts = $moreattempts;
 +$viewobj->mygradeoverridden = $mygradeoverridden;
 +$viewobj->gradebookfeedback = $gradebookfeedback;
 +$viewobj->unfinished = $unfinished;
 +$viewobj->lastfinishedattempt = $lastfinishedattempt;
 +
 +// Display information about this quiz.
 +$messages = $viewobj->accessmanager->describe_rules();
 +if ($quiz->attempts != 1) {
 +    $messages[] = get_string('gradingmethod', 'quiz',
 +            quiz_get_grading_option_name($quiz->grademethod));
 +}
 +
 +// This will be set something if as start/continue attempt button should appear.
 +$buttontext = '';
 +if (!quiz_clean_layout($quiz->questions, true)) {
-     $output .= quiz_no_questions_message($quiz, $cm, $context);
 +    $buttontext = '';
 +
 +} else {
 +    if ($viewobj->unfinished) {
 +        if ($viewobj->canattempt) {
 +            $buttontext = get_string('continueattemptquiz', 'quiz');
 +        } else if ($viewobj->canpreview) {
 +            $buttontext = get_string('continuepreview', 'quiz');
          }
 -    }
 -
 -/// Print table with existing attempts
 -    if ($attempts) {
 -
 -        echo $OUTPUT->heading(get_string('summaryofattempts', 'quiz'));
 -
 -        // Work out which columns we need, taking account what data is available in each attempt.
 -        list($someoptions, $alloptions) = quiz_get_combined_reviewoptions($quiz, $attempts, $context);
 -
 -        $attemptcolumn = $quiz->attempts != 1;
 -
 -        $gradecolumn = $someoptions->scores && quiz_has_grades($quiz);
 -        $markcolumn = $gradecolumn && ($quiz->grade != $quiz->sumgrades);
 -        $overallstats = $alloptions->scores;
  
 -        $feedbackcolumn = quiz_has_feedback($quiz) && $alloptions->overallfeedback;
 -
 -        // Prepare table header
 -        $table = new html_table();
 -        $table->attributes['class'] = 'generaltable quizattemptsummary';
 -        $table->head = array();
 -        $table->align = array();
 -        $table->size = array();
 -        if ($attemptcolumn) {
 -            $table->head[] = get_string('attemptnumber', 'quiz');
 -            $table->align[] = 'center';
 -            $table->size[] = '';
 -        }
 -        $table->head[] = get_string('timecompleted', 'quiz');
 -        $table->align[] = 'left';
 -        $table->size[] = '';
 -        if ($markcolumn) {
 -            $table->head[] = get_string('marks', 'quiz') . ' / ' . quiz_format_grade($quiz, $quiz->sumgrades);
 -            $table->align[] = 'center';
 -            $table->size[] = '';
 -        }
 -        if ($gradecolumn) {
 -            $table->head[] = get_string('grade') . ' / ' . quiz_format_grade($quiz, $quiz->grade);
 -            $table->align[] = 'center';
 -            $table->size[] = '';
 -        }
 -        if ($canreviewmine) {
 -            $table->head[] = get_string('review', 'quiz');
 -            $table->align[] = 'center';
 -            $table->size[] = '';
 -        }
 -        if ($feedbackcolumn) {
 -            $table->head[] = get_string('feedback', 'quiz');
 -            $table->align[] = 'left';
 -            $table->size[] = '';
 -        }
 -        if (isset($quiz->showtimetaken)) {
 -            $table->head[] = get_string('timetaken', 'quiz');
 -            $table->align[] = 'left';
 -            $table->size[] = '';
 -        }
 -
 -        // One row for each attempt
 -        foreach ($attempts as $attempt) {
 -            $attemptoptions = quiz_get_reviewoptions($quiz, $attempt, $context);
 -            $row = array();
 -
 -            // Add the attempt number, making it a link, if appropriate.
 -            if ($attemptcolumn) {
 -                if ($attempt->preview) {
 -                    $row[] = get_string('preview', 'quiz');
 -                } else {
 -                    $row[] = $attempt->attempt;
 -                }
 -            }
 -
 -            // prepare strings for time taken and date completed
 -            $timetaken = '';
 -            $datecompleted = '';
 -            if ($attempt->timefinish > 0) {
 -                // attempt has finished
 -                $timetaken = format_time($attempt->timefinish - $attempt->timestart);
 -                $datecompleted = userdate($attempt->timefinish);
 -            } else if (!$quiz->timeclose || $timenow < $quiz->timeclose) {
 -                // The attempt is still in progress.
 -                $timetaken = format_time($timenow - $attempt->timestart);
 -                $datecompleted = '';
 -            } else {
 -                $timetaken = format_time($quiz->timeclose - $attempt->timestart);
 -                $datecompleted = userdate($quiz->timeclose);
 -            }
 -            $row[] = $datecompleted;
 -
 -            if ($markcolumn) {
 -                if ($attemptoptions->scores && $attempt->timefinish > 0) {
 -                    $row[] = quiz_format_grade($quiz, $attempt->sumgrades);
 -                } else {
 -                    $row[] = '';
 -                }
 -            }
 -
 -            // Ouside the if because we may be showing feedback but not grades.
 -            $attemptgrade = quiz_rescale_grade($attempt->sumgrades, $quiz, false);
 -
 -            if ($gradecolumn) {
 -                if ($attemptoptions->scores && $attempt->timefinish > 0) {
 -                    $formattedgrade = quiz_format_grade($quiz, $attemptgrade);
 -                    // highlight the highest grade if appropriate
 -                    if ($overallstats && !$attempt->preview && $numattempts > 1 && !is_null($mygrade) &&
 -                            $attemptgrade == $mygrade && $quiz->grademethod == QUIZ_GRADEHIGHEST) {
 -                        $table->rowclasses[$attempt->attempt] = 'bestrow';
 -                    }
 -
 -                    $row[] = $formattedgrade;
 -                } else {
 -                    $row[] = '';
 -                }
 -            }
 -
 -            if ($canreviewmine) {
 -                $row[] = $accessmanager->make_review_link($attempt, $canpreview, $attemptoptions);
 -            }
 -
 -            if ($feedbackcolumn && $attempt->timefinish > 0) {
 -                if ($attemptoptions->overallfeedback) {
 -                    $row[] = quiz_feedback_for_grade($attemptgrade, $quiz, $context, $cm);
 -                } else {
 -                    $row[] = '';
 -                }
 -            }
 -
 -            if (isset($quiz->showtimetaken)) {
 -                $row[] = $timetaken;
 -            }
 -
 -            if ($attempt->preview) {
 -                $table->data['preview'] = $row;
 -            } else {
 -                $table->data[$attempt->attempt] = $row;
 -            }
 -        } // End of loop over attempts.
 -        echo html_writer::table($table);
 -    }
 -
 -/// Print information about the student's best score for this quiz if possible.
 -    $moreattempts = $unfinished || !$accessmanager->is_finished($numattempts, $lastfinishedattempt);
 -    if (!$moreattempts) {
 -        echo $OUTPUT->heading(get_string("nomoreattempts", "quiz"));
 -    }
 -
 -    if ($numattempts && $gradecolumn && !is_null($mygrade)) {
 -        $resultinfo = '';
 -
 -        if ($overallstats) {
 -            if ($moreattempts) {
 -                $a = new stdClass;
 -                $a->method = quiz_get_grading_option_name($quiz->grademethod);
 -                $a->mygrade = quiz_format_grade($quiz, $mygrade);
 -                $a->quizgrade = quiz_format_grade($quiz, $quiz->grade);
 -                $resultinfo .= $OUTPUT->heading(get_string('gradesofar', 'quiz', $a), 2, 'main');
 -            } else {
 -                $a = new stdClass;
 -                $a->grade = quiz_format_grade($quiz, $mygrade);
 -                $a->maxgrade = quiz_format_grade($quiz, $quiz->grade);
 -                $a = get_string('outofshort', 'quiz', $a);
 -                $resultinfo .= $OUTPUT->heading(get_string('yourfinalgradeis', 'quiz', $a), 2, 'main');
 -            }
 -        }
 -
 -        if ($mygradeoverridden) {
 -            $resultinfo .= '<p class="overriddennotice">'.get_string('overriddennotice', 'grades')."</p>\n";
 -        }
 -        if ($gradebookfeedback) {
 -            $resultinfo .= $OUTPUT->heading(get_string('comment', 'quiz'), 3, 'main');
 -            $resultinfo .= '<p class="quizteacherfeedback">'.$gradebookfeedback."</p>\n";
 -        }
 -        if ($feedbackcolumn) {
 -            $resultinfo .= $OUTPUT->heading(get_string('overallfeedback', 'quiz'), 3, 'main');
 -            $resultinfo .= '<p class="quizgradefeedback">'.quiz_feedback_for_grade($mygrade, $quiz, $context, $cm)."</p>\n";
 -        }
 -
 -        if ($resultinfo) {
 -            echo $OUTPUT->box($resultinfo, 'generalbox', 'feedback');
 -        }
 -    }
 -
 -/// Determine if we should be showing a start/continue attempt button,
 -/// or a button to go back to the course page.
 -    echo $OUTPUT->box_start('quizattempt');
 -    $buttontext = ''; // This will be set something if as start/continue attempt button should appear.
 -    if (!quiz_clean_layout($quiz->questions, true)) {
 -        echo $OUTPUT->heading(get_string("noquestions", "quiz"));
      } else {
 -        if ($unfinished) {
 -            if ($canpreview) {
 -                $buttontext = get_string('continuepreview', 'quiz');
 -            } else if ($canattempt) {
 -                $buttontext = get_string('continueattemptquiz', 'quiz');
 -            }
 -        } else {
 -            if ($canpreview) {
 -                $buttontext = get_string('previewquiznow', 'quiz');
 -            } else if ($canattempt) {
 -                $messages = $accessmanager->prevent_new_attempt($numattempts, $lastfinishedattempt);
 -                if ($messages) {
 -                    $accessmanager->print_messages($messages);
 -                } else if ($numattempts == 0) {
 -                    $buttontext = get_string('attemptquiznow', 'quiz');
 -                } else {
 -                    $buttontext = get_string('reattemptquiz', 'quiz');
 -                }
 +        if ($viewobj->canattempt) {
 +            $messages = $viewobj->accessmanager->prevent_new_attempt($viewobj->numattempts,
 +                    $viewobj->lastfinishedattempt);
 +            if ($messages) {
 +                $this->access_messages($messages);
 +            } else if ($viewobj->numattempts == 0) {
 +                $buttontext = get_string('attemptquiznow', 'quiz');
 +            } else {
 +                $buttontext = get_string('reattemptquiz', 'quiz');
              }
 -        }
  
 -        // If, so far, we think a button should be printed, so check if they will be allowed to access it.
 -        if ($buttontext) {
 -            if (!$moreattempts) {
 -                $buttontext = '';
 -            } else if ($canattempt && $messages = $accessmanager->prevent_access()) {
 -                $accessmanager->print_messages($messages);
 -                $buttontext = '';
 -            }
 +        } else if ($viewobj->canpreview) {
 +            $buttontext = get_string('previewquiznow', 'quiz');
          }
      }
  
 -/// Now actually print the appropriate button.
 +    // If, so far, we think a button should be printed, so check if they will be
 +    // allowed to access it.
      if ($buttontext) {
 -        $accessmanager->print_start_attempt_button($canpreview, $buttontext, $unfinished);
 -    } else {
 -        echo $OUTPUT->continue_button($CFG->wwwroot . '/course/view.php?id=' . $course->id);
 -    }
 -    echo $OUTPUT->box_end();
 -
 -    echo $OUTPUT->footer();
 +        if (!$viewobj->moreattempts) {
 +            $buttontext = '';
 +        } else if ($viewobj->canattempt
 +                && $messages = $viewobj->accessmanager->prevent_access()) {
 +            $this->access_messages($messages);
 +            $buttontext = '';
 +        }
 +    }
 +}
 +
 +echo $OUTPUT->header();
 +
 +// Guests can't do a quiz, so offer them a choice of logging in or going back.
 +if (isguestuser()) {
 +    echo $output->view_page_guest($course, $quiz, $cm, $context, $messages, $viewobj);
 +} else if (!isguestuser() && !($viewobj->canattempt || $viewobj->canpreview
 +          || $viewobj->canreviewmine)) {
 +    // If they are not enrolled in this course in a good enough role, tell them to enrol.
 +    echo $output->view_page_notenrolled($course, $quiz, $cm, $context, $messages, $viewobj);
 +} else {
 +    echo $output->view_page($course, $quiz, $cm, $context, $messages, $viewobj, $buttontext);
 +}
 +
- // Mark module as viewed (note, we do this here and not in finish_page,
- // otherwise the 'not enrolled' error conditions would result in marking
- // 'viewed', I think it's better if they don't.)
- $completion = new completion_info($course);
- $completion->set_module_viewed($cm);
 +echo $OUTPUT->footer();
Simple merge
index e01240c,0000000..f583751
mode 100644,000000..100644
--- /dev/null
@@@ -1,343 -1,0 +1,343 @@@
-         list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc0000');
 +<?php
 +
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 +
 +/**
 + * More object oriented wrappers around parts of the Moodle question bank.
 + *
 + * In due course, I expect that the question bank will be converted to a
 + * fully object oriented structure, at which point this file can be a
 + * starting point.
 + *
 + * @package    moodlecore
 + * @subpackage questionbank
 + * @copyright  2009 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +
 +
 +defined('MOODLE_INTERNAL') || die();
 +
 +
 +/**
 + * This static class provides access to the other question bank.
 + *
 + * It provides functions for managing question types and question definitions.
 + *
 + * @copyright  2009 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +abstract class question_bank {
 +    /** @var array question type name => question_type subclass. */
 +    private static $questiontypes = array();
 +
 +    /** @var array question type name => 1. Records which question definitions have been loaded. */
 +    private static $loadedqdefs = array();
 +
 +    protected static $questionfinder = null;
 +
 +    /** @var boolean nasty hack to allow unit tests to call {@link load_question()}. */
 +    private static $testmode = false;
 +    private static $testdata = array();
 +
 +    private static $questionconfig = null;
 +
 +    /**
 +     * @param string $qtypename a question type name, e.g. 'multichoice'.
 +     * @return bool whether that question type is installed in this Moodle.
 +     */
 +    public static function is_qtype_installed($qtypename) {
 +        $plugindir = get_plugin_directory('qtype', $qtypename);
 +        return $plugindir && is_readable($plugindir . '/questiontype.php');
 +    }
 +
 +    /**
 +     * Get the question type class for a particular question type.
 +     * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 +     * @param bool $mustexist if false, the missing question type is returned when
 +     *      the requested question type is not installed.
 +     * @return question_type the corresponding question type class.
 +     */
 +    public static function get_qtype($qtypename, $mustexist = true) {
 +        global $CFG;
 +        if (isset(self::$questiontypes[$qtypename])) {
 +            return self::$questiontypes[$qtypename];
 +        }
 +        $file = get_plugin_directory('qtype', $qtypename) . '/questiontype.php';
 +        if (!is_readable($file)) {
 +            if ($mustexist || $qtypename == 'missingtype') {
 +                throw new coding_exception('Unknown question type ' . $qtypename);
 +            } else {
 +                return self::get_qtype('missingtype');
 +            }
 +        }
 +        include_once($file);
 +        $class = 'qtype_' . $qtypename;
 +        if (!class_exists($class)) {
 +            throw new coding_exception("Class $class must be defined in $file");
 +        }
 +        self::$questiontypes[$qtypename] = new $class();
 +        return self::$questiontypes[$qtypename];
 +    }
 +
 +    /**
 +     * Load the question configuration data from config_plugins.
 +     * @return object get_config('question') with caching.
 +     */
 +    protected static function get_config() {
 +        if (is_null(self::$questionconfig)) {
 +            $questionconfig = get_config('question');
 +        }
 +        return $questionconfig;
 +    }
 +
 +    /**
 +     * @param string $qtypename the internal name of a question type. For example multichoice.
 +     * @return bool whether users are allowed to create questions of this type.
 +     */
 +    public static function qtype_enabled($qtypename) {
 +        $config = self::get_config();
 +        $enabledvar = $qtypename . '_disabled';
 +        return self::qtype_exists($qtypename) && empty($config->$enabledvar) &&
 +                self::get_qtype($qtypename)->menu_name() != '';
 +    }
 +
 +    /**
 +     * @param string $qtypename the internal name of a question type. For example multichoice.
 +     * @return bool whether this question type exists.
 +     */
 +    public static function qtype_exists($qtypename) {
 +        return array_key_exists($qtypename, get_plugin_list('qtype'));
 +    }
 +
 +    /**
 +     * @param $qtypename the internal name of a question type, for example multichoice.
 +     * @return string the human_readable name of this question type, from the language pack.
 +     */
 +    public static function get_qtype_name($qtypename) {
 +        return self::get_qtype($qtypename)->local_name();
 +    }
 +
 +    /**
 +     * @return array all the installed question types.
 +     */
 +    public static function get_all_qtypes() {
 +        $qtypes = array();
 +        foreach (get_plugin_list('qtype') as $plugin => $notused) {
 +            try {
 +                $qtypes[$plugin] = self::get_qtype($plugin);
 +            } catch (coding_exception $e) {
 +                // Catching coding_exceptions here means that incompatible
 +                // question types do not cause the rest of Moodle to break.
 +            }
 +        }
 +        return $qtypes;
 +    }
 +
 +    /**
 +     * Sort an array of question types according to the order the admin set up,
 +     * and then alphabetically for the rest.
 +     * @param array qtype->name() => qtype->local_name().
 +     * @return array sorted array.
 +     */
 +    public static function sort_qtype_array($qtypes, $config = null) {
 +        if (is_null($config)) {
 +            $config = self::get_config();
 +        }
 +
 +        $sortorder = array();
 +        $otherqtypes = array();
 +        foreach ($qtypes as $name => $localname) {
 +            $sortvar = $name . '_sortorder';
 +            if (isset($config->$sortvar)) {
 +                $sortorder[$config->$sortvar] = $name;
 +            } else {
 +                $otherqtypes[$name] = $localname;
 +            }
 +        }
 +
 +        ksort($sortorder);
 +        textlib_get_instance()->asort($otherqtypes);
 +
 +        $sortedqtypes = array();
 +        foreach ($sortorder as $name) {
 +            $sortedqtypes[$name] = $qtypes[$name];
 +        }
 +        foreach ($otherqtypes as $name => $notused) {
 +            $sortedqtypes[$name] = $qtypes[$name];
 +        }
 +        return $sortedqtypes;
 +    }
 +
 +    /**
 +     * @return array all the question types that users are allowed to create,
 +     *      sorted into the preferred order set on the admin screen.
 +     */
 +    public static function get_creatable_qtypes() {
 +        $config = self::get_config();
 +        $allqtypes = self::get_all_qtypes();
 +
 +        $qtypenames = array();
 +        foreach ($allqtypes as $name => $qtype) {
 +            if (self::qtype_enabled($name)) {
 +                $qtypenames[$name] = $qtype->local_name();
 +            }
 +        }
 +
 +        $qtypenames = self::sort_qtype_array($qtypenames);
 +
 +        $creatableqtypes = array();
 +        foreach ($qtypenames as $name => $notused) {
 +            $creatableqtypes[$name] = $allqtypes[$name];
 +        }
 +        return $creatableqtypes;
 +    }
 +
 +    /**
 +     * Load the question definition class(es) belonging to a question type. That is,
 +     * include_once('/question/type/' . $qtypename . '/question.php'), with a bit
 +     * of checking.
 +     * @param string $qtypename the question type name. For example 'multichoice' or 'shortanswer'.
 +     */
 +    public static function load_question_definition_classes($qtypename) {
 +        global $CFG;
 +        if (isset(self::$loadedqdefs[$qtypename])) {
 +            return;
 +        }
 +        $file = $CFG->dirroot . '/question/type/' . $qtypename . '/question.php';
 +        if (!is_readable($file)) {
 +            throw new coding_exception('Unknown question type (no definition) ' . $qtypename);
 +        }
 +        include_once($file);
 +        self::$loadedqdefs[$qtypename] = 1;
 +    }
 +
 +    /**
 +     * Load a question definition from the database. The object returned
 +     * will actually be of an appropriate {@link question_definition} subclass.
 +     * @param int $questionid the id of the question to load.
 +     * @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
 +     * @return question_definition loaded from the database.
 +     */
 +    public static function load_question($questionid, $allowshuffle = true) {
 +        global $DB;
 +
 +        if (self::$testmode) {
 +            // Evil, test code in production, but now way round it.
 +            return self::return_test_question_data($questionid);
 +        }
 +
 +        $questiondata = $DB->get_record_sql('
 +                SELECT q.*, qc.contextid
 +                FROM {question} q
 +                JOIN {question_categories} qc ON q.category = qc.id
 +                WHERE q.id = :id', array('id' => $questionid), MUST_EXIST);
 +        get_question_options($questiondata);
 +        if (!$allowshuffle) {
 +            $questiondata->options->shuffleanswers = false;
 +        }
 +        return self::make_question($questiondata);
 +    }
 +
 +    /**
 +     * Convert the question information loaded with {@link get_question_options()}
 +     * to a question_definintion object.
 +     * @param object $questiondata raw data loaded from the database.
 +     * @return question_definition loaded from the database.
 +     */
 +    public static function make_question($questiondata) {
 +        return self::get_qtype($questiondata->qtype, false)->make_question($questiondata, false);
 +    }
 +
 +    /**
 +     * @return question_finder a question finder.
 +     */
 +    public static function get_finder() {
 +        if (is_null(self::$questionfinder)) {
 +            self::$questionfinder = new question_finder();
 +        }
 +        return self::$questionfinder;
 +    }
 +
 +    /**
 +     * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
 +     */
 +    public static function start_unit_test() {
 +        self::$testmode = true;
 +    }
 +
 +    /**
 +     * Only to be called from unit tests. Allows {@link load_test_data()} to be used.
 +     */
 +    public static function end_unit_test() {
 +        self::$testmode = false;
 +        self::$testdata = array();
 +    }
 +
 +    private static function return_test_question_data($questionid) {
 +        if (!isset(self::$testdata[$questionid])) {
 +            throw new coding_exception('question_bank::return_test_data(' . $questionid .
 +                    ') called, but no matching question has been loaded by load_test_data.');
 +        }
 +        return self::$testdata[$questionid];
 +    }
 +
 +    /**
 +     * To be used for unit testing only. Will throw an exception if
 +     * {@link start_unit_test()} has not been called first.
 +     * @param object $questiondata a question data object to put in the test data store.
 +     */
 +    public static function load_test_question_data(question_definition $question) {
 +        if (!self::$testmode) {
 +            throw new coding_exception('question_bank::load_test_data called when not in test mode.');
 +        }
 +        self::$testdata[$question->id] = $question;
 +    }
 +}
 +
 +
 +/**
 + * Class for loading questions according to various criteria.
 + *
 + * @copyright  2009 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +class question_finder {
 +    /**
 +     * Get the ids of all the questions in a list of categoryies.
 +     * @param array $categoryids either a categoryid, or a comma-separated list
 +     *      category ids, or an array of them.
 +     * @param string $extraconditions extra conditions to AND with the rest of
 +     *      the where clause. Must use named parameters.
 +     * @param array $extraparams any parameters used by $extraconditions.
 +     * @return array questionid => questionid.
 +     */
 +    public function get_questions_from_categories($categoryids, $extraconditions, $extraparams = array()) {
 +        global $DB;
 +
++        list($qcsql, $qcparams) = $DB->get_in_or_equal($categoryids, SQL_PARAMS_NAMED, 'qc');
 +
 +        if ($extraconditions) {
 +            $extraconditions = ' AND (' . $extraconditions . ')';
 +        }
 +
 +        return $DB->get_records_select_menu('question',
 +                "category $qcsql
 +                 AND parent = 0
 +                 AND hidden = 0
 +                 $extraconditions", $qcparams + $extraparams, '', 'id,id AS id2');
 +    }
 +}
index f903d98,0000000..8a82371
mode 100644,000000..100644
--- /dev/null
@@@ -1,1236 -1,0 +1,1237 @@@
-         list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
 +<?php
 +
 +// This file is part of Moodle - http://moodle.org/
 +//
 +// Moodle is free software: you can redistribute it and/or modify
 +// it under the terms of the GNU General Public License as published by
 +// the Free Software Foundation, either version 3 of the License, or
 +// (at your option) any later version.
 +//
 +// Moodle is distributed in the hope that it will be useful,
 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +// GNU General Public License for more details.
 +//
 +// You should have received a copy of the GNU General Public License
 +// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 +
 +/**
 + * Code for loading and saving question attempts to and from the database.
 + *
 + * @package    moodlecore
 + * @subpackage questionengine
 + * @copyright  2009 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +
 +
 +defined('MOODLE_INTERNAL') || die();
 +
 +
 +/**
 + * This class controls the loading and saving of question engine data to and from
 + * the database.
 + *
 + * @copyright  2009 The Open University
 + * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 + */
 +class question_engine_data_mapper {
 +    /**
 +     * @var moodle_database normally points to global $DB, but I prefer not to
 +     * use globals if I can help it.
 +     */
 +    protected $db;
 +
 +    /**
 +     * @param moodle_database $db a database connectoin. Defaults to global $DB.
 +     */
 +    public function __construct($db = null) {
 +        if (is_null($db)) {
 +            global $DB;
 +            $this->db = $DB;
 +        } else {
 +            $this->db = $db;
 +        }
 +    }
 +
 +    /**
 +     * Store an entire {@link question_usage_by_activity} in the database,
 +     * including all the question_attempts that comprise it.
 +     * @param question_usage_by_activity $quba the usage to store.
 +     */
 +    public function insert_questions_usage_by_activity(question_usage_by_activity $quba) {
 +        $record = new stdClass();
 +        $record->contextid = $quba->get_owning_context()->id;
 +        $record->component = $quba->get_owning_component();
 +        $record->preferredbehaviour = $quba->get_preferred_behaviour();
 +
 +        $newid = $this->db->insert_record('question_usages', $record);
 +        $quba->set_id_from_database($newid);
 +
 +        foreach ($quba->get_attempt_iterator() as $qa) {
 +            $this->insert_question_attempt($qa, $quba->get_owning_context());
 +        }
 +    }
 +
 +    /**
 +     * Store an entire {@link question_attempt} in the database,
 +     * including all the question_attempt_steps that comprise it.
 +     * @param question_attempt $qa the question attempt to store.
 +     * @param object $context the context of the owning question_usage_by_activity.
 +     */
 +    public function insert_question_attempt(question_attempt $qa, $context) {
 +        $record = new stdClass();
 +        $record->questionusageid = $qa->get_usage_id();
 +        $record->slot = $qa->get_slot();
 +        $record->behaviour = $qa->get_behaviour_name();
 +        $record->questionid = $qa->get_question()->id;
 +        $record->maxmark = $qa->get_max_mark();
 +        $record->minfraction = $qa->get_min_fraction();
 +        $record->flagged = $qa->is_flagged();
 +        $record->questionsummary = $qa->get_question_summary();
 +        $record->rightanswer = $qa->get_right_answer_summary();
 +        $record->responsesummary = $qa->get_response_summary();
 +        $record->timemodified = time();
 +        $record->id = $this->db->insert_record('question_attempts', $record);
 +
 +        foreach ($qa->get_step_iterator() as $seq => $step) {
 +            $this->insert_question_attempt_step($step, $record->id, $seq, $context);
 +        }
 +    }
 +
 +    /**
 +     * Store a {@link question_attempt_step} in the database.
 +     * @param question_attempt_step $qa the step to store.
 +     * @param int $questionattemptid the question attept id this step belongs to.
 +     * @param int $seq the sequence number of this stop.
 +     * @param object $context the context of the owning question_usage_by_activity.
 +     */
 +    public function insert_question_attempt_step(question_attempt_step $step,
 +            $questionattemptid, $seq, $context) {
 +        $record = new stdClass();
 +        $record->questionattemptid = $questionattemptid;
 +        $record->sequencenumber = $seq;
 +        $record->state = '' . $step->get_state();
 +        $record->fraction = $step->get_fraction();
 +        $record->timecreated = $step->get_timecreated();
 +        $record->userid = $step->get_user_id();
 +
 +        $record->id = $this->db->insert_record('question_attempt_steps', $record);
 +
 +        foreach ($step->get_all_data() as $name => $value) {
 +            if ($value instanceof question_file_saver) {
 +                $value->save_files($record->id, $context);
 +            }
 +
 +            $data = new stdClass();
 +            $data->attemptstepid = $record->id;
 +            $data->name = $name;
 +            $data->value = $value;
 +            $this->db->insert_record('question_attempt_step_data', $data, false);
 +        }
 +    }
 +
 +    /**
 +     * Load a {@link question_attempt_step} from the database.
 +     * @param int $stepid the id of the step to load.
 +     * @param question_attempt_step the step that was loaded.
 +     */
 +    public function load_question_attempt_step($stepid) {
 +        $records = $this->db->get_records_sql("
 +SELECT
 +    COALESCE(qasd.id, -1 * qas.id) AS id,
 +    qas.id AS attemptstepid,
 +    qas.questionattemptid,
 +    qas.sequencenumber,
 +    qas.state,
 +    qas.fraction,
 +    qas.timecreated,
 +    qas.userid,
 +    qasd.name,
 +    qasd.value
 +
 +FROM {question_attempt_steps} qas
 +LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 +
 +WHERE
 +    qas.id = :stepid
 +        ", array('stepid' => $stepid));
 +
 +        if (!$records) {
 +            throw new coding_exception('Failed to load question_attempt_step ' . $stepid);
 +        }
 +
 +        return question_attempt_step::load_from_records($records, $stepid);
 +    }
 +
 +    /**
 +     * Load a {@link question_attempt} from the database, including all its
 +     * steps.
 +     * @param int $questionattemptid the id of the question attempt to load.
 +     * @param question_attempt the question attempt that was loaded.
 +     */
 +    public function load_question_attempt($questionattemptid) {
 +        $records = $this->db->get_records_sql("
 +SELECT
 +    COALESCE(qasd.id, -1 * qas.id) AS id,
 +    quba.contextid,
 +    quba.preferredbehaviour,
 +    qa.id AS questionattemptid,
 +    qa.questionusageid,
 +    qa.slot,
 +    qa.behaviour,
 +    qa.questionid,
 +    qa.maxmark,
 +    qa.minfraction,
 +    qa.flagged,
 +    qa.questionsummary,
 +    qa.rightanswer,
 +    qa.responsesummary,
 +    qa.timemodified,
 +    qas.id AS attemptstepid,
 +    qas.sequencenumber,
 +    qas.state,
 +    qas.fraction,
 +    qas.timecreated,
 +    qas.userid,
 +    qasd.name,
 +    qasd.value
 +
 +FROM      {question_attempts           qa
 +JOIN      {question_usages}            quba ON quba.id               = qa.questionusageid
 +LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 +LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 +
 +WHERE
 +    qa.id = :questionattemptid
 +
 +ORDER BY
 +    qas.sequencenumber
 +        ", array('questionattemptid' => $questionattemptid));
 +
 +        if (!$records) {
 +            throw new coding_exception('Failed to load question_attempt ' . $questionattemptid);
 +        }
 +
 +        $record = current($records);
 +        return question_attempt::load_from_records($records, $questionattemptid,
 +                new question_usage_null_observer(), $record->preferredbehaviour);
 +    }
 +
 +    /**
 +     * Load a {@link question_usage_by_activity} from the database, including
 +     * all its {@link question_attempt}s and all their steps.
 +     * @param int $qubaid the id of the usage to load.
 +     * @param question_usage_by_activity the usage that was loaded.
 +     */
 +    public function load_questions_usage_by_activity($qubaid) {
 +        $records = $this->db->get_records_sql("
 +SELECT
 +    COALESCE(qasd.id, -1 * qas.id) AS id,
 +    quba.id AS qubaid,
 +    quba.contextid,
 +    quba.component,
 +    quba.preferredbehaviour,
 +    qa.id AS questionattemptid,
 +    qa.questionusageid,
 +    qa.slot,
 +    qa.behaviour,
 +    qa.questionid,
 +    qa.maxmark,
 +    qa.minfraction,
 +    qa.flagged,
 +    qa.questionsummary,
 +    qa.rightanswer,
 +    qa.responsesummary,
 +    qa.timemodified,
 +    qas.id AS attemptstepid,
 +    qas.sequencenumber,
 +    qas.state,
 +    qas.fraction,
 +    qas.timecreated,
 +    qas.userid,
 +    qasd.name,
 +    qasd.value
 +
 +FROM      {question_usages}            quba
 +LEFT JOIN {question_attempts}          qa   ON qa.questionusageid    = quba.id
 +LEFT JOIN {question_attempt_steps}     qas  ON qas.questionattemptid = qa.id
 +LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid    = qas.id
 +
 +WHERE
 +    quba.id = :qubaid
 +
 +ORDER BY
 +    qa.slot,
 +    qas.sequencenumber
 +    ", array('qubaid' => $qubaid));
 +
 +        if (!$records) {
 +            throw new coding_exception('Failed to load questions_usage_by_activity ' . $qubaid);
 +        }
 +
 +        return question_usage_by_activity::load_from_records($records, $qubaid);
 +    }
 +
 +    /**
 +     * Load information about the latest state of each question from the database.
 +     *
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @param array $slots A list of slots for the questions you want to konw about.
 +     * @return array of records. See the SQL in this function to see the fields available.
 +     */
 +    public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
-         list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
++        list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 +
 +        $records = $this->db->get_records_sql("
 +SELECT
 +    qas.id,
 +    qa.id AS questionattemptid,
 +    qa.questionusageid,
 +    qa.slot,
 +    qa.behaviour,
 +    qa.questionid,
 +    qa.maxmark,
 +    qa.minfraction,
 +    qa.flagged,
 +    qa.questionsummary,
 +    qa.rightanswer,
 +    qa.responsesummary,
 +    qa.timemodified,
 +    qas.id AS attemptstepid,
 +    qas.sequencenumber,
 +    qas.state,
 +    qas.fraction,
 +    qas.timecreated,
 +    qas.userid
 +
 +FROM {$qubaids->from_question_attempts('qa')}
 +JOIN {question_attempt_steps} qas ON
 +        qas.id = {$this->latest_step_for_qa_subquery()}
 +
 +WHERE
 +    {$qubaids->where()} AND
 +    qa.slot $slottest
 +        ", $params + $qubaids->from_where_params());
 +
 +        return $records;
 +    }
 +
 +    /**
 +     * Load summary information about the state of each question in a group of
 +     * attempts. This is used, for example, by the quiz manual grading report,
 +     * to show how many attempts at each question need to be graded.
 +     *
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @param array $slots A list of slots for the questions you want to konw about.
 +     * @return array The array keys are slot,qestionid. The values are objects with
 +     * fields $slot, $questionid, $inprogress, $name, $needsgrading, $autograded,
 +     * $manuallygraded and $all.
 +     */
 +    public function load_questions_usages_question_state_summary(qubaid_condition $qubaids, $slots) {
-             list($slottest, $slotsparams) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000');
++        list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 +
 +        $rs = $this->db->get_recordset_sql("
 +SELECT
 +    qa.slot,
 +    qa.questionid,
 +    q.name,
 +    CASE qas.state
 +        {$this->full_states_to_summary_state_sql()}
 +    END AS summarystate,
 +    COUNT(1) AS numattempts
 +
 +FROM {$qubaids->from_question_attempts('qa')}
 +JOIN {question_attempt_steps} qas ON
 +        qas.id = {$this->latest_step_for_qa_subquery()}
 +JOIN {question} q ON q.id = qa.questionid
 +
 +WHERE
 +    {$qubaids->where()} AND
 +    qa.slot $slottest
 +
 +GROUP BY
 +    qa.slot,
 +    qa.questionid,
 +    q.name,
 +    q.id,
 +    summarystate
 +
 +ORDER BY
 +    qa.slot,
 +    qa.questionid,
 +    q.name,
 +    q.id
 +        ", $params + $qubaids->from_where_params());
 +
 +        $results = array();
 +        foreach ($rs as $row) {
 +            $index = $row->slot . ',' . $row->questionid;
 +
 +            if (!array_key_exists($index, $results)) {
 +                $res = new stdClass();
 +                $res->slot = $row->slot;
 +                $res->questionid = $row->questionid;
 +                $res->name = $row->name;
 +                $res->inprogress = 0;
 +                $res->needsgrading = 0;
 +                $res->autograded = 0;
 +                $res->manuallygraded = 0;
 +                $res->all = 0;
 +                $results[$index] = $res;
 +            }
 +
 +            $results[$index]->{$row->summarystate} = $row->numattempts;
 +            $results[$index]->all += $row->numattempts;
 +        }
 +        $rs->close();
 +
 +        return $results;
 +    }
 +
 +    /**
 +     * Get a list of usage ids where the question with slot $slot, and optionally
 +     * also with question id $questionid, is in summary state $summarystate. Also
 +     * return the total count of such states.
 +     *
 +     * Only a subset of the ids can be returned by using $orderby, $limitfrom and
 +     * $limitnum. A special value 'random' can be passed as $orderby, in which case
 +     * $limitfrom is ignored.
 +     *
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @param int $slot The slot for the questions you want to konw about.
 +     * @param int $questionid (optional) Only return attempts that were of this specific question.
 +     * @param string $summarystate the summary state of interest, or 'all'.
 +     * @param string $orderby the column to order by.
 +     * @param array $params any params required by any of the SQL fragments.
 +     * @param int $limitfrom implements paging of the results.
 +     *      Ignored if $orderby = random or $limitnum is null.
 +     * @param int $limitnum implements paging of the results. null = all.
 +     * @return array with two elements, an array of usage ids, and a count of the total number.
 +     */
 +    public function load_questions_usages_where_question_in_state(
 +            qubaid_condition $qubaids, $summarystate, $slot, $questionid = null,
 +            $orderby = 'random', $params, $limitfrom = 0, $limitnum = null) {
 +
 +        $extrawhere = '';
 +        if ($questionid) {
 +            $extrawhere .= ' AND qa.questionid = :questionid';
 +            $params['questionid'] = $questionid;
 +        }
 +        if ($summarystate != 'all') {
 +            list($test, $sparams) = $this->in_summary_state_test($summarystate);
 +            $extrawhere .= ' AND qas.state ' . $test;
 +            $params += $sparams;
 +        }
 +
 +        if ($orderby == 'random') {
 +            $sqlorderby = '';
 +        } else if ($orderby) {
 +            $sqlorderby = 'ORDER BY ' . $orderby;
 +        } else {
 +            $sqlorderby = '';
 +        }
 +
 +        // We always want the total count, as well as the partcular list of ids,
 +        // based on the paging and sort order. Becuase the list of ids is never
 +        // going to be too rediculously long. My worst-case scenario is
 +        // 10,000 students in the coures, each doing 5 quiz attempts. That
 +        // is a 50,000 element int => int array, which PHP seems to use 5MB
 +        // memeory to store on a 64 bit server.
 +        $params += $qubaids->from_where_params();
 +        $params['slot'] = $slot;
 +        $qubaids = $this->db->get_records_sql_menu("
 +SELECT
 +    qa.questionusageid,
 +    1
 +
 +FROM {$qubaids->from_question_attempts('qa')}
 +JOIN {question_attempt_steps} qas ON
 +        qas.id = {$this->latest_step_for_qa_subquery()}
 +JOIN {question} q ON q.id = qa.questionid
 +
 +WHERE
 +    {$qubaids->where()} AND
 +    qa.slot = :slot
 +    $extrawhere
 +
 +$sqlorderby
 +        ", $params);
 +
 +        $qubaids = array_keys($qubaids);
 +        $count = count($qubaids);
 +
 +        if ($orderby == 'random') {
 +            shuffle($qubaids);
 +            $limitfrom = 0;
 +        }
 +
 +        if (!is_null($limitnum)) {
 +            $qubaids = array_slice($qubaids, $limitfrom, $limitnum);
 +        }
 +
 +        return array($qubaids, $count);
 +    }
 +
 +    /**
 +     * Load a {@link question_usage_by_activity} from the database, including
 +     * all its {@link question_attempt}s and all their steps.
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @param array $slots if null, load info for all quesitions, otherwise only
 +     * load the averages for the specified questions.
 +     */
 +    public function load_average_marks(qubaid_condition $qubaids, $slots = null) {
 +        if (!empty($slots)) {
-                 question_state::$mangrright), SQL_PARAMS_NAMED, 'st00');
++            list($slottest, $slotsparams) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
 +            $slotwhere = " AND qa.slot $slottest";
 +        } else {
 +            $slotwhere = '';
 +            $params = array();
 +        }
 +
 +        list($statetest, $stateparams) = $this->db->get_in_or_equal(array(
 +                question_state::$gaveup,
 +                question_state::$gradedwrong,
 +                question_state::$gradedpartial,
 +                question_state::$gradedright,
 +                question_state::$mangaveup,
 +                question_state::$mangrwrong,
 +                question_state::$mangrpartial,
-                 WHERE id {$qubaids->usage_id_in()}", $params);
++                question_state::$mangrright), SQL_PARAMS_NAMED, 'st');
 +
 +        return $this->db->get_records_sql("
 +SELECT
 +    qa.slot,
 +    AVG(COALESCE(qas.fraction, 0)) AS averagefraction,
 +    COUNT(1) AS numaveraged
 +
 +FROM {$qubaids->from_question_attempts('qa')}
 +JOIN {question_attempt_steps} qas ON
 +        qas.id = {$this->latest_step_for_qa_subquery()}
 +
 +WHERE
 +    {$qubaids->where()}
 +    $slotwhere
 +    AND qas.state $statetest
 +
 +GROUP BY qa.slot
 +
 +ORDER BY qa.slot
 +        ", $slotsparams + $stateparams + $qubaids->from_where_params());
 +    }
 +
 +    /**
 +     * Load a {@link question_attempt} from the database, including all its
 +     * steps.
 +     * @param int $questionid the question to load all the attempts fors.
 +     * @param qubaid_condition $qubaids used to restrict which usages are included
 +     * in the query. See {@link qubaid_condition}.
 +     * @return array of question_attempts.
 +     */
 +    public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
 +        global $DB;
 +
 +        $params = $qubaids->from_where_params();
 +        $params['questionid'] = $questionid;
 +
 +        $records = $DB->get_records_sql("
 +SELECT
 +    COALESCE(qasd.id, -1 * qas.id) AS id,
 +    quba.contextid,
 +    quba.preferredbehaviour,
 +    qa.id AS questionattemptid,
 +    qa.questionusageid,
 +    qa.slot,
 +    qa.behaviour,
 +    qa.questionid,
 +    qa.maxmark,
 +    qa.minfraction,
 +    qa.flagged,
 +    qa.questionsummary,
 +    qa.rightanswer,
 +    qa.responsesummary,
 +    qa.timemodified,
 +    qas.id AS attemptstepid,
 +    qas.sequencenumber,
 +    qas.state,
 +    qas.fraction,
 +    qas.timecreated,
 +    qas.userid,
 +    qasd.name,
 +    qasd.value
 +
 +FROM {$qubaids->from_question_attempts('qa')}
 +JOIN {question_usages} quba ON quba.id = qa.questionusageid
 +LEFT JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 +LEFT JOIN {question_attempt_step_data} qasd ON qasd.attemptstepid = qas.id
 +
 +WHERE
 +    {$qubaids->where()} AND
 +    qa.questionid = :questionid
 +
 +ORDER BY
 +    quba.id,
 +    qa.id,
 +    qas.sequencenumber
 +        ", $params);
 +
 +        if (!$records) {
 +            return array();
 +        }
 +
 +        $questionattempts = array();
 +        $record = current($records);
 +        while ($record) {
 +            $questionattempts[$record->questionattemptid] =
 +                    question_attempt::load_from_records($records,
 +                    $record->questionattemptid, new question_usage_null_observer(),
 +                    $record->preferredbehaviour);
 +            $record = current($records);
 +        }
 +        return $questionattempts;
 +    }
 +
 +    /**
 +     * Update a question_usages row to refect any changes in a usage (but not
 +     * any of its question_attempts.
 +     * @param question_usage_by_activity $quba the usage that has changed.
 +     */
 +    public function update_questions_usage_by_activity(question_usage_by_activity $quba) {
 +        $record = new stdClass();
 +        $record->id = $quba->get_id();
 +        $record->contextid = $quba->get_owning_context()->id;
 +        $record->component = $quba->get_owning_component();
 +        $record->preferredbehaviour = $quba->get_preferred_behaviour();
 +
 +        $this->db->update_record('question_usages', $record);
 +    }
 +
 +    /**
 +     * Update a question_attempts row to refect any changes in a question_attempt
 +     * (but not any of its steps).
 +     * @param question_attempt $qa the question attempt that has changed.
 +     */
 +    public function update_question_attempt(question_attempt $qa) {
 +        $record = new stdClass();
 +        $record->id = $qa->get_database_id();
 +        $record->maxmark = $qa->get_max_mark();
 +        $record->minfraction = $qa->get_min_fraction();
 +        $record->flagged = $qa->is_flagged();
 +        $record->questionsummary = $qa->get_question_summary();
 +        $record->rightanswer = $qa->get_right_answer_summary();
 +        $record->responsesummary = $qa->get_response_summary();
 +        $record->timemodified = time();
 +
 +        $this->db->update_record('question_attempts', $record);
 +    }
 +
 +    /**
 +     * Delete a question_usage_by_activity and all its associated
 +     * {@link question_attempts} and {@link question_attempt_steps} from the
 +     * database.
 +     * @param qubaid_condition $qubaids identifies which question useages to delete.
 +     */
 +    public function delete_questions_usage_by_activities(qubaid_condition $qubaids) {
 +        $where = "qa.questionusageid {$qubaids->usage_id_in()}";
 +        $params = $qubaids->usage_id_in_params();
 +
 +        $contextids = $this->db->get_records_sql_menu("
 +                SELECT DISTINCT contextid, 1
 +                FROM {question_usages}
-                 "{question_attempts}.questionusageid {$qubaids->usage_id_in()}", $params);
++                WHERE id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
 +        foreach ($contextids as $contextid => $notused) {
 +            $this->delete_response_files($contextid, "IN (
 +                    SELECT qas.id
 +                    FROM {question_attempts} qa
 +                    JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 +                    WHERE $where)", $params);
 +        }
 +
 +        $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
 +                SELECT qas.id
 +                FROM {question_attempts} qa
 +                JOIN {question_attempt_steps} qas ON qas.questionattemptid = qa.id
 +                WHERE $where)", $params);
 +
 +        $this->db->delete_records_select('question_attempt_steps', "questionattemptid IN (
 +                SELECT qa.id
 +                FROM {question_attempts} qa
 +                WHERE $where)", $params);
 +
 +        $this->db->delete_records_select('question_attempts',
-                 "{question_usages}.id {$qubaids->usage_id_in()}", $params);
++                "{question_attempts}.questionusageid {$qubaids->usage_id_in()}",
++                $qubaids->usage_id_in_params());
 +
 +        $this->db->delete_records_select('question_usages',
-         return $this->db->get_in_or_equal($states, SQL_PARAMS_NAMED, $prefix . '00', $equal);
++                "{question_usages}.id {$qubaids->usage_id_in()}", $qubaids->usage_id_in_params());
 +    }
 +
 +    /**
 +     * Delete all the steps for a question attempt.
 +     * @param int $qaids question_attempt id.
 +     */
 +    public function delete_steps_for_question_attempts($qaids, $context) {
 +        if (empty($qaids)) {
 +            return;
 +        }
 +        list($test, $params) = $this->db->get_in_or_equal($qaids, SQL_PARAMS_NAMED);
 +
 +        $this->delete_response_files($context->id, "IN (
 +                SELECT id
 +                FROM question_attempt_step
 +                WHERE questionattemptid $test)", $params);
 +
 +        $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN (
 +                SELECT qas.id
 +                FROM {question_attempt_steps} qas
 +                WHERE questionattemptid $test)", $params);
 +        $this->db->delete_records_select('question_attempt_steps', 'questionattemptid ' . $test, $params);
 +    }
 +
 +    /**
 +     * Delete all the files belonging to the response variables in the gives
 +     * question attempt steps.
 +     * @param int $contextid the context these attempts belong to.
 +     * @param string $itemidstest a bit of SQL that can be used in a
 +     *      WHERE itemid $itemidstest clause. Must use named params.
 +     * @param array $params any query parameters used in $itemidstest.
 +     */
 +    protected function delete_response_files($contextid, $itemidstest, $params) {
 +        $fs = get_file_storage();
 +        foreach (question_engine::get_all_response_file_areas() as $filearea) {
 +            $fs->delete_area_files_select($contextid, 'question', $filearea,
 +                    $itemidstest, $params);
 +        }
 +    }
 +
 +    /**
 +     * Delete all the previews for a given question.
 +     * @param int $questionid question id.
 +     */
 +    public function delete_previews($questionid) {
 +        $previews = $this->db->get_records_sql_menu("
 +                SELECT DISTINCT quba.id, 1
 +                FROM {question_usages} quba
 +                JOIN {question_attempts} qa ON qa.questionusageid = quba.id
 +                WHERE quba.component = 'core_question_preview' AND
 +                    qa.questionid = ?", array($questionid));
 +        if (empty($previews)) {
 +            return;
 +        }
 +        $this->delete_questions_usage_by_activities(new qubaid_list($previews));
 +    }
 +
 +    /**
 +     * Update the flagged state of a question in the database.
 +     * @param int $qubaid the question usage id.
 +     * @param int $questionid the question id.
 +     * @param int $sessionid the question_attempt id.
 +     * @param bool $newstate the new state of the flag. true = flagged.
 +     */
 +    public function update_question_attempt_flag($qubaid, $questionid, $qaid, $slot, $newstate) {
 +        if (!$this->db->record_exists('question_attempts', array('id' => $qaid,
 +                'questionusageid' => $qubaid, 'questionid' => $questionid, 'slot' => $slot))) {
 +            throw new moodle_exception('errorsavingflags', 'question');
 +        }
 +
 +        $this->db->set_field('question_attempts', 'flagged', $newstate, array('id' => $qaid));
 +    }
 +
 +    /**
 +     * Get all the WHEN 'x' THEN 'y' terms needed to convert the question_attempt_steps.state
 +     * column to a summary state. Use this like
 +     * CASE qas.state {$this->full_states_to_summary_state_sql()} END AS summarystate,
 +     * @param string SQL fragment.
 +     */
 +    protected function full_states_to_summary_state_sql() {
 +        $sql = '';
 +        foreach (question_state::get_all() as $state) {
 +            $sql .= "WHEN '$state' THEN '{$state->get_summary_state()}'\n";
 +        }
 +        return $sql;
 +    }
 +
 +    /**
 +     * Get the SQL needed to test that question_attempt_steps.state is in a
 +     * state corresponding to $summarystate.
 +     * @param string $summarystate one of
 +     * inprogress, needsgrading, manuallygraded or autograded
 +     * @param bool $equal if false, do a NOT IN test. Default true.
 +     * @return string SQL fragment.
 +     */
 +    public function in_summary_state_test($summarystate, $equal = true, $prefix = 'summarystates') {
 +        $states = question_state::get_all_for_summary_state($summarystate);
-         list($where, $this->params) = $DB->get_in_or_equal($this->qubaids, SQL_PARAMS_NAMED, 'qubaid0000');
++        return $this->db->get_in_or_equal($states, SQL_PARAMS_NAMED, $prefix, $equal);
 +    }
 +
 +    /**
 +     * Change the maxmark for the question_attempt with number in usage $slot
 +     * for all the specified question_attempts.
 +     * @param qubaid_condition $qubaids Selects which usages are updated.
 +     * @param int $slot the number is usage to affect.
 +     * @param number $newmaxmark the new max mark to set.
 +     */
 +    public function set_max_mark_in_attempts(qubaid_condition $qubaids, $slot, $newmaxmark) {
 +        $this->db->set_field_select('question_attempts', 'maxmark', $newmaxmark,
 +                "questionusageid {$qubaids->usage_id_in()} AND slot = :slot",
 +                $qubaids->usage_id_in_params() + array('slot' => $slot));
 +    }
 +
 +    /**
 +     * Return a subquery that computes the sum of the marks for all the questions
 +     * in a usage. Which useage to compute the sum for is controlled bu the $qubaid
 +     * parameter.
 +     *
 +     * See {@link quiz_update_all_attempt_sumgrades()} for an example of the usage of
 +     * this method.
 +     *
 +     * @param string $qubaid SQL fragment that controls which usage is summed.