2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * This file defines the quiz overview report class.
21 * @subpackage overview
22 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot.'/mod/quiz/report/attemptsreport.php');
30 require_once($CFG->dirroot.'/mod/quiz/report/overview/overviewsettings_form.php');
31 require_once($CFG->dirroot.'/mod/quiz/report/overview/overview_table.php');
35 * Quiz report subclass for the overview (grades) report.
37 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class quiz_overview_report extends quiz_attempt_report {
42 public function display($quiz, $cm, $course) {
43 global $CFG, $COURSE, $DB, $OUTPUT;
45 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
47 $download = optional_param('download', '', PARAM_ALPHA);
49 list($currentgroup, $students, $groupstudents, $allowed) =
50 $this->load_relevant_students($cm);
52 $pageoptions = array();
53 $pageoptions['id'] = $cm->id;
54 $pageoptions['mode'] = 'overview';
56 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
57 $qmsubselect = quiz_report_qm_filter_select($quiz);
59 $mform = new mod_quiz_report_overview_settings($reporturl,
60 array('qmsubselect' => $qmsubselect, 'quiz' => $quiz,
61 'currentgroup' => $currentgroup, 'context' => $this->context));
63 if ($fromform = $mform->get_data()) {
65 $regradealldry = false;
66 $regradealldrydo = false;
67 $attemptsmode = $fromform->attemptsmode;
69 $qmfilter = $fromform->qmfilter;
73 $regradefilter = !empty($fromform->regradefilter);
74 set_user_preference('quiz_report_overview_detailedmarks', $fromform->detailedmarks);
75 set_user_preference('quiz_report_pagesize', $fromform->pagesize);
76 $detailedmarks = $fromform->detailedmarks;
77 $pagesize = $fromform->pagesize;
80 $regradeall = optional_param('regradeall', 0, PARAM_BOOL);
81 $regradealldry = optional_param('regradealldry', 0, PARAM_BOOL);
82 $regradealldrydo = optional_param('regradealldrydo', 0, PARAM_BOOL);
83 $attemptsmode = optional_param('attemptsmode', null, PARAM_INT);
85 $qmfilter = optional_param('qmfilter', 0, PARAM_INT);
89 $regradefilter = optional_param('regradefilter', 0, PARAM_INT);
90 $detailedmarks = get_user_preferences('quiz_report_overview_detailedmarks', 1);
91 $pagesize = get_user_preferences('quiz_report_pagesize', 0);
94 $this->validate_common_options($attemptsmode, $pagesize, $course, $currentgroup);
95 $displayoptions = array();
96 $displayoptions['attemptsmode'] = $attemptsmode;
97 $displayoptions['qmfilter'] = $qmfilter;
98 $displayoptions['regradefilter'] = $regradefilter;
100 $mform->set_data($displayoptions +
101 array('detailedmarks' => $detailedmarks, 'pagesize' => $pagesize));
103 if (!$this->should_show_grades($quiz)) {
107 // We only want to show the checkbox to delete attempts
108 // if the user has permissions and if the report mode is showing attempts.
109 $candelete = has_capability('mod/quiz:deleteattempts', $this->context)
110 && ($attemptsmode != QUIZ_REPORT_ATTEMPTS_STUDENTS_WITH_NO);
112 if ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL) {
113 // This option is only available to users who can access all groups in
114 // groups mode, so setting allowed to empty (which means all quiz attempts
115 // are accessible, is not a security porblem.
119 // Load the required questions.
120 if ($detailedmarks) {
121 $questions = quiz_report_get_significant_questions($quiz);
123 $questions = array();
126 $table = new quiz_report_overview_table($quiz, $this->context, $qmsubselect,
127 $groupstudents, $students, $detailedmarks, $questions, $candelete,
128 $reporturl, $displayoptions);
129 $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
130 $course->shortname, $quiz->name);
131 $table->is_downloading($download, $filename,
132 $COURSE->shortname . ' ' . format_string($quiz->name, true));
133 if ($table->is_downloading()) {
134 raise_memory_limit(MEMORY_EXTRA);
138 if (empty($currentgroup) || $groupstudents) {
139 if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
140 if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
141 require_capability('mod/quiz:deleteattempts', $this->context);
142 $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed);
143 redirect($reporturl->out(false, $displayoptions));
146 } else if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
147 if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
148 require_capability('mod/quiz:regrade', $this->context);
149 $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
150 redirect($reporturl->out(false, $displayoptions));
155 if ($regradeall && confirm_sesskey()) {
156 require_capability('mod/quiz:regrade', $this->context);
157 $this->regrade_attempts($quiz, false, $groupstudents);
158 redirect($reporturl->out(false, $displayoptions), '', 5);
160 } else if ($regradealldry && confirm_sesskey()) {
161 require_capability('mod/quiz:regrade', $this->context);
162 $this->regrade_attempts($quiz, true, $groupstudents);
163 redirect($reporturl->out(false, $displayoptions), '', 5);
165 } else if ($regradealldrydo && confirm_sesskey()) {
166 require_capability('mod/quiz:regrade', $this->context);
167 $this->regrade_attempts_needing_it($quiz, $groupstudents);
168 redirect($reporturl->out(false, $displayoptions), '', 5);
172 if (!$table->is_downloading()) {
173 // Only print headers if not asked to download data
174 $this->print_header_and_tabs($cm, $course, $quiz, 'overview');
177 if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used
178 if (!$table->is_downloading()) {
179 groups_print_activity_menu($cm, $reporturl->out(true, $displayoptions));
183 // Print information on the number of existing attempts
184 if (!$table->is_downloading()) { //do not print notices when downloading
185 if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
186 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
190 $hasquestions = quiz_questions_in_quiz($quiz->questions);
191 if (!$table->is_downloading()) {
192 if (!$hasquestions) {
193 echo quiz_no_questions_message($quiz, $cm, $this->context);
194 } else if (!$students) {
195 echo $OUTPUT->notification(get_string('nostudentsyet'));
196 } else if ($currentgroup && !$groupstudents) {
197 echo $OUTPUT->notification(get_string('nostudentsingroup'));
200 // Print display options
204 $hasstudents = $students && (!$currentgroup || $groupstudents);
205 if ($hasquestions && ($hasstudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL))) {
207 $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
212 " WHEN $qmsubselect THEN 1" .
214 "END) AS gradedattempt, ";
217 list($fields, $from, $where, $params) =
218 $this->base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $allowed);
220 $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
222 // Test to see if there are any regraded attempts to be listed.
223 $fields .= ", COALESCE((
224 SELECT MAX(qqr.regraded)
225 FROM {quiz_overview_regrades} qqr
226 WHERE qqr.questionusageid = quiza.uniqueid
228 if ($regradefilter) {
229 $where .= " AND COALESCE((
230 SELECT MAX(qqr.regraded)
231 FROM {quiz_overview_regrades} qqr
232 WHERE qqr.questionusageid = quiza.uniqueid
235 $table->set_sql($fields, $from, $where, $params);
237 if (!$table->is_downloading()) {
239 if (has_capability('mod/quiz:regrade', $this->context)) {
240 $regradesneeded = $this->count_question_attempts_needing_regrade(
241 $quiz, $groupstudents);
244 $a->groupname = groups_get_group_name($currentgroup);
245 $a->coursestudents = get_string('participants');
246 $a->countregradeneeded = $regradesneeded;
247 $regradealldrydolabel =
248 get_string('regradealldrydogroup', 'quiz_overview', $a);
249 $regradealldrylabel =
250 get_string('regradealldrygroup', 'quiz_overview', $a);
252 get_string('regradeallgroup', 'quiz_overview', $a);
254 $regradealldrydolabel =
255 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
256 $regradealldrylabel =
257 get_string('regradealldry', 'quiz_overview');
259 get_string('regradeall', 'quiz_overview');
261 $displayurl = new moodle_url($reporturl,
262 $displayoptions + array('sesskey' => sesskey()));
263 echo '<div class="mdl-align">';
264 echo '<form action="'.$displayurl->out_omit_querystring().'">';
266 echo html_writer::input_hidden_params($displayurl);
267 echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
268 echo '<input type="submit" name="regradealldry" value="' .
269 $regradealldrylabel . '"/>';
270 if ($regradesneeded) {
271 echo '<input type="submit" name="regradealldrydo" value="' .
272 $regradealldrydolabel . '"/>';
278 // Print information on the grading method
279 if ($strattempthighlight = quiz_report_highlighting_grading_method(
280 $quiz, $qmsubselect, $qmfilter)) {
281 echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
285 // Define table columns
289 if (!$table->is_downloading() && $candelete) {
290 $columns[] = 'checkbox';
294 $this->add_user_columns($table, $columns, $headers);
296 $this->add_time_columns($columns, $headers);
298 if ($detailedmarks) {
299 foreach ($questions as $slot => $question) {
300 // Ignore questions of zero length
301 $columns[] = 'qsgrade' . $slot;
302 $header = get_string('qbrief', 'quiz', $question->number);
303 if (!$table->is_downloading()) {
308 $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
309 $headers[] = $header;
313 if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
314 $this->has_regraded_questions($from, $where, $params)) {
315 $columns[] = 'regraded';
316 $headers[] = get_string('regrade', 'quiz_overview');
319 $this->add_grade_columns($quiz, $columns, $headers);
321 $this->set_up_table_columns(
322 $table, $columns, $headers, $reporturl, $displayoptions, false);
323 $table->set_attribute('class', 'generaltable generalbox grades');
325 $table->out($pagesize, true);
328 if (!$table->is_downloading() && $this->should_show_grades($quiz)) {
329 if ($currentgroup && $groupstudents) {
330 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
331 $params[] = $quiz->id;
332 if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
334 $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
335 array('id' => $quiz->id, 'groupid' => $currentgroup));
336 $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
337 groups_get_group_name($currentgroup));
338 echo $OUTPUT->heading($graphname);
339 echo html_writer::tag('div', html_writer::empty_tag('img',
340 array('src' => $imageurl, 'alt' => $graphname)),
341 array('class' => 'graph'));
345 if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
346 $graphname = get_string('overviewreportgraph', 'quiz_overview');
347 $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
348 array('id' => $quiz->id));
349 echo $OUTPUT->heading($graphname);
350 echo html_writer::tag('div', html_writer::empty_tag('img',
351 array('src' => $imageurl, 'alt' => $graphname)),
352 array('class' => 'graph'));
359 * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
360 * as a pretend regrade to see which fractions would change. The outcome is
361 * stored in the quiz_overview_regrades table.
363 * Note, $attempt is not upgraded in the database. The caller needs to do that.
364 * However, $attempt->sumgrades is updated, if this is not a dry run.
366 * @param object $attempt the quiz attempt to regrade.
367 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
368 * @param array $slots if null, regrade all questions, otherwise, just regrade
369 * the quetsions with those slots.
371 protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
374 $transaction = $DB->start_delegated_transaction();
376 $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
378 if (is_null($slots)) {
379 $slots = $quba->get_slots();
382 $finished = $attempt->timefinish > 0;
383 foreach ($slots as $slot) {
384 $qqr = new stdClass();
385 $qqr->oldfraction = $quba->get_question_fraction($slot);
387 $quba->regrade_question($slot, $finished);
389 $qqr->newfraction = $quba->get_question_fraction($slot);
391 if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
392 $qqr->questionusageid = $quba->get_id();
394 $qqr->regraded = empty($dryrun);
395 $qqr->timemodified = time();
396 $DB->insert_record('quiz_overview_regrades', $qqr, false);
401 question_engine::save_questions_usage_by_activity($quba);
404 $transaction->allow_commit();
408 * Regrade attempts for this quiz, exactly which attempts are regraded is
409 * controlled by the parameters.
410 * @param object $quiz the quiz settings.
411 * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
412 * @param array $groupstudents blank for all attempts, otherwise regrade attempts
414 * @param array $attemptids blank for all attempts, otherwise only regrade
415 * attempts whose id is in this list.
417 protected function regrade_attempts($quiz, $dryrun = false,
418 $groupstudents = array(), $attemptids = array()) {
421 $where = "quiz = ? AND preview = 0";
422 $params = array($quiz->id);
424 if ($groupstudents) {
425 list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
426 $where .= " AND userid $usql";
427 $params = array_merge($params, $uparams);
431 list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
432 $where .= " AND id $asql";
433 $params = array_merge($params, $aparams);
436 $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
441 $this->clear_regrade_table($quiz, $groupstudents);
443 foreach ($attempts as $attempt) {
445 $this->regrade_attempt($attempt, $dryrun);
449 $this->update_overall_grades($quiz);
454 * Regrade those questions in those attempts that are marked as needing regrading
455 * in the quiz_overview_regrades table.
456 * @param object $quiz the quiz settings.
457 * @param array $groupstudents blank for all attempts, otherwise regrade attempts
460 protected function regrade_attempts_needing_it($quiz, $groupstudents) {
463 $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
464 $params = array($quiz->id);
466 // Fetch all attempts that need regrading
467 if ($groupstudents) {
468 list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
469 $where .= " AND quiza.userid $usql";
473 $toregrade = $DB->get_records_sql("
474 SELECT quiza.uniqueid, qqr.slot
475 FROM {quiz_attempts} quiza
476 JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
477 WHERE $where", $params);
483 $attemptquestions = array();
484 foreach ($toregrade as $row) {
485 $attemptquestions[$row->uniqueid][] = $row->slot;
487 $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
488 array_keys($attemptquestions));
490 $this->clear_regrade_table($quiz, $groupstudents);
492 foreach ($attempts as $attempt) {
494 $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
497 $this->update_overall_grades($quiz);
501 * Count the number of attempts in need of a regrade.
502 * @param object $quiz the quiz settings.
503 * @param array $groupstudents user ids. If this is given, only data relating
504 * to these users is cleared.
506 protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
511 if ($groupstudents) {
512 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
513 $usertest = "quiza.userid $usql AND ";
516 $params[] = $quiz->id;
517 $sql = "SELECT COUNT(DISTINCT quiza.id)
518 FROM {quiz_attempts} quiza
519 JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
523 quiza.preview = 0 AND
525 return $DB->count_records_sql($sql, $params);
529 * Are there any pending regrades in the table we are going to show?
530 * @param string $from tables used by the main query.
531 * @param string $where where clause used by the main query.
532 * @param array $params required by the SQL.
533 * @return bool whether there are pending regrades.
535 protected function has_regraded_questions($from, $where, $params) {
537 $qubaids = new qubaid_join($from, 'uniqueid', $where, $params);
538 return $DB->record_exists_select('quiz_overview_regrades',
539 'questionusageid ' . $qubaids->usage_id_in(),
540 $qubaids->usage_id_in_params());
544 * Remove all information about pending/complete regrades from the database.
545 * @param object $quiz the quiz settings.
546 * @param array $groupstudents user ids. If this is given, only data relating
547 * to these users is cleared.
549 protected function clear_regrade_table($quiz, $groupstudents) {
552 // Fetch all attempts that need regrading
555 if ($groupstudents) {
556 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
557 $where = "userid $usql AND ";
560 $params[] = $quiz->id;
561 $DB->delete_records_select('quiz_overview_regrades',
562 "questionusageid IN (
565 WHERE $where quiz = ?
570 * Update the final grades for all attempts. This method is used following
572 * @param object $quiz the quiz settings.
573 * @param array $userids only update scores for these userids.
574 * @param array $attemptids attemptids only update scores for these attempt ids.
576 protected function update_overall_grades($quiz) {
577 quiz_update_all_attempt_sumgrades($quiz);
578 quiz_update_all_final_grades($quiz);
579 quiz_update_grades($quiz);