de707783099036b8bee0c1e44942213289ad2344
[moodle.git] / mod / quiz / report / overview / report.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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.
13 //
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/>.
17 /**
18  * This file defines the quiz overview report class.
19  *
20  * @package    quiz
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
24  */
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');
34 /**
35  * Quiz report subclass for the overview (grades) report.
36  *
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
39  */
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()) {
64             $regradeall = false;
65             $regradealldry = false;
66             $regradealldrydo = false;
67             $attemptsmode = $fromform->attemptsmode;
68             if ($qmsubselect) {
69                 $qmfilter = $fromform->qmfilter;
70             } else {
71                 $qmfilter = 0;
72             }
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;
79         } else {
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);
84             if ($qmsubselect) {
85                 $qmfilter = optional_param('qmfilter', 0, PARAM_INT);
86             } else {
87                 $qmfilter = 0;
88             }
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);
92         }
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)) {
104             $detailedmarks = 0;
105         }
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.
116             $allowed = array();
117         }
119         // Load the required questions.
120         if ($detailedmarks) {
121             $questions = quiz_report_get_significant_questions($quiz);
122         } else {
123             $questions = array();
124         }
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);
135         }
137         // Process actions.
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));
144                 }
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));
151                 }
152             }
153         }
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);
169         }
171         // Start output.
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');
175         }
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));
180             }
181         }
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>';
187             }
188         }
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'));
198             }
200             // Print display options
201             $mform->display();
202         }
204         $hasstudents = $students && (!$currentgroup || $groupstudents);
205         if ($hasquestions && ($hasstudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL))) {
206             // Construct the SQL
207             $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
208                     ' AS uniqueid, ';
209             if ($qmsubselect) {
210                 $fields .=
211                     "(CASE " .
212                     "   WHEN $qmsubselect THEN 1" .
213                     "   ELSE 0 " .
214                     "END) AS gradedattempt, ";
215             }
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
227                           ), -1) AS regraded";
228             if ($regradefilter) {
229                 $where .= " AND COALESCE((
230                                     SELECT MAX(qqr.regraded)
231                                       FROM {quiz_overview_regrades} qqr
232                                      WHERE qqr.questionusageid = quiza.uniqueid
233                                 ), -1) <> -1";
234             }
235             $table->set_sql($fields, $from, $where, $params);
237             if (!$table->is_downloading()) {
238                 // Regrade buttons
239                 if (has_capability('mod/quiz:regrade', $this->context)) {
240                     $regradesneeded = $this->count_question_attempts_needing_regrade(
241                             $quiz, $groupstudents);
242                     if ($currentgroup) {
243                         $a= new stdClass();
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);
251                         $regradealllabel =
252                                 get_string('regradeallgroup', 'quiz_overview', $a);
253                     } else {
254                         $regradealldrydolabel =
255                                 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
256                         $regradealldrylabel =
257                                 get_string('regradealldry', 'quiz_overview');
258                         $regradealllabel =
259                                 get_string('regradeall', 'quiz_overview');
260                     }
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().'">';
265                     echo '<div>';
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 . '"/>';
273                     }
274                     echo '</div>';
275                     echo '</form>';
276                     echo '</div>';
277                 }
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>';
282                 }
283             }
285             // Define table columns
286             $columns = array();
287             $headers = array();
289             if (!$table->is_downloading() && $candelete) {
290                 $columns[] = 'checkbox';
291                 $headers[] = null;
292             }
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()) {
304                         $header .= '<br />';
305                     } else {
306                         $header .= ' ';
307                     }
308                     $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
309                     $headers[] = $header;
310                 }
311             }
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');
317             }
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);
326         }
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 = ?",
333                         $params)) {
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'));
342                 }
343             }
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'));
353             }
354         }
355         return true;
356     }
358     /**
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.
362      *
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.
365      *
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.
370      */
371     protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
372         global $DB;
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();
380         }
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();
393                 $qqr->slot = $slot;
394                 $qqr->regraded = empty($dryrun);
395                 $qqr->timemodified = time();
396                 $DB->insert_record('quiz_overview_regrades', $qqr, false);
397             }
398         }
400         if (!$dryrun) {
401             question_engine::save_questions_usage_by_activity($quba);
402         }
404         $transaction->allow_commit();
405     }
407     /**
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
413      * for these users.
414      * @param array $attemptids blank for all attempts, otherwise only regrade
415      * attempts whose id is in this list.
416      */
417     protected function regrade_attempts($quiz, $dryrun = false,
418             $groupstudents = array(), $attemptids = array()) {
419         global $DB;
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);
428         }
430         if ($attemptids) {
431             list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
432             $where .= " AND id $asql";
433             $params = array_merge($params, $aparams);
434         }
436         $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
437         if (!$attempts) {
438             return;
439         }
441         $this->clear_regrade_table($quiz, $groupstudents);
443         foreach ($attempts as $attempt) {
444             set_time_limit(30);
445             $this->regrade_attempt($attempt, $dryrun);
446         }
448         if (!$dryrun) {
449             $this->update_overall_grades($quiz);
450         }
451     }
453     /**
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
458      * for these users.
459      */
460     protected function regrade_attempts_needing_it($quiz, $groupstudents) {
461         global $DB;
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";
470             $params += $uparams;
471         }
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);
479         if (!$toregrade) {
480             return;
481         }
483         $attemptquestions = array();
484         foreach ($toregrade as $row) {
485             $attemptquestions[$row->uniqueid][] = $row->slot;
486         }
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) {
493             set_time_limit(30);
494             $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
495         }
497         $this->update_overall_grades($quiz);
498     }
500     /**
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.
505      */
506     protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
507         global $DB;
509         $usertest = '';
510         $params = array();
511         if ($groupstudents) {
512             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
513             $usertest = "quiza.userid $usql AND ";
514         }
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
520                 WHERE
521                     $usertest
522                     quiza.quiz = ? AND
523                     quiza.preview = 0 AND
524                     qqr.regraded = 0";
525         return $DB->count_records_sql($sql, $params);
526     }
528     /**
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.
534      */
535     protected function has_regraded_questions($from, $where, $params) {
536         global $DB;
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());
541     }
543     /**
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.
548      */
549     protected function clear_regrade_table($quiz, $groupstudents) {
550         global $DB;
552         // Fetch all attempts that need regrading
553         $where = '';
554         $params = array();
555         if ($groupstudents) {
556             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
557             $where = "userid $usql AND ";
558         }
560         $params[] = $quiz->id;
561         $DB->delete_records_select('quiz_overview_regrades',
562                 "questionusageid IN (
563                     SELECT uniqueid
564                     FROM {quiz_attempts}
565                     WHERE $where quiz = ?
566                 )", $params);
567     }
569     /**
570      * Update the final grades for all attempts. This method is used following
571      * a regrade.
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.
575      */
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);
580     }