84cfe2720f763f99ab32fa01b173d5b3a69479d3
[moodle.git] / mod / quiz / report / statistics / 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  * Quiz statistics report class.
19  *
20  * @package   quiz_statistics
21  * @copyright 2014 Open University
22  * @author    James Pratt <me@jamiep.org>
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
29 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
30 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
31 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
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  *
37  * @copyright 2008 Jamie Pratt
38  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class quiz_statistics_report extends quiz_default_report {
42     /** @var context_module context of this quiz.*/
43     protected $context;
45     /** @var quiz_statistics_table instance of table class used for main questions stats table. */
46     protected $table;
48     /** @var \core\progress\base|null $progress Handles progress reporting or not. */
49     protected $progress = null;
51     /**
52      * Display the report.
53      */
54     public function display($quiz, $cm, $course) {
55         global $OUTPUT, $DB;
57         raise_memory_limit(MEMORY_HUGE);
59         $this->context = context_module::instance($cm->id);
61         if (!quiz_has_questions($quiz->id)) {
62             $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
63             echo quiz_no_questions_message($quiz, $cm, $this->context);
64             return true;
65         }
67         // Work out the display options.
68         $download = optional_param('download', '', PARAM_ALPHA);
69         $everything = optional_param('everything', 0, PARAM_BOOL);
70         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
71         // A qid paramter indicates we should display the detailed analysis of a sub question.
72         $qid = optional_param('qid', 0, PARAM_INT);
73         $slot = optional_param('slot', 0, PARAM_INT);
74         $variantno = optional_param('variant', null, PARAM_INT);
75         $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
76         $whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);
78         $pageoptions = array();
79         $pageoptions['id'] = $cm->id;
80         $pageoptions['mode'] = 'statistics';
82         $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
84         $mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));
86         $mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries));
88         if ($whichattempts != $quiz->grademethod) {
89             $reporturl->param('whichattempts', $whichattempts);
90         }
92         if ($whichtries != question_attempt::LAST_TRY) {
93             $reporturl->param('whichtries', $whichtries);
94         }
96         // Find out current groups mode.
97         $currentgroup = $this->get_current_group($cm, $course, $this->context);
98         $nostudentsingroup = false; // True if a group is selected and there is no one in it.
99         if (empty($currentgroup)) {
100             $currentgroup = 0;
101             $groupstudentsjoins = new \core\dml\sql_join();
103         } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
104             $groupstudentsjoins = new \core\dml\sql_join();
105             $nostudentsingroup = true;
107         } else {
108             // All users who can attempt quizzes and who are in the currently selected group.
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                 }
119             }
120         }
122         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
124         // If recalculate was requested, handle that.
125         if ($recalculate && confirm_sesskey()) {
126             $this->clear_cached_data($qubaids);
127             redirect($reporturl);
128         }
130         // Set up the main table.
131         $this->table = new quiz_statistics_table();
132         if ($everything) {
133             $report = get_string('completestatsfilename', 'quiz_statistics');
134         } else {
135             $report = get_string('questionstatsfilename', 'quiz_statistics');
136         }
137         $courseshortname = format_string($course->shortname, true,
138                 array('context' => context_course::instance($course->id)));
139         $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
140         $this->table->is_downloading($download, $filename,
141                 get_string('quizstructureanalysis', 'quiz_statistics'));
142         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
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         }
149         if (!$nostudentsingroup) {
150             // Get the data to be displayed.
151             $progress = $this->get_progress_trace_instance();
152             list($quizstats, $questionstats) =
153                 $this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudentsjoins, $questions, $progress);
154         } else {
155             // Or create empty stats containers.
156             $quizstats = new \quiz_statistics\calculated($whichattempts);
157             $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
158         }
160         // Set up the table.
161         $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
163         // Print the rest of the page header stuff (if not downloading.
164         if (!$this->table->is_downloading()) {
166             if (groups_get_activity_groupmode($cm)) {
167                 groups_print_activity_menu($cm, $reporturl->out());
168                 if ($currentgroup && $nostudentsingroup) {
169                     $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
170                 }
171             }
173             if (!$this->table->is_downloading() && $quizstats->s() == 0) {
174                 echo $OUTPUT->notification(get_string('nogradedattempts', 'quiz_statistics'));
175             }
177             foreach ($questionstats->any_error_messages() as $errormessage) {
178                 echo $OUTPUT->notification($errormessage);
179             }
181             // Print display options form.
182             $mform->display();
183         }
185         if ($everything) { // Implies is downloading.
186             // Overall report, then the analysis of each question.
187             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
188             $this->download_quiz_info_table($quizinfo);
190             if ($quizstats->s()) {
191                 $this->output_quiz_structure_analysis_table($questionstats);
193                 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
194                     $this->output_statistics_graph($quiz->id, $qubaids);
195                 }
197                 $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
198             }
200             $this->table->export_class_instance()->finish_document();
202         } else if ($qid) {
203             // Report on an individual sub-question indexed questionid.
204             if (!$questionstats->has_subq($qid, $variantno)) {
205                 print_error('questiondoesnotexist', 'question');
206             }
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,
213                                                                 $qubaids,
214                                                                 $whichtries);
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');
219         } else if ($slot) {
220             // Report on an individual question indexed by position.
221             if (!isset($questions[$slot])) {
222                 print_error('questiondoesnotexist', 'question');
223             }
225             if ($variantno === null &&
226                                 ($questionstats->for_slot($slot)->get_sub_question_ids()
227                                 || $questionstats->for_slot($slot)->get_variants())) {
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 {
235                 $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
236                 $this->output_individual_question_response_analysis($questions[$slot],
237                                                                     $variantno,
238                                                                     $questionstats->for_slot($slot, $variantno)->s,
239                                                                     $reporturl,
240                                                                     $qubaids,
241                                                                     $whichtries);
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             }
252         } else if ($this->table->is_downloading()) {
253             // Downloading overview report.
254             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
255             $this->download_quiz_info_table($quizinfo);
256             if ($quizstats->s()) {
257                 $this->output_quiz_structure_analysis_table($questionstats);
258             }
259             $this->table->export_class_instance()->finish_document();
261         } else {
262             // On-screen display of overview report.
263             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
264             echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudentsjoins, $whichattempts, $reporturl);
265             echo $this->everything_download_options($reporturl);
266             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
267             echo $this->output_quiz_info_table($quizinfo);
268             if ($quizstats->s()) {
269                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
270                 $this->output_quiz_structure_analysis_table($questionstats);
271                 $this->output_statistics_graph($quiz, $qubaids);
272             }
273         }
275         return true;
276     }
278     /**
279      * Display the statistical and introductory information about a question.
280      * Only called when not downloading.
281      *
282      * @param object                                         $quiz         the quiz settings.
283      * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
284      */
285     protected function output_individual_question_data($quiz, $questionstat) {
286         global $OUTPUT;
288         // On-screen display. Show a summary of the question's place in the quiz,
289         // and the question statistics.
290         $datumfromtable = $this->table->format_row($questionstat);
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';
298         $questioninfotable->data = array();
299         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
300         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
301                 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
303         if ($questionstat->variant !== null) {
304             $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
306         }
307         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
308                 $datumfromtable['icon'] . '&nbsp;' .
309                 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
310                 $datumfromtable['icon']);
311         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
312                 $questionstat->positions);
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';
320         unset($datumfromtable['number']);
321         unset($datumfromtable['icon']);
322         $actions = $datumfromtable['actions'];
323         unset($datumfromtable['actions']);
324         unset($datumfromtable['name']);
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         );
336         foreach ($datumfromtable as $item => $value) {
337             $questionstatstable->data[] = array($labels[$item], $value);
338         }
340         // Display the various bits.
341         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
342         echo html_writer::table($questioninfotable);
343         echo $this->render_question_text($questionstat->question);
344         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
345         echo html_writer::table($questionstatstable);
346     }
348     /**
349      * Output question text in a box with urls appropriate for a preview of the question.
350      *
351      * @param object $question question data.
352      * @return string HTML of question text, ready for display.
353      */
354     protected function render_question_text($question) {
355         global $OUTPUT;
357         $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
358                 $question->contextid, 'question', 'questiontext', $question->id,
359                 $this->context->id, 'quiz_statistics');
361         return $OUTPUT->box(format_text($text, $question->questiontextformat,
362                 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
363                 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
364     }
366     /**
367      * Display the response analysis for a question.
368      *
369      * @param object           $question  the question to report on.
370      * @param int|null         $variantno the variant
371      * @param int              $s
372      * @param moodle_url       $reporturl the URL to redisplay this report.
373      * @param qubaid_condition $qubaids
374      * @param string           $whichtries
375      */
376     protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
377                                                                     $whichtries = question_attempt::LAST_TRY) {
378         global $OUTPUT;
380         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
381             return;
382         }
384         $qtable = new quiz_statistics_question_table($question->id);
385         $exportclass = $this->table->export_class_instance();
386         $qtable->export_class_instance($exportclass);
387         if (!$this->table->is_downloading()) {
388             // Output an appropriate title.
389             echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
391         } else {
392             // Work out an appropriate title.
393             $a = clone($question);
394             $a->variant = $variantno;
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);
404             }
406             if ($this->table->is_downloading() == 'xhtml') {
407                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
408             }
410             // Set up the table.
411             $exportclass->start_table($questiontabletitle);
413             if ($this->table->is_downloading() == 'xhtml') {
414                 echo $this->render_question_text($question);
415             }
416         }
418         $responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
419         $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
421         $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
422         if ($this->table->is_downloading()) {
423             $exportclass->output_headers($qtable->headers);
424         }
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);
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));
437                 }
438             }
439         }
441         $qtable->finish_output(!$this->table->is_downloading());
442     }
444     /**
445      * Output the table that lists all the questions in the quiz with their statistics.
446      *
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.
450      */
451     protected function output_quiz_structure_analysis_table($questionstats) {
452         $tooutput = array();
453         $limitvariants = !$this->table->is_downloading();
454         foreach ($questionstats->get_all_slots() as $slot) {
455             // Output the data for these question statistics.
456             $tooutput = array_merge($tooutput, $questionstats->structure_analysis_for_one_slot($slot, $limitvariants));
457         }
458         $this->table->format_and_add_array_of_rows($tooutput);
459     }
461     /**
462      * Return HTML for table of overall quiz statistics.
463      *
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) {
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();
475         foreach ($quizinfo as $heading => $value) {
476              $quizinfotable->data[] = array($heading, $value);
477         }
479         return html_writer::table($quizinfotable);
480     }
482     /**
483      * Download the table of overall quiz statistics.
484      *
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;
490         // XHTML download is a special case.
491         if ($this->table->is_downloading() == 'xhtml') {
492             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
493             echo $this->output_quiz_info_table($quizinfo);
494             return;
495         }
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         }
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     }
513     /**
514      * Output the HTML needed to show the statistics graph.
515      *
516      * @param int|object $quizorid The quiz, or its ID.
517      * @param qubaid_condition $qubaids the question usages whose responses to analyse.
518      * @param string $whichattempts Which attempts constant.
519      */
520     protected function output_statistics_graph($quizorid, $qubaids) {
521         global $DB, $PAGE;
523         $quiz = $quizorid;
524         if (!is_object($quiz)) {
525             $quiz = $DB->get_record('quiz', array('id' => $quizorid), '*', MUST_EXIST);
526         }
528         // Load the rest of the required data.
529         $questions = quiz_report_get_significant_questions($quiz);
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()]);
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];
542         // Prepare the arrays to hold the data.
543         $xdata = [];
544         foreach (array_keys($fieldstoplot) as $fieldtoplot) {
545             $ydata[$fieldtoplot] = [];
546         }
548         // Fill in the data for each question.
549         foreach ($questionstatistics as $questionstatistic) {
550             $number = $questions[$questionstatistic->slot]->number;
551             $xdata[$number] = $number;
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         }
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));
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         }
575         // Find max.
576         $max = 0;
577         foreach ($fieldstoplot as $fieldtoplot => $notused) {
578             $max = max($max, max($ydata[$fieldtoplot]));
579         }
581         // Set Y properties.
582         $yaxis = $chart->get_yaxis(0, true);
583         $yaxis->set_stepsize(10);
584         $yaxis->set_label('%');
586         $output = $PAGE->get_renderer('mod_quiz');
587         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
588         echo $output->chart($chart, $graphname);
589     }
591     /**
592      * Get the quiz and question statistics, either by loading the cached results,
593      * or by recomputing them.
594      *
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
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.
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.
602      * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
603      * @param array  $questions          full question data.
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
607      */
608     public function get_all_stats_and_analysis(
609             $quiz, $whichattempts, $whichtries, \core\dml\sql_join $groupstudentsjoins, $questions, $progress = null) {
611         if ($progress === null) {
612             $progress = new \core\progress\none();
613         }
615         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);
617         $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
619         $quizcalc = new \quiz_statistics\calculator($progress);
621         $progress->start_progress('', 3);
622         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
624             // Recalculate now.
625             $questionstats = $qcalc->calculate($qubaids);
626             $progress->progress(1);
628             $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudentsjoins, count($questions),
629                                               $qcalc->get_sum_of_mark_variance());
630             $progress->progress(2);
631         } else {
632             $quizstats = $quizcalc->get_cached($qubaids);
633             $progress->progress(1);
634             $questionstats = $qcalc->get_cached($qubaids);
635             $progress->progress(2);
636         }
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();
649         return array($quizstats, $questionstats);
650     }
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()) {
660                 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
661                 $this->progress->set_display_names();
662             } else {
663                 $this->progress = new \core\progress\none();
664             }
665         }
666         return $this->progress;
667     }
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) {
680         if ($progress === null) {
681             $progress = new \core\progress\none();
682         }
684         // Starting response analysis tasks.
685         $progress->start_progress('', count($questions) + count($subquestions));
687         $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
689         $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
691         // Finished all response analysis tasks.
692         $progress->end_progress();
693     }
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()) {
706         $countquestions = count($questions);
707         if (!$countquestions) {
708             return array();
709         }
710         if ($progress === null) {
711             $progress = new \core\progress\none();
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])) {
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                 }
721             }
722             $done[$question->id] = 1;
723         }
724         $progress->end_progress();
725         return $done;
726     }
728     /**
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      *
732      * @param moodle_url $reporturl the base URL of the report.
733      * @return string HTML.
734      */
735     protected function everything_download_options(moodle_url $reporturl) {
736         global $OUTPUT;
737         return $OUTPUT->download_dataformat_selector(get_string('downloadeverything', 'quiz_statistics'),
738             $reporturl->out_omit_querystring(), 'download', $reporturl->params() + array('everything' => 1));
739     }
741     /**
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.
745      * @param int    $quizid         the quiz id.
746      * @param array  $groupstudentsjoins (joins, wheres, params) for students in the group or empty array if groups not used.
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.
751      * @param moodle_url $reporturl url for this report
752      * @return string HTML.
753      */
754     protected function output_caching_info($lastcachetime, $quizid, $groupstudentsjoins, $whichattempts, $reporturl) {
755         global $DB, $OUTPUT;
757         if (empty($lastcachetime)) {
758             return '';
759         }
761         // Find the number of attempts since the cached statistics were computed.
762         list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts, true);
763         $count = $DB->count_records_sql("
764                 SELECT COUNT(1)
765                 FROM $fromqa
766                 WHERE $whereqa
767                 AND quiza.timefinish > {$lastcachetime}", $qaparams);
769         if (!$count) {
770             $count = 0;
771         }
773         // Generate the output.
774         $a = new stdClass();
775         $a->lastcalculated = format_time(time() - $lastcachetime);
776         $a->count = $count;
778         $recalcualteurl = new moodle_url($reporturl,
779                 array('recalculate' => 1, 'sesskey' => sesskey()));
780         $output = '';
781         $output .= $OUTPUT->box_start(
782                 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
783         $output .= get_string('lastcalculated', 'quiz_statistics', $a);
784         $output .= $OUTPUT->single_button($recalcualteurl,
785                 get_string('recalculatenow', 'quiz_statistics'));
786         $output .= $OUTPUT->box_end(true);
788         return $output;
789     }
791     /**
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      *
795      * @param $qubaids qubaid_condition
796      */
797     protected function clear_cached_data($qubaids) {
798         global $DB;
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()));
802     }
804     /**
805      * Load the questions in this quiz and add some properties to the objects needed in the reports.
806      *
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     }
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
835      * @param $whichtries string
836      */
837     protected function output_all_question_response_analysis($qubaids,
838                                                              $questions,
839                                                              $questionstats,
840                                                              $reporturl,
841                                                              $whichtries = question_attempt::LAST_TRY) {
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,
852                                                                             $qubaids,
853                                                                             $whichtries);
854                     }
855                 } else {
856                     $this->output_individual_question_response_analysis($question,
857                                                                         null,
858                                                                         $questionstats->for_slot($slot)->s,
859                                                                         $reporturl,
860                                                                         $qubaids,
861                                                                         $whichtries);
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,
872                                 $qubaids,
873                                 $whichtries);
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,
881                             $qubaids,
882                             $whichtries);
884                     }
885                 }
886             }
887         }
888     }