From e24ee794b2a33930237a6fc556ff1dfe77ea642c Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Wed, 16 Feb 2011 17:33:51 +0000 Subject: [PATCH] MDL-20636 Convert the overview report. --- mod/quiz/report/attemptsreport.php | 35 +- mod/quiz/report/overview/db/install.xml | 14 +- mod/quiz/report/overview/db/upgrade.php | 188 ++++- .../report/overview/lang/en/quiz_overview.php | 8 +- mod/quiz/report/overview/overview_table.php | 506 ++++++----- mod/quiz/report/overview/overviewgraph.php | 129 +-- .../report/overview/overviewsettings_form.php | 56 +- mod/quiz/report/overview/report.php | 795 ++++++++---------- mod/quiz/report/overview/version.php | 29 +- mod/quiz/report/responses/responses_table.php | 1 + mod/quiz/report/statistics/qstats.php | 5 +- mod/quiz/report/statistics/report.php | 2 +- question/engine/datalib.php | 16 +- question/todo/diffstat.txt | 20 +- 14 files changed, 959 insertions(+), 845 deletions(-) diff --git a/mod/quiz/report/attemptsreport.php b/mod/quiz/report/attemptsreport.php index f8e684358bf..f007c1c55e6 100644 --- a/mod/quiz/report/attemptsreport.php +++ b/mod/quiz/report/attemptsreport.php @@ -412,33 +412,30 @@ abstract class quiz_attempt_report_table extends table_sql { return $OUTPUT->user_picture($user); } - - public function col_timestart($attempt) { - if (!$attempt->attempt) { - return '-'; + public function col_fullname($attempt) { + $html = parent::col_fullname($attempt); + if ($this->is_downloading()) { + return $html; } - $startdate = userdate($attempt->timestart, $this->strtimeformat); - if (!$this->is_downloading()) { - return html_writer::link(new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)), $startdate); + 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 $startdate; + return '-'; } } public function col_timefinish($attempt) { - if (!$attempt->attempt) { - return '-'; - } - if (!$attempt->timefinish) { - return '-'; - } - - $timefinish = userdate($attempt->timefinish, $this->strtimeformat); - if (!$this->is_downloading()) { - return html_writer::link(new moodle_url('/mod/quiz/review.php', array('attempt' => $attempt->attempt)), $timefinish); + if ($attempt->attempt && $attempt->timefinish) { + return userdate($attempt->timefinish, $this->strtimeformat); } else { - return $timefinish; + return '-'; } } diff --git a/mod/quiz/report/overview/db/install.xml b/mod/quiz/report/overview/db/install.xml index 347b9345eae..fdfbcf4af45 100644 --- a/mod/quiz/report/overview/db/install.xml +++ b/mod/quiz/report/overview/db/install.xml @@ -4,14 +4,14 @@ xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd" > - +
- - - - - - + + + + + + diff --git a/mod/quiz/report/overview/db/upgrade.php b/mod/quiz/report/overview/db/upgrade.php index 9b7bd6c2be6..3eac5b0daf9 100644 --- a/mod/quiz/report/overview/db/upgrade.php +++ b/mod/quiz/report/overview/db/upgrade.php @@ -1,6 +1,34 @@ . + +/** + * Quiz overview report upgrade script. + * + * @package quiz + * @subpackage overview + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Quiz overview report upgrade function. + * @param number $oldversion + */function xmldb_quiz_overview_upgrade($oldversion) { global $CFG, $DB; $dbman = $DB->get_manager(); @@ -9,10 +37,10 @@ function xmldb_quiz_overview_upgrade($oldversion) { if ($oldversion < 2009091400) { - /// Define table quiz_question_regrade to be created + // Define table quiz_question_regrade to be created $table = new xmldb_table('quiz_question_regrade'); - /// Adding fields to table quiz_question_regrade + // Adding fields to table quiz_question_regrade $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('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); @@ -21,19 +49,161 @@ function xmldb_quiz_overview_upgrade($oldversion) { $table->add_field('regraded', XMLDB_TYPE_INTEGER, '4', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); - /// Adding keys to table quiz_question_regrade + // Adding keys to table quiz_question_regrade $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); - /// Conditionally launch create table for quiz_question_regrade + // Conditionally launch create table for quiz_question_regrade if (!$dbman->table_exists($table)) { $dbman->create_table($table); } - /// overview savepoint reached - upgrade_plugin_savepoint(true, 2009091400, 'quizreport', 'overview'); + // overview savepoint reached + upgrade_plugin_savepoint(true, 2009091400, 'quiz', 'overview'); + } + + if ($oldversion < 2010040600) { + + // Wipe the quiz_question_regrade before we changes its structure. The data + // It contains is not important long-term, and it is almost impossible to upgrade. + $DB->delete_records('quiz_question_regrade'); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040600, 'quiz', 'overview'); + } + + if ($oldversion < 2010040601) { + + // Rename field attemptid on table quiz_question_regrade to questionusageid + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('attemptid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'id'); + + // Launch rename field questionusageid + $dbman->rename_field($table, $field, 'questionusageid'); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040601, 'quiz', 'overview'); + } + + if ($oldversion < 2010040602) { + + // Define field slot to be added to quiz_question_regrade + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('slot', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, 'questionusageid'); + + // Conditionally launch add field slot + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040602, 'quiz', 'overview'); + } + + if ($oldversion < 2010040603) { + + // Define field questionid to be dropped from quiz_question_regrade + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('questionid'); + + // Conditionally launch drop field questionusageid + if ($dbman->field_exists($table, $field)) { + $dbman->drop_field($table, $field); + } + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040603, 'quiz', 'overview'); + } + + if ($oldversion < 2010040604) { + + // Rename field newgrade on table quiz_question_regrade to newfraction + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('newgrade', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch rename field newfraction + $dbman->rename_field($table, $field, 'newfraction'); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040604, 'quiz', 'overview'); + } + + if ($oldversion < 2010040605) { + + // Rename field oldgrade on table quiz_question_regrade to oldfraction + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('oldgrade', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch rename field newfraction + $dbman->rename_field($table, $field, 'oldfraction'); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040605, 'quiz', 'overview'); + } + + if ($oldversion < 2010040606) { + + // Changing precision of field newfraction on table quiz_question_regrade to (12, 7) + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('newfraction', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch change of precision for field newfraction + $dbman->change_field_precision($table, $field); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040606, 'quiz', 'overview'); + } + + if ($oldversion < 2010040607) { + + // Changing precision of field oldfraction on table quiz_question_regrade to (12, 7) + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('oldfraction', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch change of precision for field newfraction + $dbman->change_field_precision($table, $field); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010040607, 'quiz', 'overview'); + } + + if ($oldversion < 2010082700) { + + // Changing nullability of field newfraction on table quiz_question_regrade to null + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('newfraction', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch change of nullability for field newfraction + $dbman->change_field_notnull($table, $field); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010082700, 'quiz', 'overview'); + } + + if ($oldversion < 2010082701) { + + // Changing nullability of field oldfraction on table quiz_question_regrade to null + $table = new xmldb_table('quiz_question_regrade'); + $field = new xmldb_field('oldfraction', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null, 'slot'); + + // Launch change of nullability for field newfraction + $dbman->change_field_notnull($table, $field); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2010082701, 'quiz', 'overview'); + } + + if ($oldversion < 2011021600) { + + // Define table quiz_question_regrade to be renamed to quiz_overview_regrades + // so that it follows the Moodle coding guidelines. + $table = new xmldb_table('quiz_question_regrade'); + + // Launch rename table for quiz_question_regrade + $dbman->rename_table($table, 'quiz_overview_regrades'); + + // overview savepoint reached + upgrade_plugin_savepoint(true, 2011021600, 'quiz', 'overview'); } return true; } - - diff --git a/mod/quiz/report/overview/lang/en/quiz_overview.php b/mod/quiz/report/overview/lang/en/quiz_overview.php index acb9bd9b358..0eb69b8fe24 100644 --- a/mod/quiz/report/overview/lang/en/quiz_overview.php +++ b/mod/quiz/report/overview/lang/en/quiz_overview.php @@ -47,6 +47,7 @@ $string['optonlygradedattempts'] = 'that are graded for each user ({$a})'; $string['optonlyregradedattempts'] = 'that have been regraded / are marked as needing regrading'; $string['overview'] = 'Grades'; $string['overviewdownload'] = 'Overview download'; +$string['overviewfilename'] = 'grades'; $string['overviewreport'] = 'Grades report'; $string['overviewreportgraph'] = 'Overall number of students achieving grade ranges'; $string['overviewreportgraphgroup'] = 'Number of students in group \'{$a}\' achieving grade ranges'; @@ -64,10 +65,9 @@ $string['regradealldrygroup'] = 'Dry run a full regrade for group \'{$a->groupna $string['regradeallgroup'] = 'Full regrade for group \'{$a->groupname}\''; $string['regradeheader'] = 'Regrading'; $string['regradeselected'] = 'Regrade selected attempts'; -$string['requiresgrading'] = 'Requires grading'; -$string['show'] = 'Include'; -$string['showattempts'] = 'Include attempts'; -$string['showdetailedmarks'] = 'Marks for each question'; +$string['show'] = 'Show / download'; +$string['showattempts'] = 'Only show / download attempts'; +$string['showdetailedmarks'] = 'Show / download marks for each question'; $string['showinggraded'] = 'Showing only the attempt graded for each user.'; $string['showinggradedandungraded'] = 'Showing graded and ungraded attempts for each user. The one attempt for each user that is graded is highlighted. The grading method for this quiz is {$a}.'; $string['studentingroup'] = '\'{$a->coursestudent}\' in group \'{$a->groupname}\''; diff --git a/mod/quiz/report/overview/overview_table.php b/mod/quiz/report/overview/overview_table.php index 50a21794c31..a976c65df21 100644 --- a/mod/quiz/report/overview/overview_table.php +++ b/mod/quiz/report/overview/overview_table.php @@ -1,170 +1,181 @@ . - var $useridfield = 'userid'; +/** + * This file defines the quiz grades table. + * + * @package quiz_overview + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ - var $candelete; - var $reporturl; - var $displayoptions; - var $regradedqs = array(); - function quiz_report_overview_table($quiz , $qmsubselect, $groupstudents, - $students, $detailedmarks, $questions, $candelete, $reporturl, $displayoptions, $context){ - parent::__construct('mod-quiz-report-overview-report'); - $this->quiz = $quiz; - $this->qmsubselect = $qmsubselect; - $this->groupstudents = $groupstudents; - $this->students = $students; +/** + * This is a table subclass for displaying the quiz grades report. + * + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quiz_report_overview_table extends quiz_attempt_report_table { + + protected $candelete; + protected $regradedqs = array(); + + public function __construct($quiz, $context, $qmsubselect, $groupstudents, + $students, $detailedmarks, $questions, $candelete, $reporturl, $displayoptions) { + parent::__construct('mod-quiz-report-overview-report', $quiz , $context, + $qmsubselect, $groupstudents, $students, $questions, $candelete, + $reporturl, $displayoptions); $this->detailedmarks = $detailedmarks; - $this->questions = $questions; - $this->candelete = $candelete; - $this->reporturl = $reporturl; - $this->displayoptions = $displayoptions; - $this->context = $context; } - function build_table(){ - global $CFG, $DB; + + public function build_table() { + global $DB; + if ($this->rawdata) { - // Define some things we need later to process raw data from db. $this->strtimeformat = str_replace(',', '', get_string('strftimedatetime')); parent::build_table(); + //end of adding data from attempts data to table / download //now add averages at bottom of table : $params = array($this->quiz->id); - $averagesql = "SELECT AVG(qg.grade) AS grade " . - "FROM {quiz_grades} qg " . - "WHERE quiz=?"; + $averagesql = ' + SELECT AVG(qg.grade) AS grade, COUNT(qg.grade) AS numaveraged + FROM {quiz_grades} qg + WHERE quiz = ?'; $this->add_separator(); - if ($this->is_downloading()){ + if ($this->is_downloading()) { $namekey = 'lastname'; } else { $namekey = 'fullname'; } - if ($this->groupstudents){ - list($g_usql, $g_params) = $DB->get_in_or_equal($this->groupstudents); - - $groupaveragesql = $averagesql." AND qg.userid $g_usql"; - $groupaverage = $DB->get_record_sql($groupaveragesql, array_merge($params, $g_params)); - $groupaveragerow = array($namekey => get_string('groupavg', 'grades'), - 'sumgrades' => quiz_format_grade($this->quiz, $groupaverage->grade), - 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($groupaverage->grade, $this->quiz->id, $this->context))); - if($this->detailedmarks && ($this->qmsubselect || $this->quiz->attempts == 1)) { - $avggradebyq = quiz_get_average_grade_for_questions($this->quiz, $this->groupstudents); - $groupaveragerow += quiz_format_average_grade_for_questions($avggradebyq, $this->questions, $this->quiz, $this->is_downloading()); + if ($this->groupstudents) { + list($usql, $uparams) = $DB->get_in_or_equal($this->groupstudents); + $record = $DB->get_record_sql($averagesql . ' AND qg.userid ' . $usql, array_merge($params, $uparams)); + $groupaveragerow = array( + $namekey => get_string('groupavg', 'grades'), + 'sumgrades' => $this->format_average($record), + 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($record->grade, $this->quiz->id))); + if ($this->detailedmarks && ($this->quiz->attempts == 1 || $this->qmsubselect)) { + $avggradebyq = $this->load_average_question_grades($this->groupstudents); + $groupaveragerow += $this->format_average_grade_for_questions($avggradebyq); } $this->add_data_keyed($groupaveragerow); } - list($s_usql, $s_params) = $DB->get_in_or_equal($this->students); - $overallaverage = $DB->get_record_sql($averagesql." AND qg.userid $s_usql", array_merge($params, $s_params)); - $overallaveragerow = array($namekey => get_string('overallaverage', 'grades'), - 'sumgrades' => quiz_format_grade($this->quiz, $overallaverage->grade), - 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($overallaverage->grade, $this->quiz->id, $this->context))); - if($this->detailedmarks && ($this->qmsubselect || $this->quiz->attempts == 1)) { - $avggradebyq = quiz_get_average_grade_for_questions($this->quiz, $this->students); - $overallaveragerow += quiz_format_average_grade_for_questions($avggradebyq, $this->questions, $this->quiz, $this->is_downloading()); - } - $this->add_data_keyed($overallaveragerow); - } - } - - function wrap_html_start(){ - if (!$this->is_downloading()) { - if ($this->candelete) { - // Start form - $url = new moodle_url($this->reporturl, $this->displayoptions); - echo '
'; - echo '
'; - echo '
'; - echo html_writer::input_hidden_params($url); - echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())) . "\n"; - echo '
'; - echo '
'; - } - } - } - function wrap_html_finish(){ - if (!$this->is_downloading()) { - // Print "Select all" etc. - if ($this->candelete) { - $strreallydel = addslashes_js(get_string('deleteattemptcheck','quiz')); - echo '
'; - echo ''. - get_string('selectall', 'quiz').' / '; - echo ''. - get_string('selectnone', 'quiz').' '; - echo '  '; - if (has_capability('mod/quiz:regrade', $this->context)){ - echo ''; + if ($this->students) { + list($usql, $uparams) = $DB->get_in_or_equal($this->students); + $record = $DB->get_record_sql($averagesql . ' AND qg.userid ' . $usql, array_merge($params, $uparams)); + $overallaveragerow = array( + $namekey => get_string('overallaverage', 'grades'), + 'sumgrades' => $this->format_average($record), + 'feedbacktext'=> strip_tags(quiz_report_feedback_for_grade($record->grade, $this->quiz->id, $this->context))); + if ($this->detailedmarks && ($this->quiz->attempts == 1 || $this->qmsubselect)) { + $avggradebyq = $this->load_average_question_grades($this->students); + $overallaveragerow += $this->format_average_grade_for_questions($avggradebyq); } - echo ''; - echo '
'; - // Close form - echo '
'; - echo '
'; + $this->add_data_keyed($overallaveragerow); } } } + protected function format_average_grade_for_questions($gradeaverages) { + $row = array(); + if (!$gradeaverages) { + $gradeaverages = array(); + } + foreach ($this->questions as $question) { + if (isset($gradeaverages[$question->slot]) && $question->maxmark > 0) { + $record = $gradeaverages[$question->slot]; + $record->grade = quiz_rescale_grade($record->averagefraction * $question->maxmark, $this->quiz, false); + } else { + $record = new stdClass; + $record->grade = null; + $record->numaveraged = null; + } + $row['qsgrade' . $question->slot] = $this->format_average($record, true); + } + return $row; + } - function col_checkbox($attempt){ - if ($attempt->attempt){ - return ''; + /** + * Format an entry in an average row. + * @param object $record with fields grade and numaveraged + */ + protected function format_average($record, $question = false) { + if (is_null($record->grade)) { + $average = '-'; + } else if ($question) { + $average = quiz_format_question_grade($this->quiz, $record->grade); } else { - return ''; - } - } - - 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); - } - - function col_fullname($attempt){ - $html = parent::col_fullname($attempt); - if ($this->is_downloading()) { - return $html; + $average = quiz_format_grade($this->quiz, $record->grade); } - return $html . '
'.get_string('reviewattempt', 'quiz').''; - } - - function col_timestart($attempt){ - if ($attempt->attempt) { - return userdate($attempt->timestart, $this->strtimeformat); + if ($this->download) { + return $average; + } else if (is_null($record->numaveraged)) { + return html_writer::tag('span', html_writer::tag('span', + $average, array('class' => 'average')), array('class' => 'avgcell')); } else { - return '-'; - } - } - function col_timefinish($attempt){ - if ($attempt->attempt && $attempt->timefinish) { - return userdate($attempt->timefinish, $this->strtimeformat); - } else { - return '-'; + return html_writer::tag('span', html_writer::tag('span', + $average, array('class' => 'average')) . ' ' . html_writer::tag('span', + '(' . $record->numaveraged . ')', array('class' => 'count')), array('class' => 'avgcell')); } } - 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 wrap_html_start() { + if ($this->is_downloading() || !$this->candelete) { + return; } + + // Start form + $url = new moodle_url($this->reporturl, $this->displayoptions + array('sesskey' => sesskey())); + echo '
'; + echo '
'; + echo '
'; + echo html_writer::input_hidden_params($url); + echo '
'; + echo '
'; } - function col_sumgrades($attempt){ + public function wrap_html_finish() { + if ($this->is_downloading() || !$this->candelete) { + return; + } + + // TODO add back are you sure, and convert to html_writer. + echo '
'; + echo ''. + get_string('selectall', 'quiz').' / '; + echo ''. + get_string('selectnone', 'quiz').' '; + echo '  '; + if (has_capability('mod/quiz:regrade', $this->context)) { + echo ''; + } + echo ''; + echo '
'; + // Close form + echo '
'; + echo '
'; + } + + public function col_sumgrades($attempt) { if (!$attempt->timefinish) { return '-'; } @@ -174,29 +185,26 @@ class quiz_report_overview_table extends table_sql { return $grade; } - if (isset($this->regradedqs[$attempt->attemptuniqueid])){ + if (isset($this->regradedqs[$attempt->usageid])) { $newsumgrade = 0; $oldsumgrade = 0; - foreach ($this->questions as $question){ - if (isset($this->regradedqs[$attempt->attemptuniqueid][$question->id])){ - $newsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->newgrade; - $oldsumgrade += $this->regradedqs[$attempt->attemptuniqueid][$question->id]->oldgrade; + foreach ($this->questions as $question) { + if (isset($this->regradedqs[$attempt->usageid][$question->slot])) { + $newsumgrade += $this->regradedqs[$attempt->usageid][$question->slot]->newfraction * $question->maxmark; + $oldsumgrade += $this->regradedqs[$attempt->usageid][$question->slot]->oldfraction * $question->maxmark; } else { - $newsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade; - $oldsumgrade += $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$question->id]->grade; + $newsumgrade += $this->lateststeps[$attempt->usageid][$question->slot]->fraction * $question->maxmark; + $oldsumgrade += $this->lateststeps[$attempt->usageid][$question->slot]->fraction * $question->maxmark; } } $newsumgrade = quiz_rescale_grade($newsumgrade, $this->quiz); $oldsumgrade = quiz_rescale_grade($oldsumgrade, $this->quiz); - $grade = "$oldsumgrade
$newsumgrade"; + $grade = html_writer::tag('del', $oldsumgrade) . '/' . + html_writer::empty_tag('br') . $newsumgrade; } - - $gradehtml = ''.$grade.''; - if ($this->qmsubselect && $attempt->gradedattempt){ - $gradehtml = '
'.$gradehtml.'
'; - } - return $gradehtml; + return html_writer::link(new moodle_url('/mod/quiz/review.php', + array('attempt' => $attempt->attempt)), $grade, + array('title' => get_string('reviewattempt', 'quiz'))); } /** @@ -206,77 +214,52 @@ class quiz_report_overview_table extends table_sql { * and what they are called. * @return string the contents of the cell. */ - function other_cols($colname, $attempt){ - global $OUTPUT; - - if (preg_match('/^qsgrade([0-9]+)$/', $colname, $matches)){ - $questionid = $matches[1]; - $question = $this->questions[$questionid]; - if (isset($this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid])){ - $stateforqinattempt = $this->gradedstatesbyattempt[$attempt->attemptuniqueid][$questionid]; - } else { - $stateforqinattempt = false; - } - if ($stateforqinattempt && question_state_is_graded($stateforqinattempt)) { - $grade = quiz_rescale_grade($stateforqinattempt->grade, $this->quiz, 'question'); - if (!$this->is_downloading()) { - if (isset($this->regradedqs[$attempt->attemptuniqueid][$questionid])){ - $gradefromdb = $grade; - $newgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->newgrade, $this->quiz, 'question'); - $oldgrade = quiz_rescale_grade($this->regradedqs[$attempt->attemptuniqueid][$questionid]->oldgrade, $this->quiz, 'question'); - - $grade = ''.$oldgrade.'
'. - $newgrade; - } - - $link = new moodle_url("/mod/quiz/reviewquestion.php?attempt=$attempt->attempt&question=$question->id"); - $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650)); - $linktopopup = $OUTPUT->action_link($link, $grade, $action, array('title'=>get_string('reviewresponsetoq', 'quiz', $question->formattedname))); - - if (($this->questions[$questionid]->maxgrade != 0)){ - $fractionofgrade = $stateforqinattempt->grade - / $this->questions[$questionid]->maxgrade; - $qclass = question_get_feedback_class($fractionofgrade); - $feedbackimg = question_get_feedback_image($fractionofgrade); - $questionclass = "que"; - return "".$linktopopup."$feedbackimg"; - } else { - return $linktopopup; - } - - } else { - return $grade; - } - } else if ($stateforqinattempt && question_state_is_closed($stateforqinattempt)) { - $text = get_string('requiresgrading', 'quiz_overview'); - if (!$this->is_downloading()) { - $link = new moodle_url("/mod/quiz/reviewquestion.php?attempt=$attempt->attempt&question=$question->id"); - $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650)); - return $OUTPUT->action_link($link, $text, $action, array('title'=>get_string('reviewresponsetoq', 'quiz', $question->formattedname))); - } else { - return $text; - } - } else { - return '--'; - } - } else { + public function other_cols($colname, $attempt) { + if (!preg_match('/^qsgrade(\d+)$/', $colname, $matches)) { return NULL; } - } - - function col_feedbacktext($attempt){ - if ($attempt->timefinish) { - 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, $this->context)); - } - } else { + $slot = $matches[1]; + $question = $this->questions[$slot]; + if (!isset($this->lateststeps[$attempt->usageid][$slot])) { return '-'; } + $stepdata = $this->lateststeps[$attempt->usageid][$slot]; + $state = question_state::get($stepdata->state); + + if ($question->maxmark == 0) { + $grade = '-'; + } else if (is_null($stepdata->fraction)) { + if ($state == question_state::$needsgrading) { + $grade = get_string('requiresgrading', 'question'); + } else { + $grade = '-'; + } + } else { + $grade = quiz_rescale_grade($stepdata->fraction * $question->maxmark, $this->quiz, 'question'); + } + + if ($this->is_downloading()) { + return $grade; + } + + if (isset($this->regradedqs[$attempt->usageid][$slot])) { + $gradefromdb = $grade; + $newgrade = quiz_rescale_grade( + $this->regradedqs[$attempt->usageid][$slot]->newfraction * $question->maxmark, + $this->quiz, 'question'); + $oldgrade = quiz_rescale_grade( + $this->regradedqs[$attempt->usageid][$slot]->oldfraction * $question->maxmark, + $this->quiz, 'question'); + + $grade = html_writer::tag('del', $oldgrade) . '/' . + html_writer::empty_tag('br') . $newgrade; + } + + return $this->make_review_link($grade, $attempt, $slot); } - function col_regraded($attempt){ + + public function col_regraded($attempt) { if ($attempt->regraded == '') { return ''; } else if ($attempt->regraded == 0) { @@ -285,59 +268,64 @@ class quiz_report_overview_table extends table_sql { 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(); } } -} + /** + * 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, 'u0000'); + $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')); + } +} diff --git a/mod/quiz/report/overview/overviewgraph.php b/mod/quiz/report/overview/overviewgraph.php index 9fb13b66abb..76f64b2cc92 100644 --- a/mod/quiz/report/overview/overviewgraph.php +++ b/mod/quiz/report/overview/overviewgraph.php @@ -1,36 +1,63 @@ dirroot."/lib/graphlib.php"; -include $CFG->dirroot."/mod/quiz/report/reportlib.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 . + +/** + * This file renders the quiz overview graph. + * + * @package quiz_overview + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once(dirname(__FILE__) . '/../../../../config.php'); +require_once($CFG->libdir . '/graphlib.php'); +require_once($CFG->dirroot . '/mod/quiz/locallib.php'); +require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php'); + $quizid = required_param('id', PARAM_INT); $groupid = optional_param('groupid', 0, PARAM_INT); $quiz = $DB->get_record('quiz', array('id' => $quizid)); $course = $DB->get_record('course', array('id' => $quiz->course)); $cm = get_coursemodule_from_instance('quiz', $quizid); -require_login($course, true, $cm); + +require_login($course, false, $cm); $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id); -if ($groupid && $groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used - $groups = groups_get_activity_allowed_groups($cm); - if (array_key_exists($groupid, $groups)){ - $group = $groups[$groupid]; - if (!$groupusers = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$group->id,'',false)){ - print_error('nostudentsingroup'); - } else { - $groupusers = array_keys($groupusers); - } - } else { - print_error('errorinvalidgroup', 'group', null, $groupid); - } -} else { - $groups = false; - $group = false; - $groupusers = array(); -} require_capability('mod/quiz:viewreports', $modcontext); +if ($groupid && $groupmode = groups_get_activity_groupmode($cm)) { + // Groups are being used + $groups = groups_get_activity_allowed_groups($cm); + if (!array_key_exists($groupid, $groups)) { + print_error('errorinvalidgroup', 'group', null, $groupid); + } + $group = $groups[$groupid]; + $groupusers = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),'','','','',$group->id,'',false); + if (!$groupusers) { + print_error('nostudentsingroup'); + } + $groupusers = array_keys($groupusers); +} else { + $groupusers = array(); +} + $line = new graph(800,600); -$line->parameter['title'] = ''; +$line->parameter['title'] = ''; $line->parameter['y_label_left'] = get_string('participants'); $line->parameter['x_label'] = get_string('grade'); $line->parameter['y_label_angle'] = 90; @@ -41,59 +68,57 @@ $line->parameter['x_axis_angle'] = 60; $line->y_tick_labels = null; $line->offset_relation = null; -$line->parameter['bar_size'] = 1; // will make size > 1 to get overlap effect when showing groups +$line->parameter['bar_size'] = 1; // will make size > 1 to get overlap effect when showing groups $line->parameter['bar_spacing'] = 10; // don't forget to increase spacing so that graph doesn't become one big block of colour //pick a sensible number of bands depending on quiz maximum grade. $bands = $quiz->grade; -while ($bands > 20 || $bands <= 10){ - if ($bands > 50){ - $bands = $bands /5; +while ($bands > 20 || $bands <= 10) { + if ($bands > 50) { + $bands /= 5; } else if ($bands > 20) { - $bands = $bands /2; + $bands /= 2; } - if ($bands < 4){ - $bands = $bands * 5; - } else if ($bands <= 10){ - $bands = $bands * 2; + if ($bands < 4) { + $bands *= 5; + } else if ($bands <= 10) { + $bands *= 2; } } -$bandwidth = $quiz->grade / $bands; $bands = ceil($bands); +$bandwidth = $quiz->grade / $bands; $bandlabels = array(); -for ($i=0;$i < $quiz->grade;$i += $bandwidth){ - $label = quiz_format_grade($quiz, $i).' - '; - if ($quiz->grade > $i+$bandwidth){ - $label .= quiz_format_grade($quiz, $i+$bandwidth); - } else { - $label .= quiz_format_grade($quiz, $quiz->grade); - } - $bandlabels[] = $label; +for ($i = 1; $i <= $bands; $i++) { + $bandlabels[] = quiz_format_grade($quiz, ($i - 1) * $bandwidth) . ' - ' . + quiz_format_grade($quiz, $i * $bandwidth); } -$line->x_data = $bandlabels; +$line->x_data = $bandlabels; -$line->y_format['allusers'] = - array('colour' => 'red', 'bar' => 'fill', 'shadow_offset' => 1, 'legend' => get_string('allparticipants')); +$line->y_format['allusers'] = array( + 'colour' => 'red', + 'bar' => 'fill', + 'shadow_offset' => 1, + 'legend' => get_string('allparticipants') +); $line->y_data['allusers'] = quiz_report_grade_bands($bandwidth, $bands, $quizid, $groupusers); $line->y_order = array('allusers'); +$ymax = max($line->y_data['allusers']); $line->parameter['y_min_left'] = 0; // start at 0 -$line->parameter['y_max_left'] = max($line->y_data['allusers']); +$line->parameter['y_max_left'] = $ymax; $line->parameter['y_decimal_left'] = 0; // 2 decimal places for y axis. - //pick a sensible number of gridlines depending on max value on graph. -$gridlines = max($line->y_data['allusers']); -while ($gridlines >= 10){ - if ($gridlines >= 50){ - $gridlines = $gridlines /5; +$gridlines = $ymax; +while ($gridlines >= 10) { + if ($gridlines >= 50) { + $gridlines /= 5; } else { - $gridlines = $gridlines /2; + $gridlines /= 2; } } -$line->parameter['y_axis_gridlines'] = $gridlines+1; +$line->parameter['y_axis_gridlines'] = $gridlines + 1; $line->draw(); - diff --git a/mod/quiz/report/overview/overviewsettings_form.php b/mod/quiz/report/overview/overviewsettings_form.php index b1630818ac7..0f32664de07 100644 --- a/mod/quiz/report/overview/overviewsettings_form.php +++ b/mod/quiz/report/overview/overviewsettings_form.php @@ -1,27 +1,60 @@ libdir/formslib.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 . + +/** + * This file defines the setting form for the quiz overview report. + * + * @package quiz_overview + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +require_once($CFG->libdir . '/formslib.php'); + + +/** + * Quiz overview report settings form. + * + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class mod_quiz_report_overview_settings extends moodleform { function definition() { - global $COURSE; - $mform =& $this->_form; -//------------------------------------------------------------------------------- + global $COURSE; // TODO get rid of this. + $mform = $this->_form; + $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'quiz_overview')); - if (!$this->_customdata['currentgroup']){ + if (!$this->_customdata['currentgroup']) { $studentsstring = get_string('participants'); } else { $a = new stdClass(); $a->coursestudent = get_string('participants'); $a->groupname = groups_get_group_name($this->_customdata['currentgroup']); - if (20 < strlen($a->groupname)){ + if (20 < strlen($a->groupname)) { $studentsstring = get_string('studentingrouplong', 'quiz_overview', $a); } else { $studentsstring = get_string('studentingroup', 'quiz_overview', $a); } } $options = array(); - if (!$this->_customdata['currentgroup']){ + if (!$this->_customdata['currentgroup']) { $options[QUIZ_REPORT_ATTEMPTS_ALL] = get_string('optallattempts','quiz_overview'); } if ($this->_customdata['currentgroup'] || $COURSE->id != SITEID) { @@ -33,17 +66,17 @@ class mod_quiz_report_overview_settings extends moodleform { $mform->addElement('select', 'attemptsmode', get_string('show', 'quiz_overview'), $options); $showattemptsgrp = array(); - if ($this->_customdata['qmsubselect']){ + if ($this->_customdata['qmsubselect']) { $gm = ''.quiz_get_grading_option_name($this->_customdata['quiz']->grademethod).''; $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'qmfilter', get_string('showattempts', 'quiz_overview'), get_string('optonlygradedattempts', 'quiz_overview', $gm), null, array(0,1)); } - if (has_capability('mod/quiz:regrade', $this->_customdata['context'])){ + if (has_capability('mod/quiz:regrade', $this->_customdata['context'])) { $showattemptsgrp[] =& $mform->createElement('advcheckbox', 'regradefilter', get_string('showattempts', 'quiz_overview'), get_string('optonlyregradedattempts', 'quiz_overview'), null, array(0,1)); } - if ($showattemptsgrp){ + if ($showattemptsgrp) { $mform->addGroup($showattemptsgrp, null, get_string('showattempts', 'quiz_overview'), '
', false); } -//------------------------------------------------------------------------------- + $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'quiz_overview')); $mform->addElement('text', 'pagesize', get_string('pagesize', 'quiz_overview')); @@ -54,4 +87,3 @@ class mod_quiz_report_overview_settings extends moodleform { $mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview')); } } - diff --git a/mod/quiz/report/overview/report.php b/mod/quiz/report/overview/report.php index 8d7dd43efde..b28f92fd14f 100644 --- a/mod/quiz/report/overview/report.php +++ b/mod/quiz/report/overview/report.php @@ -1,58 +1,52 @@ . + /** - * This script lists student attempts + * This file defines the quiz overview report class. * - * @author Martin Dougiamas, Tim Hunt and others. - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package quiz + * @package quiz + * @subpackage overview + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -require_once($CFG->libdir.'/tablelib.php'); + +require_once($CFG->dirroot.'/mod/quiz/report/attemptsreport.php'); require_once($CFG->dirroot.'/mod/quiz/report/overview/overviewsettings_form.php'); require_once($CFG->dirroot.'/mod/quiz/report/overview/overview_table.php'); -class quiz_overview_report extends quiz_default_report { - /** - * Display the report. - */ +/** + * Quiz report subclass for the overview (grades) report. + * + * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class quiz_overview_report extends quiz_attempt_report { + function display($quiz, $cm, $course) { global $CFG, $COURSE, $DB, $OUTPUT; $this->context = get_context_instance(CONTEXT_MODULE, $cm->id); - // Work out some display options - whether there is feedback, and whether grades should be shown. - $hasfeedback = quiz_has_feedback($quiz); - $fakeattempt = new stdClass(); - $fakeattempt->preview = false; - $fakeattempt->timefinish = $quiz->timeopen; - $fakeattempt->userid = 0; - $reviewoptions = quiz_get_review_options($quiz, $fakeattempt, $this->context); - $showgrades = quiz_has_grades($quiz) && $reviewoptions->scores; - $download = optional_param('download', '', PARAM_ALPHA); - /// find out current groups mode - $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)) { - // all users who can attempt quizzes - $allowed = $students; - $groupstudents = array(); - } else { - // all users who can attempt quizzes and who are in the 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); - } - $allowed = $groupstudents; - } + list($currentgroup, $students, $groupstudents, $allowed) = + $this->load_relevant_students($cm); $pageoptions = array(); $pageoptions['id'] = $cm->id; @@ -61,26 +55,25 @@ class quiz_overview_report extends quiz_default_report { $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions); $qmsubselect = quiz_report_qm_filter_select($quiz); - $mform = new mod_quiz_report_overview_settings($reporturl, array('qmsubselect'=> $qmsubselect, 'quiz'=>$quiz, - 'currentgroup'=>$currentgroup, 'context'=>$this->context)); + $mform = new mod_quiz_report_overview_settings($reporturl, + array('qmsubselect' => $qmsubselect, 'quiz' => $quiz, 'currentgroup' => $currentgroup, 'context'=>$this->context)); + if ($fromform = $mform->get_data()) { $regradeall = false; $regradealldry = false; $regradealldrydo = false; $attemptsmode = $fromform->attemptsmode; if ($qmsubselect) { - //control is not on the form if - //the grading method is not set - //to grade one attempt per user eg. for average attempt grade. $qmfilter = $fromform->qmfilter; } else { $qmfilter = 0; } - $regradefilter = $fromform->regradefilter; + $regradefilter = !empty($fromform->regradefilter); set_user_preference('quiz_report_overview_detailedmarks', $fromform->detailedmarks); set_user_preference('quiz_report_pagesize', $fromform->pagesize); $detailedmarks = $fromform->detailedmarks; $pagesize = $fromform->pagesize; + } else { $regradeall = optional_param('regradeall', 0, PARAM_BOOL); $regradealldry = optional_param('regradealldry', 0, PARAM_BOOL); @@ -92,82 +85,90 @@ class quiz_overview_report extends quiz_default_report { $qmfilter = 0; } $regradefilter = optional_param('regradefilter', 0, PARAM_INT); - $detailedmarks = get_user_preferences('quiz_report_overview_detailedmarks', 1); $pagesize = get_user_preferences('quiz_report_pagesize', 0); } - 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 (!$reviewoptions->scores) { - $detailedmarks = 0; - } - if ($pagesize < 1) { - $pagesize = QUIZ_REPORT_DEFAULT_PAGE_SIZE; - } - // We only want to show the checkbox to delete attempts - // if the user has permissions and if the report mode is showing attempts. - $candelete = has_capability('mod/quiz:deleteattempts', $this->context) - && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO); + $this->validate_common_options($attemptsmode, $pagesize, $course, $currentgroup); $displayoptions = array(); $displayoptions['attemptsmode'] = $attemptsmode; $displayoptions['qmfilter'] = $qmfilter; $displayoptions['regradefilter'] = $regradefilter; + $mform->set_data($displayoptions + array('detailedmarks' => $detailedmarks, 'pagesize' => $pagesize)); + + if (!$this->should_show_grades($quiz)) { + $detailedmarks = 0; + } + + // We only want to show the checkbox to delete attempts + // if the user has permissions and if the report mode is showing attempts. + $candelete = has_capability('mod/quiz:deleteattempts', $this->context) + && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO); + if ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) { + // This option is only available to users who can access all groups in + // groups mode, so setting allowed to empty (which means all quiz attempts + // are accessible, is not a security porblem. $allowed = array(); } + // Load the required questions. + if ($detailedmarks) { + $questions = quiz_report_get_significant_questions($quiz); + } else { + $questions = array(); + } + + $table = new quiz_report_overview_table($quiz, $this->context, $qmsubselect, + $groupstudents, $students, $detailedmarks, $questions, $candelete, + $reporturl, $displayoptions); + $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'), + $course->shortname, $quiz->name); + $table->is_downloading($download, $filename, + $COURSE->shortname . ' ' . format_string($quiz->name, true)); + if ($table->is_downloading()) { + raise_memory_limit(MEMORY_EXTRA); + } + + // Process actions. if (empty($currentgroup) || $groupstudents) { if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) { if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) { require_capability('mod/quiz:deleteattempts', $this->context); - $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed, $groupstudents); + $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed); redirect($reporturl->out(false, $displayoptions)); } + } else if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) { if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) { - $this->regrade_selected_attempts($quiz, $attemptids, $groupstudents); + require_capability('mod/quiz:regrade', $this->context); + $this->regrade_attempts($quiz, false, $groupstudents, $attemptids); redirect($reporturl->out(false, $displayoptions)); } } } - //work out the sql for this table. - if ($detailedmarks) { - $questions = quiz_report_load_questions($quiz); - } else { - $questions = array(); - } - $table = new quiz_report_overview_table($quiz , $qmsubselect, $groupstudents, - $students, $detailedmarks, $questions, $candelete, $reporturl, - $displayoptions, $this->context); - $table->is_downloading($download, get_string('reportoverview','quiz'), - "$COURSE->shortname ".format_string($quiz->name,true)); - if (!$table->is_downloading()) { - // Only print headers if not asked to download data - $this->print_header_and_tabs($cm, $course, $quiz, "overview"); + if ($regradeall && confirm_sesskey()) { + require_capability('mod/quiz:regrade', $this->context); + $this->regrade_attempts($quiz, false, $groupstudents); + redirect($reporturl->out(false, $displayoptions), '', 5); + + } else if ($regradealldry && confirm_sesskey()) { + require_capability('mod/quiz:regrade', $this->context); + $this->regrade_attempts($quiz, true, $groupstudents); + redirect($reporturl->out(false, $displayoptions), '', 5); + + } else if ($regradealldrydo && confirm_sesskey()) { + require_capability('mod/quiz:regrade', $this->context); + $this->regrade_attempts_needing_it($quiz, $groupstudents); + redirect($reporturl->out(false, $displayoptions), '', 5); } - if ($regradeall && confirm_sesskey()) { - $this->regrade_all(false, $quiz, $groupstudents); - } else if ($regradealldry && confirm_sesskey()) { - $this->regrade_all(true, $quiz, $groupstudents); - } else if ($regradealldrydo && confirm_sesskey()) { - $this->regrade_all_needed($quiz, $groupstudents); - } - if ($regradeall || $regradealldry || $regradealldrydo) { - redirect($reporturl->out(false, $displayoptions), '', 5); + // Start output. + if (!$table->is_downloading()) { + // Only print headers if not asked to download data + $this->print_header_and_tabs($cm, $course, $quiz, 'overview'); } if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used @@ -176,6 +177,13 @@ class quiz_overview_report extends quiz_default_report { } } + // Print information on the number of existing attempts + if (!$table->is_downloading()) { //do not print notices when downloading + if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) { + echo '
' . $strattemptnum . '
'; + } + } + $nostudents = false; if (!$students) { if (!$table->is_downloading()) { @@ -188,21 +196,15 @@ class quiz_overview_report extends quiz_default_report { } $nostudents = true; } + if (!$table->is_downloading()) { // Print display options - $mform->set_data($displayoptions +compact('detailedmarks', 'pagesize')); $mform->display(); - - // Print information on the number of existing attempts - if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) { - echo '
' . $strattemptnum . '
'; - } } if (!$nostudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL)) { - // Construct the SQL - $fields = $DB->sql_concat('u.id', '\'#\'', 'COALESCE(qa.attempt, \'0\')').' AS uniqueid, '; + $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid, '; if ($qmsubselect) { $fields .= "(CASE " . @@ -211,90 +213,44 @@ class quiz_overview_report extends quiz_default_report { "END) AS gradedattempt, "; } - $fields .='qa.uniqueid AS attemptuniqueid, qa.id AS attempt, ' . - 'u.id AS userid, u.idnumber, u.firstname, u.lastname, u.picture, u.imagealt, u.email, '. - 'qa.sumgrades, qa.timefinish, qa.timestart, qa.timefinish - qa.timestart AS duration '; - - // This part is the same for all cases - join users and quiz_attempts tables - $from = '{user} u '; - $from .= 'LEFT JOIN {quiz_attempts} qa ON qa.userid = u.id AND qa.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 = 'qa.id IS NOT NULL AND qa.preview = 0'; - break; - case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH: - // Show only students with attempts - list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); - $params += $allowed_params; - $where = "u.id $allowed_usql AND qa.preview = 0 AND qa.id IS NOT NULL"; - break; - case QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO: - // Show only students without attempts - list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); - $params += $allowed_params; - $where = "u.id $allowed_usql AND qa.id IS NULL"; - break; - case QUIZ_REPORT_ATTEMPTS_ALL_STUDENTS: - // Show all students with or without attempts - list($allowed_usql, $allowed_params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED, 'u0000'); - $params += $allowed_params; - $where = "u.id $allowed_usql AND (qa.preview = 0 OR qa.preview IS NULL)"; - break; - } + list($fields, $from, $where, $params) = + $this->base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $allowed); $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params); - $sqlobject = new stdClass(); - $sqlobject->from = $from; - $sqlobject->where = $where; - $sqlobject->params = $params; - //test to see if there are any regraded attempts to be listed. - if (quiz_get_regraded_qs($sqlobject, 0, 1)) { - $regradedattempts = true; - } else { - $regradedattempts = false; - } - $fields .= ', COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) AS regraded'; + // Test to see if there are any regraded attempts to be listed. + $fields .= ", COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_overview_regrades} qqr WHERE qqr.questionusageid = quiza.uniqueid), -1) AS regraded"; if ($regradefilter) { - $where .= ' AND COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_question_regrade} qqr WHERE qqr.attemptid = qa.uniqueid),-1) !=\'-1\''; + $where .= " AND COALESCE((SELECT MAX(qqr.regraded) FROM {quiz_overview_regrades} qqr WHERE qqr.questionusageid = quiza.uniqueid), -1) <> -1"; } $table->set_sql($fields, $from, $where, $params); - // Define table columns - $columns = array(); - $headers = array(); - if (!$table->is_downloading()) { //do not print notices when downloading - //regrade buttons + if (!$table->is_downloading()) { + // Regrade buttons if (has_capability('mod/quiz:regrade', $this->context)) { - $countregradeneeded = $this->count_regrade_all_needed($quiz, $groupstudents); + $regradesneeded = $this->count_question_attempts_needing_regrade( + $quiz, $groupstudents); if ($currentgroup) { $a= new stdClass(); $a->groupname = groups_get_group_name($currentgroup); $a->coursestudents = get_string('participants'); - $a->countregradeneeded = $countregradeneeded; + $a->countregradeneeded = $regradesneeded; $regradealldrydolabel = get_string('regradealldrydogroup', 'quiz_overview', $a); $regradealldrylabel = get_string('regradealldrygroup', 'quiz_overview', $a); $regradealllabel = get_string('regradeallgroup', 'quiz_overview', $a); } else { - $regradealldrydolabel = get_string('regradealldrydo', 'quiz_overview', $countregradeneeded); + $regradealldrydolabel = get_string('regradealldrydo', 'quiz_overview', $regradesneeded); $regradealldrylabel = get_string('regradealldry', 'quiz_overview'); $regradealllabel = get_string('regradeall', 'quiz_overview'); } - $displayurl = new moodle_url($reporturl, $displayoptions); + $displayurl = new moodle_url($reporturl, $displayoptions + array('sesskey' => sesskey())); echo '
'; echo '
'; echo '
'; echo html_writer::input_hidden_params($displayurl); - echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())) . "\n"; echo ''; echo ''; - if ($countregradeneeded) { + if ($regradesneeded) { echo ''; } echo '
'; @@ -307,90 +263,49 @@ class quiz_overview_report extends quiz_default_report { } } + // Define table columns + $columns = array(); + $headers = array(); + if (!$table->is_downloading() && $candelete) { - $columns[]= 'checkbox'; - $headers[]= NULL; + $columns[] = 'checkbox'; + $headers[] = NULL; } - 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'); - } + $this->add_user_columns($table, $columns, $headers); - if ($CFG->grade_report_showuseridnumber) { - $columns[]= 'idnumber'; - $headers[]= get_string('idnumber'); - } - - $columns[]= 'timestart'; - $headers[]= get_string('startedon', 'quiz'); - - $columns[]= 'timefinish'; - $headers[]= get_string('timecompleted','quiz'); - - $columns[]= 'duration'; - $headers[]= get_string('attemptduration', 'quiz'); + $this->add_time_columns($columns, $headers); if ($detailedmarks) { - foreach ($questions as $id => $question) { + foreach ($questions as $slot => $question) { // Ignore questions of zero length - $columns[] = 'qsgrade'.$id; - $header = '#'.$question->number; + $columns[] = 'qsgrade' . $slot; + $header = get_string('qbrief', 'quiz', $question->number); if (!$table->is_downloading()) { - $header .='
'; + $header .= '
'; } else { - $header .=' '; + $header .= ' '; } - $header .='--/'.quiz_rescale_grade($question->maxgrade, $quiz, 'question'); + $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question'); $headers[] = $header; - $question->formattedname = strip_tags(format_string($question->name)); } } - if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) && $regradedattempts) { + + if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) && + $this->has_regraded_questions($from, $where, $params)) { $columns[] = 'regraded'; $headers[] = get_string('regrade', 'quiz_overview'); } - if ($showgrades) { - $columns[] = 'sumgrades'; - $headers[] = get_string('grade', 'quiz').'/'.quiz_format_grade($quiz, $quiz->grade); - } - if ($hasfeedback) { - $columns[] = 'feedbacktext'; - $headers[] = get_string('feedback', 'quiz'); - } + $this->add_grade_columns($quiz, $columns, $headers); - $table->define_columns($columns); - $table->define_headers($headers); - $table->sortable(true, 'uniqueid'); - - // Set up the table - $table->define_baseurl($reporturl->out(true, $displayoptions)); - - $table->collapsible(true); - - $table->no_sorting('feedbacktext'); - - $table->column_class('picture', 'picture'); - $table->column_class('lastname', 'bold'); - $table->column_class('firstname', 'bold'); - $table->column_class('fullname', 'bold'); - $table->column_class('sumgrades', 'bold'); - - $table->set_attribute('id', 'attempts'); + $this->set_up_table_columns($table, $columns, $headers, $reporturl, $displayoptions, false); + $table->set_attribute('class', 'generaltable generalbox grades'); $table->out($pagesize, true); } - if (!$table->is_downloading() && $showgrades) { + + if (!$table->is_downloading() && $this->should_show_grades($quiz)) { if ($currentgroup && $groupstudents) { list($usql, $params) = $DB->get_in_or_equal($groupstudents); $params[] = $quiz->id; @@ -401,6 +316,7 @@ class quiz_overview_report extends quiz_default_report { echo '
'.$graphname.'
'; } } + if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) { $graphname = get_string('overviewreportgraph', 'quiz_overview'); $imageurl = $CFG->wwwroot.'/mod/quiz/report/overview/overviewgraph.php?id='.$quiz->id; @@ -410,261 +326,226 @@ class quiz_overview_report extends quiz_default_report { } return true; } + /** - * @param bool changedb whether to change contents of state and grades - * tables. + * Regrade a particular quiz attempt. Either for real ($dryrun = false), or + * as a pretend regrade to see which fractions would change. The outcome is + * stored in the quiz_overview_regrades table. + * + * Note, $attempt is not upgraded in the database. The caller needs to do that. + * However, $attempt->sumgrades is updated, if this is not a dry run. + * + * @param object $attempt the quiz attempt to regrade. + * @param boolean $dryrun if true, do a pretend regrade, otherwise do it for real. + * @param array $slots if null, regrade all questions, otherwise, just regrade + * the quetsions with those slots. */ - function regrade_all($dry, $quiz, $groupstudents) { - global $DB, $OUTPUT; - if (!has_capability('mod/quiz:regrade', $this->context)) { - echo $OUTPUT->notification(get_string('regradenotallowed', 'quiz')); - return true; - } - // Fetch all attempts - if ($groupstudents) { - list($usql, $params) = $DB->get_in_or_equal($groupstudents); - $select = "userid $usql AND "; - } else { - $select = ''; - $params = array(); - } - $select .= "quiz = ? AND preview = 0"; - $params[] = $quiz->id; - if (!$attempts = $DB->get_records_select('quiz_attempts', $select, $params)) { - echo $OUTPUT->heading(get_string('noattempts', 'quiz')); - return true; - } - - $this->clear_regrade_table($quiz, $groupstudents); - - // Fetch all questions - $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance', - '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); - - // Print heading - echo $OUTPUT->heading(get_string('regradingquiz', 'quiz', format_string($quiz->name))); - $qstodo = count($questions); - $qsdone = 0; - if ($qstodo > 1) { - $qpb = new progress_bar('qregradingbar', 500, true); - $qpb->update($qsdone, $qstodo, "Question $qsdone of $qstodo"); - } - $apb = new progress_bar('aregradingbar', 500, true); - - // Loop through all questions and all attempts and regrade while printing progress info - $attemptstodo = count($attempts); - foreach ($questions as $question) { - $attemptsdone = 0; - $apb->restart(); - echo '

'.get_string('regradingquestion', 'quiz', $question->name).'

'; - @flush();@ob_flush(); - foreach ($attempts as $attempt) { - set_time_limit(30); - $changed = regrade_question_in_attempt($question, $attempt, $quiz, true, $dry); - - $attemptsdone++; - $a = new stdClass(); - $a->done = $attemptsdone; - $a->todo = $attemptstodo; - $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a)); - } - $qsdone++; - if (isset($qpb)) { - $a = new stdClass(); - $a->done = $qsdone; - $a->todo = $qstodo; - $qpb->update($qsdone, $qstodo, get_string('qprogress', 'quiz_overview', $a)); - } - // the following makes sure that the output is sent immediately. - @flush();@ob_flush(); - } - - if (!$dry) { - $this->check_overall_grades($quiz, $groupstudents); - } - } - function count_regrade_all_needed($quiz, $groupstudents) { + protected function regrade_attempt($attempt, $dryrun = false, $slots = null) { global $DB; - // Fetch all attempts that need regrading - if ($groupstudents) { - list($usql, $params) = $DB->get_in_or_equal($groupstudents); - $where = "qa.userid $usql AND "; - } else { - $where = ''; - $params = array(); + + $transaction = $DB->start_delegated_transaction(); + + $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid); + + if (is_null($slots)) { + $slots = $quba->get_slots(); } - $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0"; - $params[] = $quiz->id; - return $DB->get_field_sql('SELECT COUNT(1) FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params); + + $finished = $attempt->timefinish > 0; + foreach ($slots as $slot) { + $qqr = new stdClass; + $qqr->oldfraction = $quba->get_question_fraction($slot); + + $quba->regrade_question($slot, $finished); + + $qqr->newfraction = $quba->get_question_fraction($slot); + + if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) { + $qqr->questionusageid = $quba->get_id(); + $qqr->slot = $slot; + $qqr->regraded = empty($dryrun); + $qqr->timemodified = time(); + $DB->insert_record('quiz_overview_regrades', $qqr, false); + } + } + + if (!$dryrun) { + question_engine::save_questions_usage_by_activity($quba); + } + + $transaction->allow_commit(); } - function regrade_all_needed($quiz, $groupstudents) { - global $DB, $OUTPUT; - if (!has_capability('mod/quiz:regrade', $this->context)) { - echo $OUTPUT->notification(get_string('regradenotallowed', 'quiz')); + + /** + * Regrade attempts for this quiz, exactly which attempts are regraded is + * controlled by the parameters. + * @param object $quiz the quiz settings. + * @param boolean $dryrun if true, do a pretend regrade, otherwise do it for real. + * @param array $groupstudents blank for all attempts, otherwise regrade attempts + * for these users. + * @param array $attemptids blank for all attempts, otherwise only regrade + * attempts whose id is in this list. + */ + protected function regrade_attempts($quiz, $dryrun = false, + $groupstudents = array(), $attemptids = array()) { + global $DB; + + $where = "quiz = ? AND preview = 0"; + $params = array($quiz->id); + + if ($groupstudents) { + list($usql, $uparams) = $DB->get_in_or_equal($groupstudents); + $where .= " AND userid $usql"; + $params = array_merge($params, $uparams); + } + + if ($attemptids) { + list($asql, $aparams) = $DB->get_in_or_equal($attemptids); + $where .= " AND id $asql"; + $params = array_merge($params, $aparams); + } + + $attempts = $DB->get_records_select('quiz_attempts', $where, $params); + if (!$attempts) { return; } - // Fetch all attempts that need regrading - if ($groupstudents) { - list($usql, $params) = $DB->get_in_or_equal($groupstudents); - $where = "qa.userid $usql AND "; - } else { - $where = ''; - $params = array(); - } - $where .= "qa.quiz = ? AND qa.preview = 0 AND qa.uniqueid = qqr.attemptid AND qqr.regraded = 0"; - $params[] = $quiz->id; - if (!$attempts = $DB->get_records_sql('SELECT qa.*, qqr.questionid FROM {quiz_attempts} qa, {quiz_question_regrade} qqr WHERE '. $where, $params)) { - echo $OUTPUT->heading(get_string('noattemptstoregrade', 'quiz_overview')); - return true; - } + $this->clear_regrade_table($quiz, $groupstudents); - // Fetch all questions - $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance', - '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); - // Print heading - echo $OUTPUT->heading(get_string('regradingquiz', 'quiz', format_string($quiz->name))); - - $apb = new progress_bar('aregradingbar', 500, true); - - // Loop through all questions and all attempts and regrade while printing progress info - $attemptstodo = count($attempts); - $attemptsdone = 0; - @flush();@ob_flush(); - $attemptschanged = array(); foreach ($attempts as $attempt) { - $question = $questions[$attempt->questionid]; - $changed = regrade_question_in_attempt($question, $attempt, $quiz, true); - if ($changed) { - $attemptschanged[] = $attempt->uniqueid; - $usersschanged[] = $attempt->userid; - } - if (!empty($apb)) { - $attemptsdone++; - $a = new stdClass(); - $a->done = $attemptsdone; - $a->todo = $attemptstodo; - $apb->update($attemptsdone, $attemptstodo, get_string('attemptprogress', 'quiz_overview', $a)); - } + set_time_limit(30); + $this->regrade_attempt($attempt, $dryrun); + } + + if (!$dryrun) { + $this->update_overall_grades($quiz); } - $this->check_overall_grades($quiz, array(), $attemptschanged); } - function clear_regrade_table($quiz, $groupstudents) { + /** + * Regrade those questions in those attempts that are marked as needing regrading + * in the quiz_overview_regrades table. + * @param object $quiz the quiz settings. + * @param array $groupstudents blank for all attempts, otherwise regrade attempts + * for these users. + */ + protected function regrade_attempts_needing_it($quiz, $groupstudents) { global $DB; + + $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0"; + $params = array($quiz->id); + // Fetch all attempts that need regrading + if ($groupstudents) { + list($usql, $uparams) = $DB->get_in_or_equal($groupstudents); + $where .= " AND quiza.userid $usql"; + $params += $uparams; + } + + $toregrade = $DB->get_records_sql(" + SELECT quiza.uniqueid, qqr.slot + FROM {quiz_attempts} quiza + JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid + WHERE $where", $params); + + if (!$toregrade) { + return; + } + + $attemptquestions = array(); + foreach ($toregrade as $row) { + $attemptquestions[$row->uniqueid][] = $row->slot; + } + $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid', array_keys($attemptquestions)); + + $this->clear_regrade_table($quiz, $groupstudents); + + foreach ($attempts as $attempt) { + set_time_limit(30); + $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]); + } + + $this->update_overall_grades($quiz); + } + + /** + * Count the number of attempts in need of a regrade. + * @param object $quiz the quiz settings. + * @param array $groupstudents user ids. If this is given, only data relating + * to these users is cleared. + */ + protected function count_question_attempts_needing_regrade($quiz, $groupstudents) { + global $DB; + + $usertest = ''; + $params = array(); + if ($groupstudents) { + list($usql, $params) = get_in_or_equal($groupstudents); + $usertest = "quiza.userid $usql AND "; + } + + $params[] = $quiz->id; + $sql = "SELECT COUNT(DISTINCT quiza.id) + FROM {quiz_attempts} quiza + JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid + WHERE + $usertest + quiza.quiz = ? AND + quiza.preview = 0 AND + qqr.regraded = 0"; + return $DB->count_records_sql($sql, $params); + } + + /** + * Are there any pending regrades in the table we are going to show? + * @param string $from tables used by the main query. + * @param string $where where clause used by the main query. + * @param array $params required by the SQL. + * @return bool whether there are pending regrades. + */ + protected function has_regraded_questions($from, $where, $params) { + global $DB; + $qubaids = new qubaid_join($from, 'uniqueid', $where, $params); + return $DB->record_exists_select('quiz_overview_regrades', + 'questionusageid ' . $qubaids->usage_id_in(), $qubaids->usage_id_in_params()); + } + + /** + * Remove all information about pending/complete regrades from the database. + * @param object $quiz the quiz settings. + * @param array $groupstudents user ids. If this is given, only data relating + * to these users is cleared. + */ + protected function clear_regrade_table($quiz, $groupstudents) { + global $DB; + + // Fetch all attempts that need regrading + $where = ''; + $params = array(); if ($groupstudents) { list($usql, $params) = $DB->get_in_or_equal($groupstudents); $where = "userid $usql AND "; - } else { - $usql = ''; - $where = ''; - $params = array(); } + $params[] = $quiz->id; - $delsql = 'DELETE FROM {quiz_question_regrade} WHERE attemptid IN - (SELECT uniqueid FROM {quiz_attempts} WHERE ' . $where . ' quiz = ?)'; - if (!$DB->execute($delsql, $params)) { - print_error('err_failedtodeleteregrades', 'quiz_overview'); - } + $DB->delete_records_select('quiz_overview_regrades', + "questionusageid IN ( + SELECT uniqueid + FROM {quiz_attempts} + WHERE $where quiz = ? + )", $params); } - function check_overall_grades($quiz, $userids=array(), $attemptids=array()) { - global $DB; - //recalculate $attempt->sumgrade - //already updated in regrade_question_in_attempt - $sql = "UPDATE {quiz_attempts} SET sumgrades= " . - "COALESCE((SELECT SUM(qs.grade) FROM {question_sessions} qns, {question_states} qs " . - "WHERE qns.newgraded = qs.id AND qns.attemptid = {quiz_attempts}.uniqueid ), 0) WHERE "; - $attemptsql=''; - if (!$attemptids) { - if ($userids) { - list($usql, $params) = $DB->get_in_or_equal($userids); - $attemptsql .= "{quiz_attempts}.userid $usql AND "; - } else { - $params = array(); - } - $attemptsql .= "{quiz_attempts}.quiz =? AND preview = 0"; - $params[] = $quiz->id; - } else { - list($asql, $params) = $DB->get_in_or_equal($attemptids); - $attemptsql .= "{quiz_attempts}.uniqueid $asql"; - } - $sql .= $attemptsql; - if (!$DB->execute($sql, $params)) { - print_error('err_failedtorecalculateattemptgrades', 'quiz_overview'); - } - - // Update the overall quiz grades - if ($attemptids) { - //make sure we fetch all attempts for users to calculate grade. - //not just those that have changed. - $sql = "SELECT qa2.* FROM {quiz_attempts} qa2 WHERE " . - "qa2.userid IN (SELECT DISTINCT userid FROM {quiz_attempts} WHERE $attemptsql) " . - "AND qa2.timefinish > 0"; - } else { - $sql = "SELECT * FROM {quiz_attempts} WHERE $attemptsql AND timefinish > 0"; - } - if ($attempts = $DB->get_records_sql($sql, $params)) { - $attemptsbyuser = quiz_report_index_by_keys($attempts, array('userid', 'id')); - foreach($attemptsbyuser as $userid => $attemptsforuser) { - quiz_save_best_grade($quiz, $userid, $attemptsforuser); - } - } - } - - function delete_selected_attempts($quiz, $cm, $attemptids, $allowed, $groupstudents) { - global $DB, $COURSE; - 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; - } - if ($groupstudents && !array_key_exists($attempt->userid, $groupstudents)) { - // Additional check in groups mode. - continue; - } - add_to_log($COURSE->id, 'quiz', 'delete attempt', 'report.php?id=' . $cm->id, - $attemptid, $cm->id); - quiz_delete_attempt($attempt, $quiz); - } - } - - function regrade_selected_attempts($quiz, $attemptids, $groupstudents) { - global $DB; - require_capability('mod/quiz:regrade', $this->context); - if ($groupstudents) { - list($usql, $params) = $DB->get_in_or_equal($groupstudents); - $where = "qa.userid $usql AND "; - } else { - $params = array(); - $where = ''; - } - list($asql, $aparams) = $DB->get_in_or_equal($attemptids); - $where = "qa.id $asql AND "; - $params = array_merge($params, $aparams); - - $where .= "qa.quiz = ? AND qa.preview = 0"; - $params[] = $quiz->id; - if (!$attempts = $DB->get_records_sql('SELECT qa.* FROM {quiz_attempts} qa WHERE '. $where, $params)) { - print_error('noattemptstoregrade', 'quiz_overview'); - } - - // Fetch all questions - $questions = question_load_questions(explode(',',quiz_questions_in_quiz($quiz->questions)), 'qqi.grade AS maxgrade, qqi.id AS instance', - '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question'); - $updateoverallgrades = array(); - foreach($attempts as $attempt) { - foreach ($questions as $question) { - $changed = regrade_question_in_attempt($question, $attempt, $quiz, true); - } - $updateoverallgrades[] = $attempt->uniqueid; - } - $this->check_overall_grades($quiz, array(), $updateoverallgrades); + /** + * Update the final grades for all attempts. This method is used following + * a regrade. + * @param object $quiz the quiz settings. + * @param array $userids only update scores for these userids. + * @param array $attemptids attemptids only update scores for these attempt ids. + */ + protected function update_overall_grades($quiz) { + quiz_update_all_attempt_sumgrades($quiz); + quiz_update_all_final_grades($quiz); + quiz_update_grades($quiz); } } diff --git a/mod/quiz/report/overview/version.php b/mod/quiz/report/overview/version.php index 61cc45d9f6b..3998229af46 100644 --- a/mod/quiz/report/overview/version.php +++ b/mod/quiz/report/overview/version.php @@ -1,10 +1,27 @@ version = 2009091400; // The (date) version of this module +// 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 . +/** + * Quiz overview report version information. + * + * @package quiz + * @subpackage overview + * @copyright 2008 Jamie Pratt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$plugin->version = 2011021600; // The (date) version of this module diff --git a/mod/quiz/report/responses/responses_table.php b/mod/quiz/report/responses/responses_table.php index 2b8f3b82b1a..f70b5005156 100644 --- a/mod/quiz/report/responses/responses_table.php +++ b/mod/quiz/report/responses/responses_table.php @@ -69,6 +69,7 @@ class quiz_report_responses_table extends quiz_attempt_report_table { return; } + // TODO add back are you sure, and convert to html_writer. echo '
'; echo ''. get_string('selectall', 'quiz').' / '; diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php index c723635c474..1aa4cf9f2cf 100644 --- a/mod/quiz/report/statistics/qstats.php +++ b/mod/quiz/report/statistics/qstats.php @@ -177,7 +177,10 @@ class quiz_statistics_question_stats { $this->initial_question_walker($subquestion->_stats); if ($subquestionstats[$qid]->differentweights) { - notify(get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $this->subquestions[$qid]->name)); + // 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 ($subquestion->_stats->usedin) { diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 684ebeb8537..97efa62faa0 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -540,7 +540,7 @@ class quiz_statistics_report extends quiz_default_report { $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' => 'mdl-align')); + array('class' => 'graph')); } /** diff --git a/question/engine/datalib.php b/question/engine/datalib.php index e2ac09b8062..abe2dbb2985 100644 --- a/question/engine/datalib.php +++ b/question/engine/datalib.php @@ -476,13 +476,14 @@ $sqlorderby */ public function load_average_marks(qubaid_condition $qubaids, $slots = null) { if (!empty($slots)) { - list($slottest, $params) = get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000'); + list($slottest, $slotsparams) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot0000'); $slotwhere = " AND qa.slot $slottest"; } else { $slotwhere = ''; + $params = array(); } - list($statetest) = get_in_or_equal(array( + list($statetest, $stateparams) = $this->db->get_in_or_equal(array( question_state::$gaveup, question_state::$gradedwrong, question_state::$gradedpartial, @@ -490,9 +491,9 @@ $sqlorderby question_state::$mangaveup, question_state::$mangrwrong, question_state::$mangrpartial, - question_state::$mangrright)); + question_state::$mangrright), SQL_PARAMS_NAMED, 'st00'); - $records = get_records_sql(" + return $this->db->get_records_sql(" SELECT qa.slot, AVG(COALESCE(qas.fraction, 0)) AS averagefraction, @@ -510,9 +511,7 @@ WHERE GROUP BY qa.slot ORDER BY qa.slot - "); - - return $records; + ", $slotsparams + $stateparams + $qubaids->from_where_params()); } /** @@ -659,7 +658,7 @@ ORDER BY if (empty($qaids)) { return; } - list($test, $params) = get_in_or_equal($qaids); + list($test, $params) = $this->db->get_in_or_equal($qaids); $this->db->delete_records_select('question_attempt_step_data', "attemptstepid IN ( SELECT qas.id FROM {question_attempt_steps} qas @@ -1060,6 +1059,7 @@ class qubaid_join extends qubaid_condition { * @param string $usageidcolumn the column in $from that should be * made equal to the usageid column in the JOIN clause. * @param string $where SQL fragment to go in the where clause. + * @param array $params required by the SQL. You must use named parameters. */ public function __construct($from, $usageidcolumn, $where = '', $params = array()) { $this->from = $from; diff --git a/question/todo/diffstat.txt b/question/todo/diffstat.txt index 373364e8956..2e92dcd0dcd 100644 --- a/question/todo/diffstat.txt +++ b/question/todo/diffstat.txt @@ -27,7 +27,7 @@ DONE lib/simpletest/testsimpletestlib.php | 116 + Already in 2.0 DONE lib/simpletest/testweblib.php | 4 + Useful new test. See MDL-24060. DONE lib/simpletestlib.php | 290 ++ Useful new assertions. DONE lib/simpletestlib/dumper.php | 3 + Not needed. KILL - lib/tablelib.php | 1224 +++++++-- TODO. See MDL-25716 +DONE lib/tablelib.php | 1224 +++++++-- TODO. See MDL-25716 DONE lib/weblib.php | 10 +- Improve html_to_text. See MDL-24060. DONE lib/xmlize.php | 74 +- In 2.0 DONE mod/assignment/lib.php | 2 +- TODO MDL-25716 @@ -129,15 +129,15 @@ DONE mod/quiz/report/grading/report.php | 842 ++++--- DONE mod/quiz/report/grading/version.php | 26 + DONE lang/en_utf8/quiz_grading.php | 18 - - mod/quiz/report/overview/db/install.xml | 22 + - mod/quiz/report/overview/db/upgrade.php | 152 + - mod/quiz/report/overview/lang/en_utf8/quiz_overview.php | 54 + - mod/quiz/report/overview/overview_table.php | 321 +++ - mod/quiz/report/overview/overviewgraph.php | 168 +- - mod/quiz/report/overview/overviewsettings_form.php | 69 +- - mod/quiz/report/overview/report.php | 999 +++---- - mod/quiz/report/overview/version.php | 26 + - lang/en_utf8/quiz_overview.php | 30 - +DONE mod/quiz/report/overview/db/install.xml | 22 + +DONE mod/quiz/report/overview/db/upgrade.php | 152 + +DONE mod/quiz/report/overview/lang/en_utf8/quiz_overview.php | 54 + +DONE mod/quiz/report/overview/overview_table.php | 321 +++ +DONE mod/quiz/report/overview/overviewgraph.php | 168 +- +DONE mod/quiz/report/overview/overviewsettings_form.php | 69 +- +DONE mod/quiz/report/overview/report.php | 999 +++---- +DONE mod/quiz/report/overview/version.php | 26 + +DONE lang/en_utf8/quiz_overview.php | 30 - DONE mod/quiz/report/responses/lang/en_utf8/quiz_responses.php | 19 + DONE mod/quiz/report/responses/report.php | 242 ++