'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) {
--- /dev/null
+<?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');
+ }
+}
--- /dev/null
- 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');
+ }
+}
--- /dev/null
+<?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 <p>, </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;
+}
--- /dev/null
- $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;
+ }
+}
+
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&gradeall=1&q=$quiz->id&questionid=$question->id\">".get_string('gradeall', 'quiz_grading', $totalattempts).'</a></strong>';
- if ($ungraded>0){
- $links .="<br /><strong><a href=\"report.php?mode=grading&gradeungraded=1&q=$quiz->id&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&gradenextungraded=1&q=$quiz->id&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&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&q=$quiz->id&questionid=$question->id&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&q=$quiz->id&questionid=$question->id&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);
+ }
+}
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'));
+ }
+}
* 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"/>
$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;
}
}
}
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.' '.$datumfromtable['actions']);
+ $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
+ $datumfromtable['icon'] . ' ' .
+ question_bank::get_qtype($question->qtype, false)->menu_name() . ' ' .
+ $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.' '.$datumfromtable['actions']);
- $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'].' '.get_string($question->qtype,'quiz').' '.$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 = ' ';
- } 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);
}
#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();
--- /dev/null
- 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');
+ }
+}
--- /dev/null
- 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.