MDL-41757 quiz statistics : limit subq and variant stat rows
[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
TH
20 * @package quiz_statistics
21 * @copyright 2008 Jamie Pratt
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
23 */
24
a17b297d
TH
25defined('MOODLE_INTERNAL') || die();
26
04853f27
TH
27require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
28require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
29require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
e68e4ccf 30require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
04853f27
TH
31/**
32 * The quiz statistics report provides summary information about each question in
33 * a quiz, compared to the whole quiz. It also provides a drill-down to more
34 * detailed information about each question.
35 *
8d76124c
TH
36 * @copyright 2008 Jamie Pratt
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 38 */
c386eaa3 39class quiz_statistics_report extends quiz_default_report {
7de1e35b 40
df9ddae6
JP
41 const SUBQ_AND_VARIANT_ROW_LIMIT = 10;
42
7de1e35b
JP
43 /**
44 * @var context_module
45 */
46 protected $context;
3b1d5cc4 47
515b3ae6 48 /** @var quiz_statistics_table instance of table class used for main questions stats table. */
04853f27 49 protected $table;
0c1c764e 50
8da6fc9d
JP
51 /** @var \core\progress\base|null $progress Handles progress reporting or not. */
52 protected $progress = null;
53
0c1c764e 54 /**
55 * Display the report.
56 */
04853f27 57 public function display($quiz, $cm, $course) {
8da6fc9d
JP
58 global $OUTPUT;
59
60 raise_memory_limit(MEMORY_HUGE);
0c1c764e 61
26aded55 62 $this->context = context_module::instance($cm->id);
0c1c764e 63
f16ed06c
JP
64 if (!quiz_questions_in_quiz($quiz->questions)) {
65 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
66 echo quiz_no_questions_message($quiz, $cm, $this->context);
67 return true;
68 }
69
04853f27 70 // Work out the display options.
0c1c764e 71 $download = optional_param('download', '', PARAM_ALPHA);
869309b8 72 $everything = optional_param('everything', 0, PARAM_BOOL);
d1789d5d 73 $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
d50b05e6 74 // A qid paramter indicates we should display the detailed analysis of a sub question.
43ec99aa 75 $qid = optional_param('qid', 0, PARAM_INT);
04853f27 76 $slot = optional_param('slot', 0, PARAM_INT);
6dd9362e 77 $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
04853f27 78
0c1c764e 79 $pageoptions = array();
80 $pageoptions['id'] = $cm->id;
0c1c764e 81 $pageoptions['mode'] = 'statistics';
82
a6855934 83 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
0c1c764e 84
6b4e2d76 85 $mform = new quiz_statistics_settings_form($reporturl);
6dd9362e
JP
86
87 $mform->set_data(array('whichattempts' => $whichattempts));
88
04853f27 89 if ($fromform = $mform->get_data()) {
6dd9362e
JP
90 $whichattempts = $fromform->whichattempts;
91 }
04853f27 92
6dd9362e
JP
93 if ($whichattempts != $quiz->grademethod) {
94 $reporturl->param('whichattempts', $whichattempts);
0c1c764e 95 }
96
768a7588 97 // Find out current groups mode.
490668bb 98 $currentgroup = $this->get_current_group($cm, $course, $this->context);
04853f27
TH
99 $nostudentsingroup = false; // True if a group is selected and there is no one in it.
100 if (empty($currentgroup)) {
101 $currentgroup = 0;
102 $groupstudents = array();
3b1d5cc4 103
e4977ba5
TH
104 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
105 $groupstudents = array();
106 $nostudentsingroup = true;
107
04853f27 108 } else {
768a7588 109 // All users who can attempt quizzes and who are in the currently selected group.
fdb5bc03 110 $groupstudents = get_users_by_capability($this->context,
04853f27
TH
111 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
112 '', '', '', '', $currentgroup, '', false);
113 if (!$groupstudents) {
e72efdd4 114 $nostudentsingroup = true;
0c1c764e 115 }
0c1c764e 116 }
3b1d5cc4 117
6dd9362e 118 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
e68e4ccf 119
04853f27 120 // If recalculate was requested, handle that.
db77f410 121 if ($recalculate && confirm_sesskey()) {
e68e4ccf 122 $this->clear_cached_data($qubaids);
e42f153c 123 redirect($reporturl);
43ec99aa 124 }
3b1d5cc4 125
04853f27 126 // Set up the main table.
59ea8176 127 $this->table = new quiz_statistics_table();
04853f27
TH
128 if ($everything) {
129 $report = get_string('completestatsfilename', 'quiz_statistics');
130 } else {
131 $report = get_string('questionstatsfilename', 'quiz_statistics');
132 }
0eafc988 133 $courseshortname = format_string($course->shortname, true,
26aded55 134 array('context' => context_course::instance($course->id)));
8ebbb06a 135 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
25a03faa
TH
136 $this->table->is_downloading($download, $filename,
137 get_string('quizstructureanalysis', 'quiz_statistics'));
3652dddd 138 $questions = $this->load_and_initialise_questions_for_calculations($quiz);
04853f27 139
8da6fc9d
JP
140 // Print the page header stuff (if not downloading.
141 if (!$this->table->is_downloading()) {
142 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
143 }
144
7de1e35b
JP
145 if (!$nostudentsingroup) {
146 // Get the data to be displayed.
8da6fc9d 147 $progress = $this->get_progress_trace_instance();
515b3ae6 148 list($quizstats, $questionstats, $subquestionstats) =
8da6fc9d 149 $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
7de1e35b
JP
150 } else {
151 // Or create empty stats containers.
8da6fc9d 152 $quizstats = new \quiz_statistics\calculated($whichattempts);
515b3ae6
JP
153 $questionstats = array();
154 $subquestionstats = array();
7de1e35b 155 }
04853f27 156
04853f27 157 // Set up the table, if there is data.
7de1e35b
JP
158 if ($quizstats->s()) {
159 $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
43ec99aa 160 }
3b1d5cc4 161
8da6fc9d 162 // Print the rest of the page header stuff (if not downloading.
04853f27 163 if (!$this->table->is_downloading()) {
04853f27 164
e4977ba5 165 if (groups_get_activity_groupmode($cm)) {
04853f27
TH
166 groups_print_activity_menu($cm, $reporturl->out());
167 if ($currentgroup && !$groupstudents) {
168 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
b0e4fa41
TH
169 }
170 }
171
f16ed06c 172 if (!$this->table->is_downloading() && $quizstats->s() == 0) {
3c6185e9
TH
173 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
174 }
175
04853f27 176 // Print display options form.
04853f27
TH
177 $mform->display();
178 }
179
180 if ($everything) { // Implies is downloading.
181 // Overall report, then the analysis of each question.
d50b05e6 182 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27
TH
183 $this->download_quiz_info_table($quizinfo);
184
7de1e35b 185 if ($quizstats->s()) {
515b3ae6 186 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
04853f27 187
7de1e35b 188 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
6dd9362e 189 $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
869309b8 190 }
04853f27 191
515b3ae6 192 foreach ($questions as $slot => $question) {
25a03faa
TH
193 if (question_bank::get_qtype(
194 $question->qtype, false)->can_analyse_responses()) {
04853f27 195 $this->output_individual_question_response_analysis(
515b3ae6 196 $question, $questionstats[$slot]->s, $reporturl, $qubaids);
04853f27 197
515b3ae6
JP
198 } else if (!empty($questionstats[$slot]->subquestions)) {
199 $subitemstodisplay = explode(',', $questionstats[$slot]->subquestions);
04853f27
TH
200 foreach ($subitemstodisplay as $subitemid) {
201 $this->output_individual_question_response_analysis(
515b3ae6 202 $subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->s, $reporturl, $qubaids);
869309b8 203 }
204 }
869309b8 205 }
206 }
04853f27
TH
207
208 $this->table->export_class_instance()->finish_document();
209
210 } else if ($slot) {
211 // Report on an individual question indexed by position.
212 if (!isset($questions[$slot])) {
213 print_error('questiondoesnotexist', 'question');
869309b8 214 }
04853f27 215
515b3ae6
JP
216 $this->output_individual_question_data($quiz, $questionstats[$slot]);
217 $this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids);
04853f27
TH
218
219 // Back to overview link.
220 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
221 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
2f1ba2ae 222 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
04853f27
TH
223
224 } else if ($qid) {
225 // Report on an individual sub-question indexed questionid.
d50b05e6 226 if (!isset($subquestionstats[$qid])) {
43ec99aa 227 print_error('questiondoesnotexist', 'question');
71a2b878 228 }
04853f27 229
515b3ae6
JP
230 $this->output_individual_question_data($quiz, $subquestionstats[$qid]);
231 $this->output_individual_question_response_analysis($subquestionstats[$qid]->question,
232 $subquestionstats[$qid]->s, $reporturl, $qubaids);
04853f27
TH
233
234 // Back to overview link.
235 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
236 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
237 'boxaligncenter generalbox boxwidthnormal mdl-align');
238
239 } else if ($this->table->is_downloading()) {
240 // Downloading overview report.
d50b05e6 241 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27 242 $this->download_quiz_info_table($quizinfo);
515b3ae6 243 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
04853f27
TH
244 $this->table->finish_output();
245
246 } else {
247 // On-screen display of overview report.
c544ee92 248 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
6dd9362e 249 echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
04853f27 250 echo $this->everything_download_options();
d50b05e6 251 $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
04853f27 252 echo $this->output_quiz_info_table($quizinfo);
7de1e35b 253 if ($quizstats->s()) {
c544ee92 254 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
515b3ae6 255 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
6dd9362e 256 $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
b0e4fa41 257 }
43ec99aa 258 }
04853f27 259
43ec99aa 260 return true;
261 }
3b1d5cc4 262
04853f27
TH
263 /**
264 * Display the statistical and introductory information about a question.
265 * Only called when not downloading.
d50b05e6 266 * @param object $quiz the quiz settings.
515b3ae6 267 * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
04853f27 268 */
515b3ae6 269 protected function output_individual_question_data($quiz, $questionstat) {
04853f27
TH
270 global $OUTPUT;
271
272 // On-screen display. Show a summary of the question's place in the quiz,
273 // and the question statistics.
515b3ae6 274 $datumfromtable = $this->table->format_row($questionstat);
04853f27
TH
275
276 // Set up the question info table.
277 $questioninfotable = new html_table();
278 $questioninfotable->align = array('center', 'center');
279 $questioninfotable->width = '60%';
280 $questioninfotable->attributes['class'] = 'generaltable titlesleft';
281
282 $questioninfotable->data = array();
283 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
284 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
515b3ae6 285 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
04853f27
TH
286 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
287 $datumfromtable['icon'] . '&nbsp;' .
515b3ae6 288 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
04853f27
TH
289 $datumfromtable['icon']);
290 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
515b3ae6 291 $questionstat->positions);
04853f27
TH
292
293 // Set up the question statistics table.
294 $questionstatstable = new html_table();
295 $questionstatstable->align = array('center', 'center');
296 $questionstatstable->width = '60%';
297 $questionstatstable->attributes['class'] = 'generaltable titlesleft';
298
299 unset($datumfromtable['number']);
300 unset($datumfromtable['icon']);
301 $actions = $datumfromtable['actions'];
302 unset($datumfromtable['actions']);
303 unset($datumfromtable['name']);
25a03faa
TH
304 $labels = array(
305 's' => get_string('attempts', 'quiz_statistics'),
306 'facility' => get_string('facility', 'quiz_statistics'),
307 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
308 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
309 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
310 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
311 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
312 'discriminative_efficiency' =>
313 get_string('discriminative_efficiency', 'quiz_statistics')
314 );
04853f27
TH
315 foreach ($datumfromtable as $item => $value) {
316 $questionstatstable->data[] = array($labels[$item], $value);
869309b8 317 }
04853f27
TH
318
319 // Display the various bits.
c544ee92 320 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
04853f27 321 echo html_writer::table($questioninfotable);
515b3ae6 322 echo $this->render_question_text($questionstat->question);
c544ee92 323 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
04853f27 324 echo html_writer::table($questionstatstable);
869309b8 325 }
3b1d5cc4 326
edfa0d80
TH
327 /**
328 * @param object $question question data.
329 * @return string HTML of question text, ready for display.
330 */
fdb5bc03 331 protected function render_question_text($question) {
edfa0d80 332 global $OUTPUT;
fdb5bc03 333
68d2f6a0
TH
334 $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
335 $question->contextid, 'question', 'questiontext', $question->id,
336 $this->context->id, 'quiz_statistics');
fdb5bc03
TH
337
338 return $OUTPUT->box(format_text($text, $question->questiontextformat,
339 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
edfa0d80
TH
340 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
341 }
342
04853f27
TH
343 /**
344 * Display the response analysis for a question.
d50b05e6
JP
345 * @param object $question the question to report on.
346 * @param int $s
347 * @param moodle_url $reporturl the URL to redisplay this report.
e68e4ccf 348 * @param qubaid_condition $qubaids
04853f27 349 */
515b3ae6 350 protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
04853f27 351 global $OUTPUT;
3b1d5cc4 352
04853f27
TH
353 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
354 return;
355 }
3b1d5cc4 356
59ea8176 357 $qtable = new quiz_statistics_question_table($question->id);
04853f27
TH
358 $exportclass = $this->table->export_class_instance();
359 $qtable->export_class_instance($exportclass);
360 if (!$this->table->is_downloading()) {
361 // Output an appropriate title.
c544ee92 362 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
3b1d5cc4 363
869309b8 364 } else {
04853f27
TH
365 // Work out an appropriate title.
366 $questiontabletitle = '"' . $question->name . '"';
367 if (!empty($question->number)) {
368 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
369 }
370 if ($this->table->is_downloading() == 'xhtml') {
d50b05e6 371 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
869309b8 372 }
04853f27
TH
373
374 // Set up the table.
869309b8 375 $exportclass->start_table($questiontabletitle);
edfa0d80
TH
376
377 if ($this->table->is_downloading() == 'xhtml') {
378 echo $this->render_question_text($question);
379 }
869309b8 380 }
3b1d5cc4 381
d50b05e6
JP
382 $responesanalyser = new \core_question\statistics\responses\analyser($question);
383 $responseanalysis = $responesanalyser->load_cached($qubaids);
04853f27 384
d50b05e6 385 $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
04853f27
TH
386 if ($this->table->is_downloading()) {
387 $exportclass->output_headers($qtable->headers);
388 }
d50b05e6
JP
389 foreach ($responseanalysis->get_subpart_ids() as $partid) {
390 $subpart = $responseanalysis->get_subpart($partid);
391 foreach ($subpart->get_response_class_ids() as $responseclassid) {
392 $responseclass = $subpart->get_response_class($responseclassid);
393 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
394 foreach ($tabledata as $row) {
395 $qtable->add_data_keyed($qtable->format_row($row));
869309b8 396 }
869309b8 397 }
43ec99aa 398 }
04853f27
TH
399
400 $qtable->finish_output(!$this->table->is_downloading());
869309b8 401 }
3b1d5cc4 402
04853f27
TH
403 /**
404 * Output the table that lists all the questions in the quiz with their statistics.
f7970e3c 405 * @param int $s number of attempts.
515b3ae6
JP
406 * @param \core_question\statistics\questions\calculated[] $questionstats the stats for the main questions in the quiz.
407 * @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats the stats of any random questions.
04853f27 408 */
515b3ae6 409 protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
04853f27
TH
410 if (!$s) {
411 return;
412 }
413
515b3ae6
JP
414 foreach ($questionstats as $questionstat) {
415 // Output the data for these question statistics.
416 $this->table->add_data_keyed($this->table->format_row($questionstat));
1239d287 417 if (count($questionstat->variantstats) > 1) {
df9ddae6
JP
418 if (count($questionstat->variantstats) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
419 $statstoadd = $this->find_min_median_and_max_facility_stats_objects($questionstat->variantstats);
420 } else {
421 ksort($questionstat->variantstats);
422 $statstoadd = $questionstat->variantstats;
1239d287 423 }
df9ddae6 424 $this->add_array_of_rows_to_table($statstoadd);
1239d287 425 }
04853f27 426
515b3ae6 427 if (empty($questionstat->subquestions)) {
04853f27 428 continue;
71a2b878 429 }
43ec99aa 430
df9ddae6 431 // And its sub-questions, if it has any.
515b3ae6 432 $subitemstodisplay = explode(',', $questionstat->subquestions);
df9ddae6
JP
433
434 // We need to get all variants out of sub-questions to count them and possibly find min, median and max.
1239d287 435 $displayorder = 1;
df9ddae6 436 $subqvariants = array();
04853f27 437 foreach ($subitemstodisplay as $subitemid) {
1239d287
JP
438 if (count($subquestionstats[$subitemid]->variantstats) > 1) {
439 ksort($subquestionstats[$subitemid]->variantstats);
440 foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) {
441 $variantstat->subqdisplayorder = $displayorder;
442 $variantstat->question->number = $questionstat->question->number;
df9ddae6 443 $subqvariants[] = $variantstat;
1239d287
JP
444 }
445 }
446 $displayorder++;
04853f27 447 }
df9ddae6
JP
448 if (count($subqvariants) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
449 // Too many variants from randomly selected questions.
450 $toadd = $this->find_min_median_and_max_facility_stats_objects($subqvariants);
451 $this->add_array_of_rows_to_table($toadd);
452 } else if (count($subitemstodisplay) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
453 // Too many randomly selected questions.
454 $toadd = $this->find_min_median_and_max_facility_stats_objects($subitemstodisplay);
455 $this->add_array_of_rows_to_table($toadd);
456 } else {
457 foreach ($subitemstodisplay as $subitemid) {
458 $subquestionstats[$subitemid]->maxmark = $questionstat->maxmark;
459 $subquestionstats[$subitemid]->subqdisplayorder = $displayorder;
460 $subquestionstats[$subitemid]->question->number = $questionstat->question->number;
461 $this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid]));
462 if (count($subquestionstats[$subitemid]->variantstats) > 1) {
463 ksort($subquestionstats[$subitemid]->variantstats);
464 foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) {
465 $this->table->add_data_keyed($this->table->format_row($variantstat));
466 }
467 }
468 }
469 }
470
0c1c764e 471 }
04853f27
TH
472
473 $this->table->finish_output(!$this->table->is_downloading());
43ec99aa 474 }
3b1d5cc4 475
df9ddae6
JP
476 protected function find_min_median_and_max_facility_stats_objects($questionstats) {
477 $facilities = array();
478 foreach ($questionstats as $key => $questionstat) {
479 $facilities[$key] = (float)$questionstat->facility;
480 }
481 asort($facilities);
482 $facilitykeys = array_keys($facilities);
483 $keyformin = $facilitykeys[0];
484 $keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2)-1)];
485 $keyformax = $facilitykeys[count($facilitykeys) - 1];
486 $toreturn = array();
487 foreach (array($keyformin => 'minimumfacility',
488 $keyformedian => 'medianfacility',
489 $keyformax => 'maximumfacility') as $key => $stringid) {
490 $questionstats[$key]->minmedianmaxnotice = get_string($stringid, 'quiz_statistics');
491 $toreturn[] = $questionstats[$key];
492 }
493 return $toreturn;
494 }
495
496
497 /**
498 * @param \core_question\statistics\questions\calculator $statstoadd
499 */
500 protected function add_array_of_rows_to_table($statstoadd) {
501 foreach ($statstoadd as $stattoadd) {
502 $this->table->add_data_keyed($this->table->format_row($stattoadd));
503 }
504 }
505
04853f27
TH
506 /**
507 * Output the table of overall quiz statistics.
508 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
509 * @return string the HTML.
510 */
511 protected function output_quiz_info_table($quizinfo) {
512
513 $quizinfotable = new html_table();
514 $quizinfotable->align = array('center', 'center');
515 $quizinfotable->width = '60%';
516 $quizinfotable->attributes['class'] = 'generaltable titlesleft';
517 $quizinfotable->data = array();
518
519 foreach ($quizinfo as $heading => $value) {
520 $quizinfotable->data[] = array($heading, $value);
71a2b878 521 }
04853f27
TH
522
523 return html_writer::table($quizinfotable);
71a2b878 524 }
43ec99aa 525
04853f27
TH
526 /**
527 * Download the table of overall quiz statistics.
528 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
529 */
530 protected function download_quiz_info_table($quizinfo) {
531 global $OUTPUT;
3b1d5cc4 532
04853f27
TH
533 // XHTML download is a special case.
534 if ($this->table->is_downloading() == 'xhtml') {
c544ee92 535 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
04853f27
TH
536 echo $this->output_quiz_info_table($quizinfo);
537 return;
538 }
3b1d5cc4 539
04853f27
TH
540 // Reformat the data ready for output.
541 $headers = array();
542 $row = array();
543 foreach ($quizinfo as $heading => $value) {
544 $headers[] = $heading;
545 $row[] = $value;
546 }
3b1d5cc4 547
04853f27
TH
548 // Do the output.
549 $exportclass = $this->table->export_class_instance();
550 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
551 $exportclass->output_headers($headers);
552 $exportclass->add_data($row);
553 $exportclass->finish_table();
554 }
555
556 /**
557 * Output the HTML needed to show the statistics graph.
e68e4ccf
JP
558 * @param $quizid
559 * @param $currentgroup
6dd9362e 560 * @param $whichattempts
04853f27 561 */
6dd9362e 562 protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
2cdcb905 563 global $PAGE;
04853f27 564
2cdcb905 565 $output = $PAGE->get_renderer('mod_quiz');
04853f27 566 $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
6dd9362e 567 compact('quizid', 'currentgroup', 'whichattempts'));
2cdcb905
TH
568 $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
569 echo $output->graph($imageurl, $graphname);
04853f27
TH
570 }
571
04853f27
TH
572 /**
573 * Get the quiz and question statistics, either by loading the cached results,
574 * or by recomputing them.
575 *
8da6fc9d
JP
576 * @param object $quiz the quiz settings.
577 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
6dd9362e
JP
578 * $quiz->grademethod ie.
579 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
580 * we calculate stats based on which attempts would affect the grade for each student.
8da6fc9d
JP
581 * @param array $groupstudents students in this group.
582 * @param array $questions full question data.
04853f27
TH
583 * @return array with 4 elements:
584 * - $quizstats The statistics for overall attempt scores.
515b3ae6
JP
585 * - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot.
586 * - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id.
04853f27 587 */
8da6fc9d
JP
588 public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
589
590 if ($progress === null) {
591 $progress = new \core\progress\null();
592 }
04853f27 593
6dd9362e 594 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
04853f27 595
8da6fc9d 596 $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
04853f27 597
8da6fc9d 598 $quizcalc = new \quiz_statistics\calculator($progress);
7de1e35b
JP
599
600 if ($quizcalc->get_last_calculated_time($qubaids) === false) {
8da6fc9d
JP
601
602 $progress->start_progress('', 3);
603
e68e4ccf 604 // Recalculate now.
515b3ae6 605 list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
8da6fc9d 606 $progress->progress(1);
04853f27 607
6dd9362e
JP
608 $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
609 $qcalc->get_sum_of_mark_variance());
8da6fc9d 610 $progress->progress(2);
7de1e35b 611 if ($quizstats->s()) {
8da6fc9d 612 $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress);
e68e4ccf 613 }
8da6fc9d
JP
614 $progress->progress(3);
615 $progress->end_progress();
e68e4ccf 616 } else {
7de1e35b 617 $quizstats = $quizcalc->get_cached($qubaids);
515b3ae6 618 list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
04853f27
TH
619 }
620
515b3ae6 621 return array($quizstats, $questionstats, $subquestionstats);
04853f27
TH
622 }
623
8da6fc9d
JP
624 /**
625 * Appropriate instance depending if we want html output for the user or not.
626 *
627 * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
628 */
629 protected function get_progress_trace_instance() {
630 if ($this->progress === null) {
631 if (!$this->table->is_downloading()) {
632 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
633 $this->progress->set_display_names();
634 } else {
635 $this->progress = new \core\progress\null();
636 }
637 }
638 return $this->progress;
639 }
640
641 protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats,
642 $progress = null) {
643
644 if ($progress === null) {
645 $progress = new \core\progress\null();
646 }
647
648 // Starting response analysis tasks.
649 $progress->start_progress('', count($questions) + count($subquestionstats));
650
651 // Starting response analysis of main questions.
652 $progress->start_progress('', count($questions), count($questions));
04853f27
TH
653
654 $done = array();
8da6fc9d 655 $donecount = 1;
04853f27 656 foreach ($questions as $question) {
8da6fc9d
JP
657 $progress->progress($donecount);
658 $donecount++;
04853f27
TH
659 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
660 continue;
43ec99aa 661 }
04853f27
TH
662 $done[$question->id] = 1;
663
515b3ae6 664 $responesstats = new \core_question\statistics\responses\analyser($question);
e68e4ccf 665 $responesstats->calculate($qubaids);
04853f27 666 }
8da6fc9d 667 $progress->end_progress();
04853f27 668
8da6fc9d
JP
669 // Starting response analysis of sub-questions.
670 $countsubquestions = count($subquestionstats);
671 $progress->start_progress('', $countsubquestions, $countsubquestions);
672 $donecount = 1;
515b3ae6 673 foreach ($subquestionstats as $subquestionstat) {
8da6fc9d
JP
674 $progress->progress($donecount);
675 $donecount++;
515b3ae6
JP
676 if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
677 isset($done[$subquestionstat->question->id])) {
04853f27 678 continue;
43ec99aa 679 }
515b3ae6 680 $done[$subquestionstat->question->id] = 1;
04853f27 681
515b3ae6 682 $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
e68e4ccf 683 $responesstats->calculate($qubaids);
04853f27 684 }
8da6fc9d
JP
685 // Finished sub-question tasks.
686 $progress->end_progress();
687
688 // Finished all response analysis tasks.
689 $progress->end_progress();
04853f27
TH
690 }
691
692 /**
693 * @return string HTML snipped for the Download full report as UI.
694 */
695 protected function everything_download_options() {
696 $downloadoptions = $this->table->get_download_menu();
697
0465ef6e
RT
698 $downloadelements = new stdClass();
699 $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
700 $this->table->defaultdownloadformat, false);
701 $downloadelements->downloadbutton = '<input type="submit" value="' .
702 get_string('download') . '"/>';
703
04853f27
TH
704 $output = '<form action="'. $this->table->baseurl .'" method="post">';
705 $output .= '<div class="mdl-align">';
706 $output .= '<input type="hidden" name="everything" value="1"/>';
0465ef6e 707 $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
04853f27
TH
708 $output .= '</div></form>';
709
710 return $output;
711 }
712
713 /**
714 * Generate the snipped of HTML that says when the stats were last caculated,
715 * with a recalcuate now button.
e68e4ccf
JP
716 * @param object $quizstats the overall quiz statistics.
717 * @param int $quizid the quiz id.
6dd9362e
JP
718 * @param array $groupstudents ids of students in the group or empty array if groups not used.
719 * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
720 * $quiz->grademethod ie.
721 * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
722 * we calculate stats based on which attempts would affect the grade for each student.
e68e4ccf 723 * @param moodle_url $reporturl url for this report
25a03faa
TH
724 * @return string a HTML snipped saying when the stats were last computed,
725 * or blank if that is not appropriate.
04853f27 726 */
6dd9362e 727 protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
04853f27
TH
728 global $DB, $OUTPUT;
729
730 if (empty($quizstats->timemodified)) {
731 return '';
732 }
733
734 // Find the number of attempts since the cached statistics were computed.
6dd9362e 735 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
04853f27
TH
736 $count = $DB->count_records_sql("
737 SELECT COUNT(1)
738 FROM $fromqa
739 WHERE $whereqa
740 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
741
742 if (!$count) {
743 $count = 0;
744 }
745
746 // Generate the output.
0ff4bd08 747 $a = new stdClass();
04853f27
TH
748 $a->lastcalculated = format_time(time() - $quizstats->timemodified);
749 $a->count = $count;
750
25a03faa
TH
751 $recalcualteurl = new moodle_url($reporturl,
752 array('recalculate' => 1, 'sesskey' => sesskey()));
04853f27 753 $output = '';
25a03faa
TH
754 $output .= $OUTPUT->box_start(
755 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
04853f27 756 $output .= get_string('lastcalculated', 'quiz_statistics', $a);
25a03faa
TH
757 $output .= $OUTPUT->single_button($recalcualteurl,
758 get_string('recalculatenow', 'quiz_statistics'));
04853f27
TH
759 $output .= $OUTPUT->box_end(true);
760
761 return $output;
762 }
763
764 /**
765 * Clear the cached data for a particular report configuration. This will
766 * trigger a re-computation the next time the report is displayed.
e68e4ccf 767 * @param $qubaids qubaid_condition
04853f27 768 */
e68e4ccf 769 protected function clear_cached_data($qubaids) {
04853f27 770 global $DB;
e68e4ccf
JP
771 $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
772 $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
773 $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
04853f27
TH
774 }
775
3652dddd
JP
776 /**
777 * @param object $quiz the quiz.
778 * @return array of questions for this quiz.
779 */
780 public function load_and_initialise_questions_for_calculations($quiz) {
781 // Load the questions.
782 $questions = quiz_report_get_significant_questions($quiz);
783 $questionids = array();
784 foreach ($questions as $question) {
785 $questionids[] = $question->id;
786 }
787 $fullquestions = question_load_questions($questionids);
788 foreach ($questions as $qno => $question) {
789 $q = $fullquestions[$question->id];
790 $q->maxmark = $question->maxmark;
791 $q->slot = $qno;
792 $q->number = $question->number;
793 $questions[$qno] = $q;
794 }
795 return $questions;
796 }
0c1c764e 797}
04853f27 798