MDL-34211 Use the $table_name argument to check against $metatables key values
[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_overview
21  * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/mod/quiz/report/attemptsreport.php');
29 require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_options.php');
30 require_once($CFG->dirroot . '/mod/quiz/report/overview/overview_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_attempts_report {
42     public function display($quiz, $cm, $course) {
43         global $CFG, $DB, $OUTPUT;
45         list($currentgroup, $students, $groupstudents, $allowed) =
46                 $this->init('overview', 'quiz_overview_settings_form', $quiz, $cm, $course);
47         $options = new quiz_overview_options('overview', $quiz, $cm, $course);
49         if ($fromform = $this->form->get_data()) {
50             $options->process_settings_from_form($fromform);
52         } else {
53             $options->process_settings_from_params();
54         }
56         $this->form->set_data($options->get_initial_form_data());
58         if ($options->attempts == self::ALL_WITH) {
59             // This option is only available to users who can access all groups in
60             // groups mode, so setting allowed to empty (which means all quiz attempts
61             // are accessible, is not a security porblem.
62             $allowed = array();
63         }
65         // Load the required questions.
66         $questions = quiz_report_get_significant_questions($quiz);
68         // Prepare for downloading, if applicable.
69         $courseshortname = format_string($course->shortname, true,
70                 array('context' => context_course::instance($course->id)));
71         $table = new quiz_overview_table($quiz, $this->context, $this->qmsubselect,
72                 $options, $groupstudents, $students, $questions, $this->get_base_url());
73         $filename = quiz_report_download_filename(get_string('overviewfilename', 'quiz_overview'),
74                 $courseshortname, $quiz->name);
75         $table->is_downloading($options->download, $filename,
76                 $courseshortname . ' ' . format_string($quiz->name, true));
77         if ($table->is_downloading()) {
78             raise_memory_limit(MEMORY_EXTRA);
79         }
81         $this->process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $options->get_url());
83         // Start output.
84         if (!$table->is_downloading()) {
85             // Only print headers if not asked to download data.
86             $this->print_header_and_tabs($cm, $course, $quiz, $this->mode);
87         }
89         if ($groupmode = groups_get_activity_groupmode($cm)) {
90             // Groups are being used, so output the group selector if we are not downloading.
91             if (!$table->is_downloading()) {
92                 groups_print_activity_menu($cm, $options->get_url());
93             }
94         }
96         // Print information on the number of existing attempts.
97         if (!$table->is_downloading()) {
98             // Do not print notices when downloading.
99             if ($strattemptnum = quiz_num_attempt_summary($quiz, $cm, true, $currentgroup)) {
100                 echo '<div class="quizattemptcounts">' . $strattemptnum . '</div>';
101             }
102         }
104         $hasquestions = quiz_questions_in_quiz($quiz->questions);
105         if (!$table->is_downloading()) {
106             if (!$hasquestions) {
107                 echo quiz_no_questions_message($quiz, $cm, $this->context);
108             } else if (!$students) {
109                 echo $OUTPUT->notification(get_string('nostudentsyet'));
110             } else if ($currentgroup && !$groupstudents) {
111                 echo $OUTPUT->notification(get_string('nostudentsingroup'));
112             }
114             // Print the display options.
115             $this->form->display();
116         }
118         $hasstudents = $students && (!$currentgroup || $groupstudents);
119         if ($hasquestions && ($hasstudents || $options->attempts == self::ALL_WITH)) {
120             // Construct the SQL.
121             $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') .
122                     ' AS uniqueid, ';
123             if ($this->qmsubselect) {
124                 $fields .=
125                     "(CASE " .
126                     "   WHEN {$this->qmsubselect} THEN 1" .
127                     "   ELSE 0 " .
128                     "END) AS gradedattempt, ";
129             }
131             list($fields, $from, $where, $params) = $table->base_sql($allowed);
133             $table->set_count_sql("SELECT COUNT(1) FROM $from WHERE $where", $params);
135             // Test to see if there are any regraded attempts to be listed.
136             $fields .= ", COALESCE((
137                                 SELECT MAX(qqr.regraded)
138                                   FROM {quiz_overview_regrades} qqr
139                                  WHERE qqr.questionusageid = quiza.uniqueid
140                           ), -1) AS regraded";
141             if ($options->onlyregraded) {
142                 $where .= " AND COALESCE((
143                                     SELECT MAX(qqr.regraded)
144                                       FROM {quiz_overview_regrades} qqr
145                                      WHERE qqr.questionusageid = quiza.uniqueid
146                                 ), -1) <> -1";
147             }
148             $table->set_sql($fields, $from, $where, $params);
150             if (!$table->is_downloading()) {
151                 // Output the regrade buttons.
152                 if (has_capability('mod/quiz:regrade', $this->context)) {
153                     $regradesneeded = $this->count_question_attempts_needing_regrade(
154                             $quiz, $groupstudents);
155                     if ($currentgroup) {
156                         $a= new stdClass();
157                         $a->groupname = groups_get_group_name($currentgroup);
158                         $a->coursestudents = get_string('participants');
159                         $a->countregradeneeded = $regradesneeded;
160                         $regradealldrydolabel =
161                                 get_string('regradealldrydogroup', 'quiz_overview', $a);
162                         $regradealldrylabel =
163                                 get_string('regradealldrygroup', 'quiz_overview', $a);
164                         $regradealllabel =
165                                 get_string('regradeallgroup', 'quiz_overview', $a);
166                     } else {
167                         $regradealldrydolabel =
168                                 get_string('regradealldrydo', 'quiz_overview', $regradesneeded);
169                         $regradealldrylabel =
170                                 get_string('regradealldry', 'quiz_overview');
171                         $regradealllabel =
172                                 get_string('regradeall', 'quiz_overview');
173                     }
174                     $displayurl = new moodle_url($options->get_url(), array('sesskey' => sesskey()));
175                     echo '<div class="mdl-align">';
176                     echo '<form action="'.$displayurl->out_omit_querystring().'">';
177                     echo '<div>';
178                     echo html_writer::input_hidden_params($displayurl);
179                     echo '<input type="submit" name="regradeall" value="'.$regradealllabel.'"/>';
180                     echo '<input type="submit" name="regradealldry" value="' .
181                             $regradealldrylabel . '"/>';
182                     if ($regradesneeded) {
183                         echo '<input type="submit" name="regradealldrydo" value="' .
184                                 $regradealldrydolabel . '"/>';
185                     }
186                     echo '</div>';
187                     echo '</form>';
188                     echo '</div>';
189                 }
190                 // Print information on the grading method.
191                 if ($strattempthighlight = quiz_report_highlighting_grading_method(
192                         $quiz, $this->qmsubselect, $options->onlygraded)) {
193                     echo '<div class="quizattemptcounts">' . $strattempthighlight . '</div>';
194                 }
195             }
197             // Define table columns.
198             $columns = array();
199             $headers = array();
201             if (!$table->is_downloading() && $options->checkboxcolumn) {
202                 $columns[] = 'checkbox';
203                 $headers[] = null;
204             }
206             $this->add_user_columns($table, $columns, $headers);
207             $this->add_state_column($columns, $headers);
208             $this->add_time_columns($columns, $headers);
210             $this->add_grade_columns($quiz, $options->usercanseegrades, $columns, $headers, false);
212             if (!$table->is_downloading() && has_capability('mod/quiz:regrade', $this->context) &&
213                     $this->has_regraded_questions($from, $where, $params)) {
214                 $columns[] = 'regraded';
215                 $headers[] = get_string('regrade', 'quiz_overview');
216             }
218             if ($options->slotmarks) {
219                 foreach ($questions as $slot => $question) {
220                     // Ignore questions of zero length.
221                     $columns[] = 'qsgrade' . $slot;
222                     $header = get_string('qbrief', 'quiz', $question->number);
223                     if (!$table->is_downloading()) {
224                         $header .= '<br />';
225                     } else {
226                         $header .= ' ';
227                     }
228                     $header .= '/' . quiz_rescale_grade($question->maxmark, $quiz, 'question');
229                     $headers[] = $header;
230                 }
231             }
233             $this->set_up_table_columns($table, $columns, $headers, $this->get_base_url(), $options, false);
234             $table->set_attribute('class', 'generaltable generalbox grades');
236             $table->out($options->pagesize, true);
237         }
239         if (!$table->is_downloading() && $options->usercanseegrades) {
240             if ($currentgroup && $groupstudents) {
241                 list($usql, $params) = $DB->get_in_or_equal($groupstudents);
242                 $params[] = $quiz->id;
243                 if ($DB->record_exists_select('quiz_grades', "userid $usql AND quiz = ?",
244                         $params)) {
245                      $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
246                             array('id' => $quiz->id, 'groupid' => $currentgroup));
247                      $graphname = get_string('overviewreportgraphgroup', 'quiz_overview',
248                             groups_get_group_name($currentgroup));
249                      echo $OUTPUT->heading($graphname);
250                      echo html_writer::tag('div', html_writer::empty_tag('img',
251                             array('src' => $imageurl, 'alt' => $graphname)),
252                             array('class' => 'graph'));
253                 }
254             }
256             if ($DB->record_exists('quiz_grades', array('quiz'=> $quiz->id))) {
257                  $graphname = get_string('overviewreportgraph', 'quiz_overview');
258                  $imageurl = new moodle_url('/mod/quiz/report/overview/overviewgraph.php',
259                         array('id' => $quiz->id));
260                  echo $OUTPUT->heading($graphname);
261                  echo html_writer::tag('div', html_writer::empty_tag('img',
262                         array('src' => $imageurl, 'alt' => $graphname)),
263                         array('class' => 'graph'));
264             }
265         }
266         return true;
267     }
269     protected function process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl) {
270         parent::process_actions($quiz, $cm, $currentgroup, $groupstudents, $allowed, $redirecturl);
272         if (empty($currentgroup) || $groupstudents) {
273             if (optional_param('regrade', 0, PARAM_BOOL) && confirm_sesskey()) {
274                 if ($attemptids = optional_param_array('attemptid', array(), PARAM_INT)) {
275                     require_capability('mod/quiz:regrade', $this->context);
276                     $this->regrade_attempts($quiz, false, $groupstudents, $attemptids);
277                     redirect($redirecturl, '', 5);
278                 }
279             }
280         }
282         if (optional_param('regradeall', 0, PARAM_BOOL) && confirm_sesskey()) {
283             require_capability('mod/quiz:regrade', $this->context);
284             $this->regrade_attempts($quiz, false, $groupstudents);
285             redirect($redirecturl, '', 5);
287         } else if (optional_param('regradealldry', 0, PARAM_BOOL) && confirm_sesskey()) {
288             require_capability('mod/quiz:regrade', $this->context);
289             $this->regrade_attempts($quiz, true, $groupstudents);
290             redirect($redirecturl, '', 5);
292         } else if (optional_param('regradealldrydo', 0, PARAM_BOOL) && confirm_sesskey()) {
293             require_capability('mod/quiz:regrade', $this->context);
294             $this->regrade_attempts_needing_it($quiz, $groupstudents);
295             redirect($redirecturl, '', 5);
296         }
297     }
299     /**
300      * Regrade a particular quiz attempt. Either for real ($dryrun = false), or
301      * as a pretend regrade to see which fractions would change. The outcome is
302      * stored in the quiz_overview_regrades table.
303      *
304      * Note, $attempt is not upgraded in the database. The caller needs to do that.
305      * However, $attempt->sumgrades is updated, if this is not a dry run.
306      *
307      * @param object $attempt the quiz attempt to regrade.
308      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
309      * @param array $slots if null, regrade all questions, otherwise, just regrade
310      *      the quetsions with those slots.
311      */
312     protected function regrade_attempt($attempt, $dryrun = false, $slots = null) {
313         global $DB;
314         set_time_limit(30);
316         $transaction = $DB->start_delegated_transaction();
318         $quba = question_engine::load_questions_usage_by_activity($attempt->uniqueid);
320         if (is_null($slots)) {
321             $slots = $quba->get_slots();
322         }
324         $finished = $attempt->state == quiz_attempt::FINISHED;
325         foreach ($slots as $slot) {
326             $qqr = new stdClass();
327             $qqr->oldfraction = $quba->get_question_fraction($slot);
329             $quba->regrade_question($slot, $finished);
331             $qqr->newfraction = $quba->get_question_fraction($slot);
333             if (abs($qqr->oldfraction - $qqr->newfraction) > 1e-7) {
334                 $qqr->questionusageid = $quba->get_id();
335                 $qqr->slot = $slot;
336                 $qqr->regraded = empty($dryrun);
337                 $qqr->timemodified = time();
338                 $DB->insert_record('quiz_overview_regrades', $qqr, false);
339             }
340         }
342         if (!$dryrun) {
343             question_engine::save_questions_usage_by_activity($quba);
344         }
346         $transaction->allow_commit();
348         // Really, PHP should not need this hint, but without this, we just run out of memory.
349         $quba = null;
350         $transaction = null;
351         gc_collect_cycles();
352     }
354     /**
355      * Regrade attempts for this quiz, exactly which attempts are regraded is
356      * controlled by the parameters.
357      * @param object $quiz the quiz settings.
358      * @param bool $dryrun if true, do a pretend regrade, otherwise do it for real.
359      * @param array $groupstudents blank for all attempts, otherwise regrade attempts
360      * for these users.
361      * @param array $attemptids blank for all attempts, otherwise only regrade
362      * attempts whose id is in this list.
363      */
364     protected function regrade_attempts($quiz, $dryrun = false,
365             $groupstudents = array(), $attemptids = array()) {
366         global $DB;
368         $where = "quiz = ? AND preview = 0";
369         $params = array($quiz->id);
371         if ($groupstudents) {
372             list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
373             $where .= " AND userid $usql";
374             $params = array_merge($params, $uparams);
375         }
377         if ($attemptids) {
378             list($asql, $aparams) = $DB->get_in_or_equal($attemptids);
379             $where .= " AND id $asql";
380             $params = array_merge($params, $aparams);
381         }
383         $attempts = $DB->get_records_select('quiz_attempts', $where, $params);
384         if (!$attempts) {
385             return;
386         }
388         $this->clear_regrade_table($quiz, $groupstudents);
390         foreach ($attempts as $attempt) {
391             $this->regrade_attempt($attempt, $dryrun);
392         }
394         if (!$dryrun) {
395             $this->update_overall_grades($quiz);
396         }
397     }
399     /**
400      * Regrade those questions in those attempts that are marked as needing regrading
401      * in the quiz_overview_regrades table.
402      * @param object $quiz the quiz settings.
403      * @param array $groupstudents blank for all attempts, otherwise regrade attempts
404      * for these users.
405      */
406     protected function regrade_attempts_needing_it($quiz, $groupstudents) {
407         global $DB;
409         $where = "quiza.quiz = ? AND quiza.preview = 0 AND qqr.regraded = 0";
410         $params = array($quiz->id);
412         // Fetch all attempts that need regrading.
413         if ($groupstudents) {
414             list($usql, $uparams) = $DB->get_in_or_equal($groupstudents);
415             $where .= " AND quiza.userid $usql";
416             $params += $uparams;
417         }
419         $toregrade = $DB->get_records_sql("
420                 SELECT quiza.uniqueid, qqr.slot
421                 FROM {quiz_attempts} quiza
422                 JOIN {quiz_overview_regrades} qqr ON qqr.questionusageid = quiza.uniqueid
423                 WHERE $where", $params);
425         if (!$toregrade) {
426             return;
427         }
429         $attemptquestions = array();
430         foreach ($toregrade as $row) {
431             $attemptquestions[$row->uniqueid][] = $row->slot;
432         }
433         $attempts = $DB->get_records_list('quiz_attempts', 'uniqueid',
434                 array_keys($attemptquestions));
436         $this->clear_regrade_table($quiz, $groupstudents);
438         foreach ($attempts as $attempt) {
439             $this->regrade_attempt($attempt, false, $attemptquestions[$attempt->uniqueid]);
440         }
442         $this->update_overall_grades($quiz);
443     }
445     /**
446      * Count the number of attempts in need of a regrade.
447      * @param object $quiz the quiz settings.
448      * @param array $groupstudents user ids. If this is given, only data relating
449      * to these users is cleared.
450      */
451     protected function count_question_attempts_needing_regrade($quiz, $groupstudents) {
452         global $DB;
454         $usertest = '';
455         $params = array();
456         if ($groupstudents) {
457             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
458             $usertest = "quiza.userid $usql AND ";
459         }
461         $params[] = $quiz->id;
462         $sql = "SELECT COUNT(DISTINCT quiza.id)
463                 FROM {quiz_attempts} quiza
464                 JOIN {quiz_overview_regrades} qqr ON quiza.uniqueid = qqr.questionusageid
465                 WHERE
466                     $usertest
467                     quiza.quiz = ? AND
468                     quiza.preview = 0 AND
469                     qqr.regraded = 0";
470         return $DB->count_records_sql($sql, $params);
471     }
473     /**
474      * Are there any pending regrades in the table we are going to show?
475      * @param string $from tables used by the main query.
476      * @param string $where where clause used by the main query.
477      * @param array $params required by the SQL.
478      * @return bool whether there are pending regrades.
479      */
480     protected function has_regraded_questions($from, $where, $params) {
481         global $DB;
482         $qubaids = new qubaid_join($from, 'uniqueid', $where, $params);
483         return $DB->record_exists_select('quiz_overview_regrades',
484                 'questionusageid ' . $qubaids->usage_id_in(),
485                 $qubaids->usage_id_in_params());
486     }
488     /**
489      * Remove all information about pending/complete regrades from the database.
490      * @param object $quiz the quiz settings.
491      * @param array $groupstudents user ids. If this is given, only data relating
492      * to these users is cleared.
493      */
494     protected function clear_regrade_table($quiz, $groupstudents) {
495         global $DB;
497         // Fetch all attempts that need regrading.
498         $where = '';
499         $params = array();
500         if ($groupstudents) {
501             list($usql, $params) = $DB->get_in_or_equal($groupstudents);
502             $where = "userid $usql AND ";
503         }
505         $params[] = $quiz->id;
506         $DB->delete_records_select('quiz_overview_regrades',
507                 "questionusageid IN (
508                     SELECT uniqueid
509                     FROM {quiz_attempts}
510                     WHERE $where quiz = ?
511                 )", $params);
512     }
514     /**
515      * Update the final grades for all attempts. This method is used following
516      * a regrade.
517      * @param object $quiz the quiz settings.
518      * @param array $userids only update scores for these userids.
519      * @param array $attemptids attemptids only update scores for these attempt ids.
520      */
521     protected function update_overall_grades($quiz) {
522         quiz_update_all_attempt_sumgrades($quiz);
523         quiz_update_all_final_grades($quiz);
524         quiz_update_grades($quiz);
525     }