aec78095ae238e26de0f95cd8c498d6a71c2964f
[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         $questions = quiz_report_get_significant_questions($quiz);
122         $table = new quiz_report_overview_table($quiz, $this->context, $qmsubselect,
123                 $groupstudents, $students, $detailedmarks, $questions, $candelete,
124                 $reporturl, $displayoptions);
125         $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
126                 $course->shortname, $quiz->name);
127         $table->is_downloading($download, $filename,
128                 $COURSE->shortname . ' ' . format_string($quiz->name, true));
129         if ($table->is_downloading()) {
130             raise_memory_limit(MEMORY_EXTRA);
131         }
133         // Process actions.
134         if (empty($currentgroup) || $groupstudents) {
135             if (optional_param('delete', 0, PARAM_BOOL) && confirm_sesskey()) {
136                 if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
137                     require_capability('mod/quiz:deleteattempts', $this->context);
138                     $this->delete_selected_attempts($quiz, $cm, $attemptids, $allowed);
139                     redirect($reporturl->out(false, $displayoptions));
140                 }
142             } else if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
143                 if ($attemptids = optional_param('attemptid', array(), PARAM_INT)) {
144                     require_capability('mod/quiz:regrade', $this->context);
145                     $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
146                     redirect($reporturl->out(false, $displayoptions));
147                 }
148             }
149         }
151         if ($regradeall && confirm_sesskey()) {
152             require_capability('mod/quiz:regrade', $this->context);
153             $this->regrade_attempts($quiz, false, $groupstudents);
154             redirect($reporturl->out(false, $displayoptions), '', 5);
156         } else if ($regradealldry && confirm_sesskey()) {
157             require_capability('mod/quiz:regrade', $this->context);
158             $this->regrade_attempts($quiz, true, $groupstudents);
159             redirect($reporturl->out(false, $displayoptions), '', 5);
161         } else if ($regradealldrydo && confirm_sesskey()) {
162             require_capability('mod/quiz:regrade', $this->context);
163             $this->regrade_attempts_needing_it($quiz, $groupstudents);
164             redirect($reporturl->out(false, $displayoptions), '', 5);
165         }
167         // Start output.
168         if (!$table->is_downloading()) {
169             // Only print headers if not asked to download data
170             $this->print_header_and_tabs($cm, $course, $quiz, 'overview');
171         }
173         if ($groupmode = groups_get_activity_groupmode($cm)) {   // Groups are being used
174             if (!$table->is_downloading()) {
175                 groups_print_activity_menu($cm, $reporturl->out(true, $displayoptions));
176             }
177         }
179         // Print information on the number of existing attempts
180         if (!$table->is_downloading()) { //do not print notices when downloading
181             if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
182                 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
183             }
184         }
186         $hasquestions = quiz_questions_in_quiz($quiz->questions);
187         if (!$table->is_downloading()) {
188             if (!$hasquestions) {
189                 echo quiz_no_questions_message($quiz, $cm, $this->context);
190             } else if (!$students) {
191                 echo $OUTPUT->notification(get_string('nostudentsyet'));
192             } else if ($currentgroup && !$groupstudents) {
193                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
194             }
196             // Print display options
197             $mform->display();
198         }
200         $hasstudents = $students && (!$currentgroup || $groupstudents);
201         if ($hasquestions && ($hasstudents || ($attemptsmode == QUIZ_REPORT_ATTEMPTS_ALL))) {
202             // Construct the SQL
203             $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
204                     ' AS uniqueid, ';
205             if ($qmsubselect) {
206                 $fields .=
207                     "(CASE " .
208                     "   WHEN $qmsubselect THEN 1" .
209                     "   ELSE 0 " .
210                     "END) AS gradedattempt, ";
211             }
213             list($fields, $from, $where, $params) =
214                     $this->base_sql($quiz, $qmsubselect, $qmfilter, $attemptsmode, $allowed);
216             $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
218             // Test to see if there are any regraded attempts to be listed.
219             $fields .= ", COALESCE((
220                                 SELECT MAX(qqr.regraded)
221                                   FROM {quiz_overview_regrades} qqr
222                                  WHERE qqr.questionusageid = quiza.uniqueid
223                           ), -1) AS regraded";
224             if ($regradefilter) {
225                 $where .= " AND COALESCE((
226                                     SELECT MAX(qqr.regraded)
227                                       FROM {quiz_overview_regrades} qqr
228                                      WHERE qqr.questionusageid = quiza.uniqueid
229                                 ), -1) <> -1";
230             }
231             $table->set_sql($fields, $from, $where, $params);
233             if (!$table->is_downloading()) {
234                 // Regrade buttons
235                 if (has_capability('mod/quiz:regrade', $this->context)) {
236                     $regradesneeded = $this->count_question_attempts_needing_regrade(
237                             $quiz, $groupstudents);
238                     if ($currentgroup) {
239                         $a= new stdClass();
240                         $a->groupname = groups_get_group_name($currentgroup);
241                         $a->coursestudents = get_string('participants');
242                         $a->countregradeneeded = $regradesneeded;
243                         $regradealldrydolabel =
244                                 get_string('regradealldrydogroup', 'quiz_overview', $a);
245                         $regradealldrylabel =
246                                 get_string('regradealldrygroup', 'quiz_overview', $a);
247                         $regradealllabel =
248                                 get_string('regradeallgroup', 'quiz_overview', $a);
249                     } else {
250                         $regradealldrydolabel =
251                                 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
252                         $regradealldrylabel =
253                                 get_string('regradealldry', 'quiz_overview');
254                         $regradealllabel =
255                                 get_string('regradeall', 'quiz_overview');
256                     }
257                     $displayurl = new moodle_url($reporturl,
258                             $displayoptions + array('sesskey' => sesskey()));
259                     echo '<div class="mdl-align">';
260                     echo '<form action="'.$displayurl->out_omit_querystring().'">';
261                     echo '<div>';
262                     echo html_writer::input_hidden_params($displayurl);
263                     echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
264                     echo '<input type="submit" name="regradealldry" value="' .
265                             $regradealldrylabel . '"/>';
266                     if ($regradesneeded) {
267                         echo '<input type="submit" name="regradealldrydo" value="' .
268                                 $regradealldrydolabel . '"/>';
269                     }
270                     echo '</div>';
271                     echo '</form>';
272                     echo '</div>';
273                 }
274                 // Print information on the grading method
275                 if ($strattempthighlight = quiz_report_highlighting_grading_method(
276                         $quiz, $qmsubselect, $qmfilter)) {
277                     echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
278                 }
279             }
281             // Define table columns
282             $columns = array();
283             $headers = array();
285             if (!$table->is_downloading() && $candelete) {
286                 $columns[] = 'checkbox';
287                 $headers[] = null;
288             }
290             $this->add_user_columns($table, $columns, $headers);
292             $this->add_time_columns($columns, $headers);
294             if ($detailedmarks) {
295                 foreach ($questions as $slot => $question) {
296                     // Ignore questions of zero length
297                     $columns[] = 'qsgrade' . $slot;
298                     $header = get_string('qbrief', 'quiz', $question->number);
299                     if (!$table->is_downloading()) {
300                         $header .= '<br />';
301                     } else {
302                         $header .= ' ';
303                     }
304                     $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
305                     $headers[] = $header;
306                 }
307             }
309             if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
310                     $this->has_regraded_questions($from, $where, $params)) {
311                 $columns[] = 'regraded';
312                 $headers[] = get_string('regrade', 'quiz_overview');
313             }
315             $this->add_grade_columns($quiz, $columns, $headers);
317             $this->set_up_table_columns(
318                     $table, $columns, $headers, $reporturl, $displayoptions, false);
319             $table->set_attribute('class', 'generaltable generalbox grades');
321             $table->out($pagesize, true);
322         }
324         if (!$table->is_downloading() && $this->should_show_grades($quiz)) {
325             if ($currentgroup && $groupstudents) {
326                 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
327                 $params[] = $quiz->id;
328                 if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
329                         $params)) {
330                      $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
331                             array('id' => $quiz->id, 'groupid' => $currentgroup));
332                      $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
333                             groups_get_group_name($currentgroup));
334                      echo $OUTPUT->heading($graphname);
335                      echo html_writer::tag('div', html_writer::empty_tag('img',
336                             array('src' => $imageurl, 'alt' => $graphname)),
337                             array('class' => 'graph'));
338                 }
339             }
341             if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
342                  $graphname = get_string('overviewreportgraph', 'quiz_overview');
343                  $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
344                         array('id' => $quiz->id));
345                  echo $OUTPUT->heading($graphname);
346                  echo html_writer::tag('div', html_writer::empty_tag('img',
347                         array('src' => $imageurl, 'alt' => $graphname)),
348                         array('class' => 'graph'));
349             }
350         }
351         return true;
352     }
354     /**
355      * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
356      * as a pretend regrade to see which fractions would change. The outcome is
357      * stored in the quiz_overview_regrades table.
358      *
359      * Note, $attempt is not upgraded in the database. The caller needs to do that.
360      * However, $attempt->sumgrades is updated, if this is not a dry run.
361      *
362      * @param object $attempt the quiz attempt to regrade.
363      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
364      * @param array $slots if null, regrade all questions, otherwise, just regrade
365      *      the quetsions with those slots.
366      */
367     protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
368         global $DB;
370         $transaction = $DB->start_delegated_transaction();
372         $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
374         if (is_null($slots)) {
375             $slots = $quba->get_slots();
376         }
378         $finished = $attempt->timefinish > 0;
379         foreach ($slots as $slot) {
380             $qqr = new stdClass();
381             $qqr->oldfraction = $quba->get_question_fraction($slot);
383             $quba->regrade_question($slot, $finished);
385             $qqr->newfraction = $quba->get_question_fraction($slot);
387             if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
388                 $qqr->questionusageid = $quba->get_id();
389                 $qqr->slot = $slot;
390                 $qqr->regraded = empty($dryrun);
391                 $qqr->timemodified = time();
392                 $DB->insert_record('quiz_overview_regrades', $qqr, false);
393             }
394         }
396         if (!$dryrun) {
397             question_engine::save_questions_usage_by_activity($quba);
398         }
400         $transaction->allow_commit();
401     }
403     /**
404      * Regrade attempts for this quiz, exactly which attempts are regraded is
405      * controlled by the parameters.
406      * @param object $quiz the quiz settings.
407      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
408      * @param array $groupstudents blank for all attempts, otherwise regrade attempts
409      * for these users.
410      * @param array $attemptids blank for all attempts, otherwise only regrade
411      * attempts whose id is in this list.
412      */
413     protected function regrade_attempts($quiz, $dryrun = false,
414             $groupstudents = array(), $attemptids = array()) {
415         global $DB;
417         $where = "quiz = ? AND preview = 0";
418         $params = array($quiz->id);
420         if ($groupstudents) {
421             list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
422             $where .= " AND userid $usql";
423             $params = array_merge($params, $uparams);
424         }
426         if ($attemptids) {
427             list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
428             $where .= " AND id $asql";
429             $params = array_merge($params, $aparams);
430         }
432         $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
433         if (!$attempts) {
434             return;
435         }
437         $this->clear_regrade_table($quiz, $groupstudents);
439         foreach ($attempts as $attempt) {
440             set_time_limit(30);
441             $this->regrade_attempt($attempt, $dryrun);
442         }
444         if (!$dryrun) {
445             $this->update_overall_grades($quiz);
446         }
447     }
449     /**
450      * Regrade those questions in those attempts that are marked as needing regrading
451      * in the quiz_overview_regrades table.
452      * @param object $quiz the quiz settings.
453      * @param array $groupstudents blank for all attempts, otherwise regrade attempts
454      * for these users.
455      */
456     protected function regrade_attempts_needing_it($quiz, $groupstudents) {
457         global $DB;
459         $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
460         $params = array($quiz->id);
462         // Fetch all attempts that need regrading
463         if ($groupstudents) {
464             list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
465             $where .= " AND quiza.userid $usql";
466             $params += $uparams;
467         }
469         $toregrade = $DB->get_records_sql("
470                 SELECT quiza.uniqueid, qqr.slot
471                 FROM {quiz_attempts} quiza
472                 JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
473                 WHERE $where", $params);
475         if (!$toregrade) {
476             return;
477         }
479         $attemptquestions = array();
480         foreach ($toregrade as $row) {
481             $attemptquestions[$row->uniqueid][] = $row->slot;
482         }
483         $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
484                 array_keys($attemptquestions));
486         $this->clear_regrade_table($quiz, $groupstudents);
488         foreach ($attempts as $attempt) {
489             set_time_limit(30);
490             $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
491         }
493         $this->update_overall_grades($quiz);
494     }
496     /**
497      * Count the number of attempts in need of a regrade.
498      * @param object $quiz the quiz settings.
499      * @param array $groupstudents user ids. If this is given, only data relating
500      * to these users is cleared.
501      */
502     protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
503         global $DB;
505         $usertest = '';
506         $params = array();
507         if ($groupstudents) {
508             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
509             $usertest = "quiza.userid $usql AND ";
510         }
512         $params[] = $quiz->id;
513         $sql = "SELECT COUNT(DISTINCT quiza.id)
514                 FROM {quiz_attempts} quiza
515                 JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
516                 WHERE
517                     $usertest
518                     quiza.quiz = ? AND
519                     quiza.preview = 0 AND
520                     qqr.regraded = 0";
521         return $DB->count_records_sql($sql, $params);
522     }
524     /**
525      * Are there any pending regrades in the table we are going to show?
526      * @param string $from tables used by the main query.
527      * @param string $where where clause used by the main query.
528      * @param array $params required by the SQL.
529      * @return bool whether there are pending regrades.
530      */
531     protected function has_regraded_questions($from, $where, $params) {
532         global $DB;
533         $qubaids = new qubaid_join($from, 'uniqueid', $where, $params);
534         return $DB->record_exists_select('quiz_overview_regrades',
535                 'questionusageid ' . $qubaids->usage_id_in(),
536                 $qubaids->usage_id_in_params());
537     }
539     /**
540      * Remove all information about pending/complete regrades from the database.
541      * @param object $quiz the quiz settings.
542      * @param array $groupstudents user ids. If this is given, only data relating
543      * to these users is cleared.
544      */
545     protected function clear_regrade_table($quiz, $groupstudents) {
546         global $DB;
548         // Fetch all attempts that need regrading
549         $where = '';
550         $params = array();
551         if ($groupstudents) {
552             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
553             $where = "userid $usql AND ";
554         }
556         $params[] = $quiz->id;
557         $DB->delete_records_select('quiz_overview_regrades',
558                 "questionusageid IN (
559                     SELECT uniqueid
560                     FROM {quiz_attempts}
561                     WHERE $where quiz = ?
562                 )", $params);
563     }
565     /**
566      * Update the final grades for all attempts. This method is used following
567      * a regrade.
568      * @param object $quiz the quiz settings.
569      * @param array $userids only update scores for these userids.
570      * @param array $attemptids attemptids only update scores for these attempt ids.
571      */
572     protected function update_overall_grades($quiz) {
573         quiz_update_all_attempt_sumgrades($quiz);
574         quiz_update_all_final_grades($quiz);
575         quiz_update_grades($quiz);
576     }