MDL-61520 quiz: fix references for xhtml on stats report
[moodle.git] / mod / quiz / report / statistics / report.php
CommitLineData
0c1c764e 1<?php
04853f27
TH
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/>.
16
0c1c764e 17/**
04853f27 18 * Quiz statistics report class.
0c1c764e 19 *
8d76124c 20 * @package quiz_statistics
038014c4
JP
21 * @copyright 2014 Open University
22 * @author James Pratt <me@jamiep.org>
8d76124c 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
24 */
25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
04853f27
TH
28require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
29require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
30require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
e68e4ccf 31require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
04853f27
TH
32/**
33 * The quiz statistics report provides summary information about each question in
34 * a quiz, compared to the whole quiz. It also provides a drill-down to more
35 * detailed information about each question.
36 *
8d76124c
TH
37 * @copyright 2008 Jamie Pratt
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 39 */
c386eaa3 40class quiz_statistics_report extends quiz_default_report {
7de1e35b 41
038014c4 42 /** @var context_module context of this quiz.*/
7de1e35b 43 protected $context;
3b1d5cc4 44
515b3ae6 45 /** @var quiz_statistics_table instance of table class used for main questions stats table. */
04853f27 46 protected $table;
0c1c764e 47
8da6fc9d
JP
48 /** @var \core\progress\base|null $progress Handles progress reporting or not. */
49 protected $progress = null;
50
0c1c764e 51 /**
52 * Display the report.
53 */
04853f27 54 public function display($quiz, $cm, $course) {
10c4fce5 55 global $OUTPUT, $DB;
8da6fc9d
JP
56
57 raise_memory_limit(MEMORY_HUGE);
0c1c764e 58
26aded55 59 $this->context = context_module::instance($cm->id);
0c1c764e 60
ccba5b88 61 if (!quiz_has_questions($quiz->id)) {
f16ed06c
JP
62 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
63 echo quiz_no_questions_message($quiz, $cm, $this->context);
64 return true;
65 }
66
04853f27 67 // Work out the display options.
0c1c764e 68 $download = optional_param('download', '', PARAM_ALPHA);
869309b8 69 $everything = optional_param('everything', 0, PARAM_BOOL);
d1789d5d 70 $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
d50b05e6 71 // A qid paramter indicates we should display the detailed analysis of a sub question.
43ec99aa 72 $qid = optional_param('qid', 0, PARAM_INT);
04853f27 73 $slot = optional_param('slot', 0, PARAM_INT);
4922e79f 74 $variantno = optional_param('variant', null, PARAM_INT);
6dd9362e 75 $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
038014c4 76 $whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);
04853f27 77
0c1c764e 78 $pageoptions = array();
79 $pageoptions['id'] = $cm->id;
0c1c764e 80 $pageoptions['mode'] = 'statistics';
81
a6855934 82 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
0c1c764e 83
038014c4 84 $mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));
6dd9362e 85
038014c4 86 $mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries));
04853f27 87
6dd9362e
JP
88 if ($whichattempts != $quiz->grademethod) {
89 $reporturl->param('whichattempts', $whichattempts);
0c1c764e 90 }
91
038014c4
JP
92 if ($whichtries != question_attempt::LAST_TRY) {
93 $reporturl->param('whichtries', $whichtries);
94 }
95
768a7588 96 // Find out current groups mode.
490668bb 97 $currentgroup = $this->get_current_group($cm, $course, $this->context);
04853f27
TH
98 $nostudentsingroup = false; // True if a group is selected and there is no one in it.
99 if (empty($currentgroup)) {
100 $currentgroup = 0;
10c4fce5 101 $groupstudentsjoins = new \core\dml\sql_join();
3b1d5cc4 102
e4977ba5 103 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
10c4fce5 104 $groupstudentsjoins = new \core\dml\sql_join();
e4977ba5
TH
105 $nostudentsingroup = true;
106
04853f27 107 } else {
768a7588 108 // All users who can attempt quizzes and who are in the currently selected group.
10c4fce5
JB
109 $groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',
110 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'), $currentgroup);
111 if (!empty($groupstudentsjoins->joins)) {
112 $sql = "SELECT DISTINCT u.id
113 FROM {user} u
114 {$groupstudentsjoins->joins}
115 WHERE {$groupstudentsjoins->wheres}";
116 if (!$DB->record_exists_sql($sql, $groupstudentsjoins->params)) {
117 $nostudentsingroup = true;
118 }
0c1c764e 119 }
0c1c764e 120 }
3b1d5cc4 121
10c4fce5 122 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
e68e4ccf 123
04853f27 124 // If recalculate was requested, handle that.
db77f410 125 if ($recalculate && confirm_sesskey()) {
e68e4ccf 126 $this->clear_cached_data($qubaids);
e42f153c 127 redirect($reporturl);
43ec99aa 128 }
3b1d5cc4 129
04853f27 130 // Set up the main table.
59ea8176 131 $this->table = new quiz_statistics_table();
04853f27
TH
132 if ($everything) {
133 $report = get_string('completestatsfilename', 'quiz_statistics');
134 } else {
135 $report = get_string('questionstatsfilename', 'quiz_statistics');
136 }
0eafc988 137 $courseshortname = format_string($course->shortname, true,
26aded55 138 array('context' => context_course::instance($course->id)));
8ebbb06a 139 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
25a03faa
TH
140 $this->table->is_downloading($download, $filename,
141 get_string('quizstructureanalysis', 'quiz_statistics'));
3652dddd 142 $questions = $this->load_and_initialise_questions_for_calculations($quiz);
04853f27 143
8da6fc9d
JP
144 // Print the page header stuff (if not downloading.
145 if (!$this->table->is_downloading()) {
146 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
147 }
148
7de1e35b
JP
149 if (!$nostudentsingroup) {
150 // Get the data to be displayed.
8da6fc9d 151 $progress = $this->get_progress_trace_instance();
c3e2e754 152 list($quizstats, $questionstats) =
10c4fce5 153 $this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudentsjoins, $questions, $progress);
7de1e35b
JP
154 } else {
155 // Or create empty stats containers.
8da6fc9d 156 $quizstats = new \quiz_statistics\calculated($whichattempts);
c3e2e754 157 $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
7de1e35b 158 }
04853f27 159
10c4fce5
JB
160 // Set up the table.
161 $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
3b1d5cc4 162
8da6fc9d 163 // Print the rest of the page header stuff (if not downloading.
04853f27 164 if (!$this->table->is_downloading()) {
04853f27 165
e4977ba5 166 if (groups_get_activity_groupmode($cm)) {
04853f27 167 groups_print_activity_menu($cm, $reporturl->out());
10c4fce5 168 if ($currentgroup && $nostudentsingroup) {
04853f27 169 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
b0e4fa41
TH
170 }
171 }
172
f16ed06c 173 if (!$this->table->is_downloading() && $quizstats->s() == 0) {
9e5e0d8d 174 echo $OUTPUT->notification(get_string('nogradedattempts', 'quiz_statistics'));
3c6185e9
TH
175 }
176
4922e79f 177 foreach ($questionstats->any_error_messages() as $errormessage) {
c3e2e754
JP
178 echo $OUTPUT->notification($errormessage);
179 }
180
04853f27 181 // Print display options form.
04853f27
TH
182 $mform->display();
183 }
184
185 if ($everything) { // Implies is downloading.
186 // Overall report, then the analysis of each question.
d50b05e6 187 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27
TH
188 $this->download_quiz_info_table($quizinfo);
189
7de1e35b 190 if ($quizstats->s()) {
c3e2e754 191 $this->output_quiz_structure_analysis_table($questionstats);
04853f27 192
de81dc87 193 if ($this->table->is_downloading() == 'html' && $quizstats->s() != 0) {
10c4fce5 194 $this->output_statistics_graph($quiz->id, $qubaids);
869309b8 195 }
04853f27 196
038014c4 197 $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
869309b8 198 }
04853f27
TH
199
200 $this->table->export_class_instance()->finish_document();
201
4922e79f
JP
202 } else if ($qid) {
203 // Report on an individual sub-question indexed questionid.
bccad386 204 if (!$questionstats->has_subq($qid, $variantno)) {
4922e79f
JP
205 print_error('questiondoesnotexist', 'question');
206 }
207
208 $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));
209 $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,
210 $variantno,
211 $questionstats->for_subq($qid, $variantno)->s,
212 $reporturl,
038014c4
JP
213 $qubaids,
214 $whichtries);
4922e79f
JP
215 // Back to overview link.
216 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
217 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
218 'boxaligncenter generalbox boxwidthnormal mdl-align');
04853f27
TH
219 } else if ($slot) {
220 // Report on an individual question indexed by position.
221 if (!isset($questions[$slot])) {
222 print_error('questiondoesnotexist', 'question');
869309b8 223 }
04853f27 224
4922e79f
JP
225 if ($variantno === null &&
226 ($questionstats->for_slot($slot)->get_sub_question_ids()
227 || $questionstats->for_slot($slot)->get_variants())) {
51e3ded8
JP
228 if (!$this->table->is_downloading()) {
229 $number = $questionstats->for_slot($slot)->question->number;
230 echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
231 }
232 $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot)));
233 $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
234 } else {
4922e79f 235 $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
51e3ded8 236 $this->output_individual_question_response_analysis($questions[$slot],
4922e79f
JP
237 $variantno,
238 $questionstats->for_slot($slot, $variantno)->s,
51e3ded8 239 $reporturl,
038014c4
JP
240 $qubaids,
241 $whichtries);
51e3ded8
JP
242 }
243 if (!$this->table->is_downloading()) {
244 // Back to overview link.
245 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
246 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
247 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
248 } else {
249 $this->table->finish_output();
250 }
04853f27 251
04853f27
TH
252 } else if ($this->table->is_downloading()) {
253 // Downloading overview report.
d50b05e6 254 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27 255 $this->download_quiz_info_table($quizinfo);
c3e2e754
JP
256 if ($quizstats->s()) {
257 $this->output_quiz_structure_analysis_table($questionstats);
258 }
1370d793 259 $this->table->export_class_instance()->finish_document();
04853f27
TH
260
261 } else {
262 // On-screen display of overview report.
c544ee92 263 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
10c4fce5 264 echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudentsjoins, $whichattempts, $reporturl);
6ae04c26 265 echo $this->everything_download_options($reporturl);
d50b05e6 266 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27 267 echo $this->output_quiz_info_table($quizinfo);
7de1e35b 268 if ($quizstats->s()) {
c544ee92 269 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
c3e2e754 270 $this->output_quiz_structure_analysis_table($questionstats);
10c4fce5 271 $this->output_statistics_graph($quiz, $qubaids);
b0e4fa41 272 }
43ec99aa 273 }
04853f27 274
43ec99aa 275 return true;
276 }
3b1d5cc4 277
04853f27
TH
278 /**
279 * Display the statistical and introductory information about a question.
280 * Only called when not downloading.
038014c4 281 *
d50b05e6 282 * @param object $quiz the quiz settings.
515b3ae6 283 * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
04853f27 284 */
515b3ae6 285 protected function output_individual_question_data($quiz, $questionstat) {
04853f27
TH
286 global $OUTPUT;
287
288 // On-screen display. Show a summary of the question's place in the quiz,
289 // and the question statistics.
515b3ae6 290 $datumfromtable = $this->table->format_row($questionstat);
04853f27
TH
291
292 // Set up the question info table.
293 $questioninfotable = new html_table();
294 $questioninfotable->align = array('center', 'center');
295 $questioninfotable->width = '60%';
296 $questioninfotable->attributes['class'] = 'generaltable titlesleft';
297
298 $questioninfotable->data = array();
299 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
300 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
515b3ae6 301 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
4922e79f
JP
302
303 if ($questionstat->variant !== null) {
304 $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
305
306 }
04853f27
TH
307 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
308 $datumfromtable['icon'] . '&nbsp;' .
515b3ae6 309 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
04853f27
TH
310 $datumfromtable['icon']);
311 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
515b3ae6 312 $questionstat->positions);
04853f27
TH
313
314 // Set up the question statistics table.
315 $questionstatstable = new html_table();
316 $questionstatstable->align = array('center', 'center');
317 $questionstatstable->width = '60%';
318 $questionstatstable->attributes['class'] = 'generaltable titlesleft';
319
320 unset($datumfromtable['number']);
321 unset($datumfromtable['icon']);
322 $actions = $datumfromtable['actions'];
323 unset($datumfromtable['actions']);
324 unset($datumfromtable['name']);
25a03faa
TH
325 $labels = array(
326 's' => get_string('attempts', 'quiz_statistics'),
327 'facility' => get_string('facility', 'quiz_statistics'),
328 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
329 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
330 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
331 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
332 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
333 'discriminative_efficiency' =>
334 get_string('discriminative_efficiency', 'quiz_statistics')
335 );
04853f27
TH
336 foreach ($datumfromtable as $item => $value) {
337 $questionstatstable->data[] = array($labels[$item], $value);
869309b8 338 }
04853f27
TH
339
340 // Display the various bits.
c544ee92 341 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
04853f27 342 echo html_writer::table($questioninfotable);
515b3ae6 343 echo $this->render_question_text($questionstat->question);
c544ee92 344 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
04853f27 345 echo html_writer::table($questionstatstable);
869309b8 346 }
3b1d5cc4 347
edfa0d80 348 /**
038014c4
JP
349 * Output question text in a box with urls appropriate for a preview of the question.
350 *
edfa0d80
TH
351 * @param object $question question data.
352 * @return string HTML of question text, ready for display.
353 */
fdb5bc03 354 protected function render_question_text($question) {
edfa0d80 355 global $OUTPUT;
fdb5bc03 356
68d2f6a0
TH
357 $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
358 $question->contextid, 'question', 'questiontext', $question->id,
359 $this->context->id, 'quiz_statistics');
fdb5bc03
TH
360
361 return $OUTPUT->box(format_text($text, $question->questiontextformat,
362 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
edfa0d80
TH
363 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
364 }
365
04853f27
TH
366 /**
367 * Display the response analysis for a question.
4922e79f 368 *
d50b05e6 369 * @param object $question the question to report on.
4922e79f 370 * @param int|null $variantno the variant
d50b05e6
JP
371 * @param int $s
372 * @param moodle_url $reporturl the URL to redisplay this report.
e68e4ccf 373 * @param qubaid_condition $qubaids
038014c4 374 * @param string $whichtries
04853f27 375 */
038014c4
JP
376 protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
377 $whichtries = question_attempt::LAST_TRY) {
04853f27 378 global $OUTPUT;
3b1d5cc4 379
04853f27
TH
380 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
381 return;
382 }
3b1d5cc4 383
59ea8176 384 $qtable = new quiz_statistics_question_table($question->id);
04853f27
TH
385 $exportclass = $this->table->export_class_instance();
386 $qtable->export_class_instance($exportclass);
387 if (!$this->table->is_downloading()) {
388 // Output an appropriate title.
c544ee92 389 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
3b1d5cc4 390
869309b8 391 } else {
04853f27 392 // Work out an appropriate title.
7076b309
TH
393 $a = clone($question);
394 $a->variant = $variantno;
395
396 if (!empty($question->number) && !is_null($variantno)) {
397 $questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a);
398 } else if (!empty($question->number)) {
399 $questiontabletitle = get_string('analysisno', 'quiz_statistics', $a);
400 } else if (!is_null($variantno)) {
401 $questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a);
402 } else {
403 $questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a);
4922e79f 404 }
7076b309 405
de81dc87 406 if ($this->table->is_downloading() == 'html') {
d50b05e6 407 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
869309b8 408 }
04853f27
TH
409
410 // Set up the table.
869309b8 411 $exportclass->start_table($questiontabletitle);
edfa0d80 412
de81dc87 413 if ($this->table->is_downloading() == 'html') {
edfa0d80
TH
414 echo $this->render_question_text($question);
415 }
869309b8 416 }
3b1d5cc4 417
038014c4
JP
418 $responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
419 $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
04853f27 420
d50b05e6 421 $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
04853f27
TH
422 if ($this->table->is_downloading()) {
423 $exportclass->output_headers($qtable->headers);
424 }
4922e79f
JP
425
426 // Where no variant no is specified the variant no is actually one.
427 if ($variantno === null) {
428 $variantno = 1;
429 }
430 foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
431 $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
d50b05e6
JP
432 foreach ($subpart->get_response_class_ids() as $responseclassid) {
433 $responseclass = $subpart->get_response_class($responseclassid);
434 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
435 foreach ($tabledata as $row) {
436 $qtable->add_data_keyed($qtable->format_row($row));
869309b8 437 }
869309b8 438 }
43ec99aa 439 }
04853f27
TH
440
441 $qtable->finish_output(!$this->table->is_downloading());
869309b8 442 }
3b1d5cc4 443
04853f27
TH
444 /**
445 * Output the table that lists all the questions in the quiz with their statistics.
038014c4 446 *
c3e2e754
JP
447 * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
448 * the quiz including subqs and
449 * variants.
04853f27 450 */
c3e2e754 451 protected function output_quiz_structure_analysis_table($questionstats) {
ac3e5ed7 452 $tooutput = array();
51e3ded8 453 $limitvariants = !$this->table->is_downloading();
c3e2e754 454 foreach ($questionstats->get_all_slots() as $slot) {
515b3ae6 455 // Output the data for these question statistics.
51e3ded8 456 $tooutput = array_merge($tooutput, $questionstats->structure_analysis_for_one_slot($slot, $limitvariants));
df9ddae6 457 }
ac3e5ed7 458 $this->table->format_and_add_array_of_rows($tooutput);
df9ddae6
JP
459 }
460
04853f27 461 /**
038014c4
JP
462 * Return HTML for table of overall quiz statistics.
463 *
04853f27
TH
464 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
465 * @return string the HTML.
466 */
467 protected function output_quiz_info_table($quizinfo) {
468
469 $quizinfotable = new html_table();
470 $quizinfotable->align = array('center', 'center');
471 $quizinfotable->width = '60%';
472 $quizinfotable->attributes['class'] = 'generaltable titlesleft';
473 $quizinfotable->data = array();
474
475 foreach ($quizinfo as $heading => $value) {
476 $quizinfotable->data[] = array($heading, $value);
71a2b878 477 }
04853f27
TH
478
479 return html_writer::table($quizinfotable);
71a2b878 480 }
43ec99aa 481
04853f27
TH
482 /**
483 * Download the table of overall quiz statistics.
038014c4 484 *
04853f27
TH
485 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
486 */
487 protected function download_quiz_info_table($quizinfo) {
488 global $OUTPUT;
3b1d5cc4 489
de81dc87
SL
490 // HTML download is a special case.
491 if ($this->table->is_downloading() == 'html') {
c544ee92 492 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
04853f27
TH
493 echo $this->output_quiz_info_table($quizinfo);
494 return;
495 }
3b1d5cc4 496
04853f27
TH
497 // Reformat the data ready for output.
498 $headers = array();
499 $row = array();
500 foreach ($quizinfo as $heading => $value) {
501 $headers[] = $heading;
502 $row[] = $value;
503 }
3b1d5cc4 504
04853f27
TH
505 // Do the output.
506 $exportclass = $this->table->export_class_instance();
507 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
508 $exportclass->output_headers($headers);
509 $exportclass->add_data($row);
510 $exportclass->finish_table();
511 }
512
513 /**
514 * Output the HTML needed to show the statistics graph.
038014c4 515 *
baea27e4 516 * @param int|object $quizorid The quiz, or its ID.
10c4fce5 517 * @param qubaid_condition $qubaids the question usages whose responses to analyse.
baea27e4 518 * @param string $whichattempts Which attempts constant.
04853f27 519 */
10c4fce5 520 protected function output_statistics_graph($quizorid, $qubaids) {
baea27e4
FM
521 global $DB, $PAGE;
522
523 $quiz = $quizorid;
524 if (!is_object($quiz)) {
525 $quiz = $DB->get_record('quiz', array('id' => $quizorid), '*', MUST_EXIST);
526 }
baea27e4
FM
527
528 // Load the rest of the required data.
529 $questions = quiz_report_get_significant_questions($quiz);
530
531 // Only load main question not sub questions.
532 $questionstatistics = $DB->get_records_select('question_statistics', 'hashcode = ? AND slot IS NOT NULL',
533 [$qubaids->get_hash_code()]);
534
535 // Configure what to display.
536 $fieldstoplot = [
537 'facility' => get_string('facility', 'quiz_statistics'),
538 'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics')
539 ];
540 $fieldstoplotfactor = ['facility' => 100, 'discriminativeefficiency' => 1];
541
542 // Prepare the arrays to hold the data.
543 $xdata = [];
544 foreach (array_keys($fieldstoplot) as $fieldtoplot) {
545 $ydata[$fieldtoplot] = [];
546 }
547
548 // Fill in the data for each question.
549 foreach ($questionstatistics as $questionstatistic) {
550 $number = $questions[$questionstatistic->slot]->number;
551 $xdata[$number] = $number;
552
553 foreach ($fieldstoplot as $fieldtoplot => $notused) {
554 $value = $questionstatistic->$fieldtoplot;
555 if (is_null($value)) {
556 $value = 0;
557 }
558 $value *= $fieldstoplotfactor[$fieldtoplot];
559 $ydata[$fieldtoplot][$number] = number_format($value, 2);
560 }
561 }
562
563 // Create the chart.
564 sort($xdata);
565 $chart = new \core\chart_bar();
566 $chart->get_xaxis(0, true)->set_label(get_string('position', 'quiz_statistics'));
567 $chart->set_labels(array_values($xdata));
568
569 foreach ($fieldstoplot as $fieldtoplot => $notused) {
570 ksort($ydata[$fieldtoplot]);
571 $series = new \core\chart_series($fieldstoplot[$fieldtoplot], array_values($ydata[$fieldtoplot]));
572 $chart->add_series($series);
573 }
574
575 // Find max.
576 $max = 0;
577 foreach ($fieldstoplot as $fieldtoplot => $notused) {
578 $max = max($max, max($ydata[$fieldtoplot]));
579 }
580
581 // Set Y properties.
582 $yaxis = $chart->get_yaxis(0, true);
583 $yaxis->set_stepsize(10);
584 $yaxis->set_label('%');
04853f27 585
2cdcb905 586 $output = $PAGE->get_renderer('mod_quiz');
2cdcb905 587 $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
baea27e4 588 echo $output->chart($chart, $graphname);
04853f27
TH
589 }
590
04853f27
TH
591 /**
592 * Get the quiz and question statistics, either by loading the cached results,
593 * or by recomputing them.
594 *
8da6fc9d
JP
595 * @param object $quiz the quiz settings.
596 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
6dd9362e
JP
597 * $quiz->grademethod ie.
598 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
599 * we calculate stats based on which attempts would affect the grade for each student.
038014c4
JP
600 * @param string $whichtries which tries to analyse for response analysis. Will be one of
601 * question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
10c4fce5 602 * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
8da6fc9d 603 * @param array $questions full question data.
c3e2e754
JP
604 * @param \core\progress\base|null $progress
605 * @return array with 2 elements: - $quizstats The statistics for overall attempt scores.
606 * - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
04853f27 607 */
10c4fce5
JB
608 public function get_all_stats_and_analysis(
609 $quiz, $whichattempts, $whichtries, \core\dml\sql_join $groupstudentsjoins, $questions, $progress = null) {
8da6fc9d
JP
610
611 if ($progress === null) {
303936aa 612 $progress = new \core\progress\none();
8da6fc9d 613 }
04853f27 614
10c4fce5 615 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
04853f27 616
8da6fc9d 617 $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
04853f27 618
8da6fc9d 619 $quizcalc = new \quiz_statistics\calculator($progress);
7de1e35b 620
038014c4 621 $progress->start_progress('', 3);
7de1e35b 622 if ($quizcalc->get_last_calculated_time($qubaids) === false) {
8da6fc9d 623
e68e4ccf 624 // Recalculate now.
c3e2e754 625 $questionstats = $qcalc->calculate($qubaids);
8da6fc9d 626 $progress->progress(1);
04853f27 627
10c4fce5 628 $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudentsjoins, count($questions),
6dd9362e 629 $qcalc->get_sum_of_mark_variance());
8da6fc9d 630 $progress->progress(2);
e68e4ccf 631 } else {
7de1e35b 632 $quizstats = $quizcalc->get_cached($qubaids);
038014c4 633 $progress->progress(1);
c3e2e754 634 $questionstats = $qcalc->get_cached($qubaids);
038014c4 635 $progress->progress(2);
04853f27
TH
636 }
637
038014c4
JP
638 if ($quizstats->s()) {
639 $subquestions = $questionstats->get_sub_questions();
640 $this->analyse_responses_for_all_questions_and_subquestions($questions,
641 $subquestions,
642 $qubaids,
643 $whichtries,
644 $progress);
645 }
646 $progress->progress(3);
647 $progress->end_progress();
648
c3e2e754 649 return array($quizstats, $questionstats);
04853f27
TH
650 }
651
8da6fc9d
JP
652 /**
653 * Appropriate instance depending if we want html output for the user or not.
654 *
655 * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
656 */
657 protected function get_progress_trace_instance() {
658 if ($this->progress === null) {
659 if (!$this->table->is_downloading()) {
4922e79f 660 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
8da6fc9d
JP
661 $this->progress->set_display_names();
662 } else {
303936aa 663 $this->progress = new \core\progress\none();
8da6fc9d
JP
664 }
665 }
666 return $this->progress;
667 }
668
038014c4
JP
669 /**
670 * Analyse responses for all questions and sub questions in this quiz.
671 *
672 * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
673 * @param object[] $subquestions full question objects.
674 * @param qubaid_condition $qubaids the question usages whose responses to analyse.
675 * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
676 * @param null|\core\progress\base $progress Used to indicate progress of task.
677 */
678 protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
679 $whichtries, $progress = null) {
8da6fc9d 680 if ($progress === null) {
303936aa 681 $progress = new \core\progress\none();
8da6fc9d
JP
682 }
683
684 // Starting response analysis tasks.
c3e2e754 685 $progress->start_progress('', count($questions) + count($subquestions));
8da6fc9d 686
038014c4 687 $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
04853f27 688
038014c4 689 $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
04853f27 690
fc604410 691 // Finished all response analysis tasks.
8da6fc9d 692 $progress->end_progress();
fc604410 693 }
04853f27 694
038014c4
JP
695 /**
696 * Analyse responses for an array of questions or sub questions.
697 *
698 * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations.
699 * @param qubaid_condition $qubaids the question usages whose responses to analyse.
700 * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
701 * @param null|\core\progress\base $progress Used to indicate progress of task.
702 * @param int[] $done array keys are ids of questions that have been analysed before calling method.
703 * @return array array keys are ids of questions that were analysed after this method call.
704 */
705 protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
fc604410
JP
706 $countquestions = count($questions);
707 if (!$countquestions) {
708 return array();
709 }
710 if ($progress === null) {
303936aa 711 $progress = new \core\progress\none();
fc604410
JP
712 }
713 $progress->start_progress('', $countquestions, $countquestions);
714 foreach ($questions as $question) {
715 $progress->increment_progress();
716 if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) {
038014c4
JP
717 $responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
718 if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
719 $responesstats->calculate($qubaids, $whichtries);
720 }
43ec99aa 721 }
fc604410 722 $done[$question->id] = 1;
04853f27 723 }
8da6fc9d 724 $progress->end_progress();
fc604410 725 return $done;
04853f27
TH
726 }
727
728 /**
038014c4
JP
729 * Return a little form for the user to request to download the full report, including quiz stats and response analysis for
730 * all questions and sub-questions.
731 *
6ae04c26 732 * @param moodle_url $reporturl the base URL of the report.
038014c4 733 * @return string HTML.
04853f27 734 */
6ae04c26 735 protected function everything_download_options(moodle_url $reporturl) {
bc3b5a6a 736 global $OUTPUT;
bc3b5a6a 737 return $OUTPUT->download_dataformat_selector(get_string('downloadeverything', 'quiz_statistics'),
6ae04c26 738 $reporturl->out_omit_querystring(), 'download', $reporturl->params() + array('everything' => 1));
04853f27
TH
739 }
740
741 /**
038014c4
JP
742 * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
743 *
744 * @param int $lastcachetime the time the stats were last cached.
e68e4ccf 745 * @param int $quizid the quiz id.
10c4fce5 746 * @param array $groupstudentsjoins (joins, wheres, params) for students in the group or empty array if groups not used.
6dd9362e
JP
747 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
748 * $quiz->grademethod ie.
749 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
750 * we calculate stats based on which attempts would affect the grade for each student.
e68e4ccf 751 * @param moodle_url $reporturl url for this report
038014c4 752 * @return string HTML.
04853f27 753 */
10c4fce5 754 protected function output_caching_info($lastcachetime, $quizid, $groupstudentsjoins, $whichattempts, $reporturl) {
04853f27
TH
755 global $DB, $OUTPUT;
756
038014c4 757 if (empty($lastcachetime)) {
04853f27
TH
758 return '';
759 }
760
761 // Find the number of attempts since the cached statistics were computed.
10c4fce5 762 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts, true);
04853f27
TH
763 $count = $DB->count_records_sql("
764 SELECT COUNT(1)
765 FROM $fromqa
766 WHERE $whereqa
038014c4 767 AND quiza.timefinish > {$lastcachetime}", $qaparams);
04853f27
TH
768
769 if (!$count) {
770 $count = 0;
771 }
772
773 // Generate the output.
0ff4bd08 774 $a = new stdClass();
038014c4 775 $a->lastcalculated = format_time(time() - $lastcachetime);
04853f27
TH
776 $a->count = $count;
777
25a03faa
TH
778 $recalcualteurl = new moodle_url($reporturl,
779 array('recalculate' => 1, 'sesskey' => sesskey()));
04853f27 780 $output = '';
25a03faa
TH
781 $output .= $OUTPUT->box_start(
782 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
04853f27 783 $output .= get_string('lastcalculated', 'quiz_statistics', $a);
25a03faa
TH
784 $output .= $OUTPUT->single_button($recalcualteurl,
785 get_string('recalculatenow', 'quiz_statistics'));
04853f27
TH
786 $output .= $OUTPUT->box_end(true);
787
788 return $output;
789 }
790
791 /**
038014c4
JP
792 * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
793 * is displayed.
794 *
e68e4ccf 795 * @param $qubaids qubaid_condition
04853f27 796 */
e68e4ccf 797 protected function clear_cached_data($qubaids) {
04853f27 798 global $DB;
e68e4ccf
JP
799 $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
800 $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
801 $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
04853f27
TH
802 }
803
3652dddd 804 /**
038014c4
JP
805 * Load the questions in this quiz and add some properties to the objects needed in the reports.
806 *
3652dddd
JP
807 * @param object $quiz the quiz.
808 * @return array of questions for this quiz.
809 */
810 public function load_and_initialise_questions_for_calculations($quiz) {
811 // Load the questions.
812 $questions = quiz_report_get_significant_questions($quiz);
813 $questionids = array();
814 foreach ($questions as $question) {
815 $questionids[] = $question->id;
816 }
817 $fullquestions = question_load_questions($questionids);
818 foreach ($questions as $qno => $question) {
819 $q = $fullquestions[$question->id];
820 $q->maxmark = $question->maxmark;
821 $q->slot = $qno;
822 $q->number = $question->number;
823 $questions[$qno] = $q;
824 }
825 return $questions;
826 }
04853f27 827
4922e79f
JP
828 /**
829 * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
830 *
831 * @param $qubaids
832 * @param $questions
833 * @param $questionstats
834 * @param $reporturl
038014c4 835 * @param $whichtries string
4922e79f 836 */
038014c4
JP
837 protected function output_all_question_response_analysis($qubaids,
838 $questions,
839 $questionstats,
840 $reporturl,
841 $whichtries = question_attempt::LAST_TRY) {
4922e79f
JP
842 foreach ($questions as $slot => $question) {
843 if (question_bank::get_qtype(
844 $question->qtype, false)->can_analyse_responses()
845 ) {
846 if ($questionstats->for_slot($slot)->get_variants()) {
847 foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
848 $this->output_individual_question_response_analysis($question,
849 $variantno,
850 $questionstats->for_slot($slot, $variantno)->s,
851 $reporturl,
038014c4
JP
852 $qubaids,
853 $whichtries);
4922e79f
JP
854 }
855 } else {
856 $this->output_individual_question_response_analysis($question,
857 null,
858 $questionstats->for_slot($slot)->s,
859 $reporturl,
038014c4
JP
860 $qubaids,
861 $whichtries);
4922e79f
JP
862 }
863 } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
864 foreach ($subqids as $subqid) {
865 if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
866 foreach ($variants as $variantno) {
867 $this->output_individual_question_response_analysis(
868 $questionstats->for_subq($subqid, $variantno)->question,
869 $variantno,
870 $questionstats->for_subq($subqid, $variantno)->s,
871 $reporturl,
038014c4
JP
872 $qubaids,
873 $whichtries);
4922e79f
JP
874 }
875 } else {
876 $this->output_individual_question_response_analysis(
877 $questionstats->for_subq($subqid)->question,
878 null,
879 $questionstats->for_subq($subqid)->s,
880 $reporturl,
038014c4
JP
881 $qubaids,
882 $whichtries);
4922e79f
JP
883
884 }
885 }
886 }
887 }
888 }
889}