MDL-41759 quiz statistics : break down responses for each variant
[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 2008 Jamie Pratt
22  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
28 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
29 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
30 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
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  *
36  * @copyright 2008 Jamie Pratt
37  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class quiz_statistics_report extends quiz_default_report {
41     /**
42      * @var context_module
43      */
44     protected $context;
46     /** @var quiz_statistics_table instance of table class used for main questions stats table. */
47     protected $table;
49     /** @var \core\progress\base|null $progress Handles progress reporting or not. */
50     protected $progress = null;
52     /**
53      * Display the report.
54      */
55     public function display($quiz, $cm, $course) {
56         global $OUTPUT;
58         raise_memory_limit(MEMORY_HUGE);
60         $this->context = context_module::instance($cm->id);
62         if (!quiz_questions_in_quiz($quiz->questions)) {
63             $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
64             echo quiz_no_questions_message($quiz, $cm, $this->context);
65             return true;
66         }
68         // Work out the display options.
69         $download = optional_param('download', '', PARAM_ALPHA);
70         $everything = optional_param('everything', 0, PARAM_BOOL);
71         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
72         // A qid paramter indicates we should display the detailed analysis of a sub question.
73         $qid = optional_param('qid', 0, PARAM_INT);
74         $slot = optional_param('slot', 0, PARAM_INT);
75         $variantno = optional_param('variant', null, PARAM_INT);
76         $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
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);
86         $mform->set_data(array('whichattempts' => $whichattempts));
88         if ($fromform = $mform->get_data()) {
89             $whichattempts = $fromform->whichattempts;
90         }
92         if ($whichattempts != $quiz->grademethod) {
93             $reporturl->param('whichattempts', $whichattempts);
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             $groupstudents = array();
103         } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
104             $groupstudents = array();
105             $nostudentsingroup = true;
107         } else {
108             // All users who can attempt quizzes and who are in the currently selected group.
109             $groupstudents = get_users_by_capability($this->context,
110                     array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
111                     '', '', '', '', $currentgroup, '', false);
112             if (!$groupstudents) {
113                 $nostudentsingroup = true;
114             }
115         }
117         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
119         // If recalculate was requested, handle that.
120         if ($recalculate && confirm_sesskey()) {
121             $this->clear_cached_data($qubaids);
122             redirect($reporturl);
123         }
125         // Set up the main table.
126         $this->table = new quiz_statistics_table();
127         if ($everything) {
128             $report = get_string('completestatsfilename', 'quiz_statistics');
129         } else {
130             $report = get_string('questionstatsfilename', 'quiz_statistics');
131         }
132         $courseshortname = format_string($course->shortname, true,
133                 array('context' => context_course::instance($course->id)));
134         $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
135         $this->table->is_downloading($download, $filename,
136                 get_string('quizstructureanalysis', 'quiz_statistics'));
137         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
139         // Print the page header stuff (if not downloading.
140         if (!$this->table->is_downloading()) {
141             $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
142         }
144         if (!$nostudentsingroup) {
145             // Get the data to be displayed.
146             $progress = $this->get_progress_trace_instance();
147             list($quizstats, $questionstats) =
148                 $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
149         } else {
150             // Or create empty stats containers.
151             $quizstats = new \quiz_statistics\calculated($whichattempts);
152             $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
153         }
155         // Set up the table, if there is data.
156         if ($quizstats->s()) {
157             $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
158         }
160         // Print the rest of the page header stuff (if not downloading.
161         if (!$this->table->is_downloading()) {
163             if (groups_get_activity_groupmode($cm)) {
164                 groups_print_activity_menu($cm, $reporturl->out());
165                 if ($currentgroup && !$groupstudents) {
166                     $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
167                 }
168             }
170             if (!$this->table->is_downloading() && $quizstats->s() == 0) {
171                 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
172             }
174             foreach ($questionstats->any_error_messages() as $errormessage) {
175                 echo $OUTPUT->notification($errormessage);
176             }
178             // Print display options form.
179             $mform->display();
180         }
182         if ($everything) { // Implies is downloading.
183             // Overall report, then the analysis of each question.
184             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
185             $this->download_quiz_info_table($quizinfo);
187             if ($quizstats->s()) {
188                 $this->output_quiz_structure_analysis_table($questionstats);
190                 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
191                     $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
192                 }
194                 $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl);
195             }
197             $this->table->export_class_instance()->finish_document();
199         } else if ($qid) {
200             // Report on an individual sub-question indexed questionid.
201             if (is_null($questionstats->for_subq($qid, $variantno))) {
202                 print_error('questiondoesnotexist', 'question');
203             }
205             $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));
206             $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,
207                                                                 $variantno,
208                                                                 $questionstats->for_subq($qid, $variantno)->s,
209                                                                 $reporturl,
210                                                                 $qubaids);
211             // Back to overview link.
212             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
213                               get_string('backtoquizreport', 'quiz_statistics') . '</a>',
214                               'boxaligncenter generalbox boxwidthnormal mdl-align');
215         } else if ($slot) {
216             // Report on an individual question indexed by position.
217             if (!isset($questions[$slot])) {
218                 print_error('questiondoesnotexist', 'question');
219             }
221             if ($variantno === null &&
222                                 ($questionstats->for_slot($slot)->get_sub_question_ids()
223                                 || $questionstats->for_slot($slot)->get_variants())) {
224                 if (!$this->table->is_downloading()) {
225                     $number = $questionstats->for_slot($slot)->question->number;
226                     echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
227                 }
228                 $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot)));
229                 $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
230             } else {
231                 $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
232                 $this->output_individual_question_response_analysis($questions[$slot],
233                                                                     $variantno,
234                                                                     $questionstats->for_slot($slot, $variantno)->s,
235                                                                     $reporturl,
236                                                                     $qubaids);
237             }
238             if (!$this->table->is_downloading()) {
239                 // Back to overview link.
240                 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
241                         get_string('backtoquizreport', 'quiz_statistics') . '</a>',
242                         'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
243             } else {
244                 $this->table->finish_output();
245             }
247         } else if ($this->table->is_downloading()) {
248             // Downloading overview report.
249             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
250             $this->download_quiz_info_table($quizinfo);
251             if ($quizstats->s()) {
252                 $this->output_quiz_structure_analysis_table($questionstats);
253             }
254             $this->table->finish_output();
256         } else {
257             // On-screen display of overview report.
258             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
259             echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
260             echo $this->everything_download_options();
261             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
262             echo $this->output_quiz_info_table($quizinfo);
263             if ($quizstats->s()) {
264                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
265                 $this->output_quiz_structure_analysis_table($questionstats);
266                 $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
267             }
268         }
270         return true;
271     }
273     /**
274      * Display the statistical and introductory information about a question.
275      * Only called when not downloading.
276      * @param object                                         $quiz         the quiz settings.
277      * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
278      */
279     protected function output_individual_question_data($quiz, $questionstat) {
280         global $OUTPUT;
282         // On-screen display. Show a summary of the question's place in the quiz,
283         // and the question statistics.
284         $datumfromtable = $this->table->format_row($questionstat);
286         // Set up the question info table.
287         $questioninfotable = new html_table();
288         $questioninfotable->align = array('center', 'center');
289         $questioninfotable->width = '60%';
290         $questioninfotable->attributes['class'] = 'generaltable titlesleft';
292         $questioninfotable->data = array();
293         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
294         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
295                 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
297         if ($questionstat->variant !== null) {
298             $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
300         }
301         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
302                 $datumfromtable['icon'] . '&nbsp;' .
303                 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
304                 $datumfromtable['icon']);
305         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
306                 $questionstat->positions);
308         // Set up the question statistics table.
309         $questionstatstable = new html_table();
310         $questionstatstable->align = array('center', 'center');
311         $questionstatstable->width = '60%';
312         $questionstatstable->attributes['class'] = 'generaltable titlesleft';
314         unset($datumfromtable['number']);
315         unset($datumfromtable['icon']);
316         $actions = $datumfromtable['actions'];
317         unset($datumfromtable['actions']);
318         unset($datumfromtable['name']);
319         $labels = array(
320             's' => get_string('attempts', 'quiz_statistics'),
321             'facility' => get_string('facility', 'quiz_statistics'),
322             'sd' => get_string('standarddeviationq', 'quiz_statistics'),
323             'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
324             'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
325             'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
326             'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
327             'discriminative_efficiency' =>
328                                 get_string('discriminative_efficiency', 'quiz_statistics')
329         );
330         foreach ($datumfromtable as $item => $value) {
331             $questionstatstable->data[] = array($labels[$item], $value);
332         }
334         // Display the various bits.
335         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
336         echo html_writer::table($questioninfotable);
337         echo $this->render_question_text($questionstat->question);
338         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
339         echo html_writer::table($questionstatstable);
340     }
342     /**
343      * @param object $question question data.
344      * @return string HTML of question text, ready for display.
345      */
346     protected function render_question_text($question) {
347         global $OUTPUT;
349         $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
350                 $question->contextid, 'question', 'questiontext', $question->id,
351                 $this->context->id, 'quiz_statistics');
353         return $OUTPUT->box(format_text($text, $question->questiontextformat,
354                 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
355                 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
356     }
358     /**
359      * Display the response analysis for a question.
360      *
361      * @param object           $question  the question to report on.
362      * @param int|null         $variantno the variant
363      * @param int              $s
364      * @param moodle_url       $reporturl the URL to redisplay this report.
365      * @param qubaid_condition $qubaids
366      */
367     protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids) {
368         global $OUTPUT;
370         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
371             return;
372         }
374         $qtable = new quiz_statistics_question_table($question->id);
375         $exportclass = $this->table->export_class_instance();
376         $qtable->export_class_instance($exportclass);
377         if (!$this->table->is_downloading()) {
378             // Output an appropriate title.
379             echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
381         } else {
382             // Work out an appropriate title.
383             $questiontabletitle = '"' . $question->name . '"';
384             if (!empty($question->number)) {
385                 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
386             }
387             if (!is_null($variantno)) {
388                 $questiontabletitle .= ' '.get_string('variantno', 'quiz_statistics', $variantno);
389             }
390             if ($this->table->is_downloading() == 'xhtml') {
391                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
392             }
394             // Set up the table.
395             $exportclass->start_table($questiontabletitle);
397             if ($this->table->is_downloading() == 'xhtml') {
398                 echo $this->render_question_text($question);
399             }
400         }
402         $responesanalyser = new \core_question\statistics\responses\analyser($question);
403         $responseanalysis = $responesanalyser->load_cached($qubaids);
405         $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
406         if ($this->table->is_downloading()) {
407             $exportclass->output_headers($qtable->headers);
408         }
410         // Where no variant no is specified the variant no is actually one.
411         if ($variantno === null) {
412             $variantno = 1;
413         }
414         foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
415             $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
416             foreach ($subpart->get_response_class_ids() as $responseclassid) {
417                 $responseclass = $subpart->get_response_class($responseclassid);
418                 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
419                 foreach ($tabledata as $row) {
420                     $qtable->add_data_keyed($qtable->format_row($row));
421                 }
422             }
423         }
425         $qtable->finish_output(!$this->table->is_downloading());
426     }
428     /**
429      * Output the table that lists all the questions in the quiz with their statistics.
430      * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
431      *                                                                                               the quiz including subqs and
432      *                                                                                               variants.
433      */
434     protected function output_quiz_structure_analysis_table($questionstats) {
435         $tooutput = array();
436         $limitvariants = !$this->table->is_downloading();
437         foreach ($questionstats->get_all_slots() as $slot) {
438             // Output the data for these question statistics.
439             $tooutput = array_merge($tooutput, $questionstats->structure_analysis_for_one_slot($slot, $limitvariants));
440         }
441         $this->table->format_and_add_array_of_rows($tooutput);
442     }
444     /**
445      * Output the table of overall quiz statistics.
446      * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
447      * @return string the HTML.
448      */
449     protected function output_quiz_info_table($quizinfo) {
451         $quizinfotable = new html_table();
452         $quizinfotable->align = array('center', 'center');
453         $quizinfotable->width = '60%';
454         $quizinfotable->attributes['class'] = 'generaltable titlesleft';
455         $quizinfotable->data = array();
457         foreach ($quizinfo as $heading => $value) {
458              $quizinfotable->data[] = array($heading, $value);
459         }
461         return html_writer::table($quizinfotable);
462     }
464     /**
465      * Download the table of overall quiz statistics.
466      * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
467      */
468     protected function download_quiz_info_table($quizinfo) {
469         global $OUTPUT;
471         // XHTML download is a special case.
472         if ($this->table->is_downloading() == 'xhtml') {
473             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
474             echo $this->output_quiz_info_table($quizinfo);
475             return;
476         }
478         // Reformat the data ready for output.
479         $headers = array();
480         $row = array();
481         foreach ($quizinfo as $heading => $value) {
482             $headers[] = $heading;
483             $row[] = $value;
484         }
486         // Do the output.
487         $exportclass = $this->table->export_class_instance();
488         $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
489         $exportclass->output_headers($headers);
490         $exportclass->add_data($row);
491         $exportclass->finish_table();
492     }
494     /**
495      * Output the HTML needed to show the statistics graph.
496      * @param $quizid
497      * @param $currentgroup
498      * @param $whichattempts
499      */
500     protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
501         global $PAGE;
503         $output = $PAGE->get_renderer('mod_quiz');
504         $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
505                                     compact('quizid', 'currentgroup', 'whichattempts'));
506         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
507         echo $output->graph($imageurl, $graphname);
508     }
510     /**
511      * Get the quiz and question statistics, either by loading the cached results,
512      * or by recomputing them.
513      *
514      * @param object $quiz               the quiz settings.
515      * @param string $whichattempts      which attempts to use, represented internally as one of the constants as used in
516      *                                   $quiz->grademethod ie.
517      *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
518      *                                   we calculate stats based on which attempts would affect the grade for each student.
519      * @param array  $groupstudents      students in this group.
520      * @param array  $questions          full question data.
521      * @param \core\progress\base|null   $progress
522      * @return array with 2 elements:    - $quizstats The statistics for overall attempt scores.
523      *                                   - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
524      */
525     public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
527         if ($progress === null) {
528             $progress = new \core\progress\null();
529         }
531         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
533         $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
535         $quizcalc = new \quiz_statistics\calculator($progress);
537         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
539             $progress->start_progress('', 3);
541             // Recalculate now.
542             $questionstats = $qcalc->calculate($qubaids);
543             $progress->progress(1);
545             $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
546                                               $qcalc->get_sum_of_mark_variance());
547             $progress->progress(2);
548             if ($quizstats->s()) {
549                 $subquestions = $questionstats->get_sub_questions();
550                 $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions, $progress);
551             }
552             $progress->progress(3);
553             $progress->end_progress();
554         } else {
555             $quizstats = $quizcalc->get_cached($qubaids);
556             $questionstats = $qcalc->get_cached($qubaids);
557         }
559         return array($quizstats, $questionstats);
560     }
562     /**
563      * Appropriate instance depending if we want html output for the user or not.
564      *
565      * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
566      */
567     protected function get_progress_trace_instance() {
568         if ($this->progress === null) {
569             if (!$this->table->is_downloading()) {
570                 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
571                 $this->progress->set_display_names();
572             } else {
573                 $this->progress = new \core\progress\null();
574             }
575         }
576         return $this->progress;
577     }
579     protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions,
580                                                                             $progress = null) {
582         if ($progress === null) {
583             $progress = new \core\progress\null();
584         }
586         // Starting response analysis tasks.
587         $progress->start_progress('', count($questions) + count($subquestions));
589         // Starting response analysis of main questions.
590         $progress->start_progress('', count($questions), count($questions));
592         $done = array();
593         $donecount = 1;
594         foreach ($questions as $question) {
595             $progress->progress($donecount);
596             $donecount++;
597             if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
598                 continue;
599             }
600             $done[$question->id] = 1;
602             $responesstats = new \core_question\statistics\responses\analyser($question);
603             $responesstats->calculate($qubaids);
604         }
605         $progress->end_progress();
607         // Starting response analysis of sub-questions.
608         $countsubquestions = count($subquestions);
609         $progress->start_progress('', $countsubquestions, $countsubquestions);
610         $donecount = 1;
611         foreach ($subquestions as $subquestion) {
612             $progress->progress($donecount);
613             $donecount++;
614             if (!question_bank::get_qtype($subquestion->qtype, false)->can_analyse_responses() ||
615                     isset($done[$subquestion->id])) {
616                 continue;
617             }
618             $done[$subquestion->id] = 1;
620             $responesstats = new \core_question\statistics\responses\analyser($subquestion);
621             $responesstats->calculate($qubaids);
622         }
623         // Finished sub-question tasks.
624         $progress->end_progress();
626         // Finished all response analysis tasks.
627         $progress->end_progress();
628     }
630     /**
631      * @return string HTML snipped for the Download full report as UI.
632      */
633     protected function everything_download_options() {
634         $downloadoptions = $this->table->get_download_menu();
636         $downloadelements = new stdClass();
637         $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
638                 $this->table->defaultdownloadformat, false);
639         $downloadelements->downloadbutton = '<input type="submit" value="' .
640                 get_string('download') . '"/>';
642         $output = '<form action="'. $this->table->baseurl .'" method="post">';
643         $output .= '<div class="mdl-align">';
644         $output .= '<input type="hidden" name="everything" value="1"/>';
645         $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
646         $output .= '</div></form>';
648         return $output;
649     }
651     /**
652      * Generate the snipped of HTML that says when the stats were last caculated,
653      * with a recalcuate now button.
654      * @param object $quizstats      the overall quiz statistics.
655      * @param int    $quizid         the quiz id.
656      * @param array  $groupstudents  ids of students in the group or empty array if groups not used.
657      * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
658      *                                   $quiz->grademethod ie.
659      *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
660      *                                   we calculate stats based on which attempts would affect the grade for each student.
661      * @param moodle_url $reporturl url for this report
662      * @return string a HTML snipped saying when the stats were last computed,
663      *      or blank if that is not appropriate.
664      */
665     protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
666         global $DB, $OUTPUT;
668         if (empty($quizstats->timemodified)) {
669             return '';
670         }
672         // Find the number of attempts since the cached statistics were computed.
673         list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
674         $count = $DB->count_records_sql("
675                 SELECT COUNT(1)
676                 FROM $fromqa
677                 WHERE $whereqa
678                 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
680         if (!$count) {
681             $count = 0;
682         }
684         // Generate the output.
685         $a = new stdClass();
686         $a->lastcalculated = format_time(time() - $quizstats->timemodified);
687         $a->count = $count;
689         $recalcualteurl = new moodle_url($reporturl,
690                 array('recalculate' => 1, 'sesskey' => sesskey()));
691         $output = '';
692         $output .= $OUTPUT->box_start(
693                 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
694         $output .= get_string('lastcalculated', 'quiz_statistics', $a);
695         $output .= $OUTPUT->single_button($recalcualteurl,
696                 get_string('recalculatenow', 'quiz_statistics'));
697         $output .= $OUTPUT->box_end(true);
699         return $output;
700     }
702     /**
703      * Clear the cached data for a particular report configuration. This will
704      * trigger a re-computation the next time the report is displayed.
705      * @param $qubaids qubaid_condition
706      */
707     protected function clear_cached_data($qubaids) {
708         global $DB;
709         $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
710         $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
711         $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
712     }
714     /**
715      * @param object $quiz the quiz.
716      * @return array of questions for this quiz.
717      */
718     public function load_and_initialise_questions_for_calculations($quiz) {
719         // Load the questions.
720         $questions = quiz_report_get_significant_questions($quiz);
721         $questionids = array();
722         foreach ($questions as $question) {
723             $questionids[] = $question->id;
724         }
725         $fullquestions = question_load_questions($questionids);
726         foreach ($questions as $qno => $question) {
727             $q = $fullquestions[$question->id];
728             $q->maxmark = $question->maxmark;
729             $q->slot = $qno;
730             $q->number = $question->number;
731             $questions[$qno] = $q;
732         }
733         return $questions;
734     }
736     /**
737      * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
738      *
739      * @param $qubaids
740      * @param $questions
741      * @param $questionstats
742      * @param $reporturl
743      */
744     protected function output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl) {
745         foreach ($questions as $slot => $question) {
746             if (question_bank::get_qtype(
747                 $question->qtype, false)->can_analyse_responses()
748             ) {
749                 if ($questionstats->for_slot($slot)->get_variants()) {
750                     foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
751                         $this->output_individual_question_response_analysis($question,
752                                                                             $variantno,
753                                                                             $questionstats->for_slot($slot, $variantno)->s,
754                                                                             $reporturl,
755                                                                             $qubaids);
756                     }
757                 } else {
758                     $this->output_individual_question_response_analysis($question,
759                                                                         null,
760                                                                         $questionstats->for_slot($slot)->s,
761                                                                         $reporturl,
762                                                                         $qubaids);
763                 }
764             } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
765                 foreach ($subqids as $subqid) {
766                     if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
767                         foreach ($variants as $variantno) {
768                             $this->output_individual_question_response_analysis(
769                                 $questionstats->for_subq($subqid, $variantno)->question,
770                                 $variantno,
771                                 $questionstats->for_subq($subqid, $variantno)->s,
772                                 $reporturl,
773                                 $qubaids);
774                         }
775                     } else {
776                         $this->output_individual_question_response_analysis(
777                             $questionstats->for_subq($subqid)->question,
778                             null,
779                             $questionstats->for_subq($subqid)->s,
780                             $reporturl,
781                             $qubaids);
783                     }
784                 }
785             }
786         }
787     }