MDL-41757 quiz statistics : limit subq and variant stat rows
[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     const SUBQ_AND_VARIANT_ROW_LIMIT = 10;
43     /**
44      * @var context_module
45      */
46     protected $context;
48     /** @var quiz_statistics_table instance of table class used for main questions stats table. */
49     protected $table;
51     /** @var \core\progress\base|null $progress Handles progress reporting or not. */
52     protected $progress = null;
54     /**
55      * Display the report.
56      */
57     public function display($quiz, $cm, $course) {
58         global $OUTPUT;
60         raise_memory_limit(MEMORY_HUGE);
62         $this->context = context_module::instance($cm->id);
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         }
70         // Work out the display options.
71         $download = optional_param('download', '', PARAM_ALPHA);
72         $everything = optional_param('everything', 0, PARAM_BOOL);
73         $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
74         // A qid paramter indicates we should display the detailed analysis of a sub question.
75         $qid = optional_param('qid', 0, PARAM_INT);
76         $slot = optional_param('slot', 0, PARAM_INT);
77         $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
79         $pageoptions = array();
80         $pageoptions['id'] = $cm->id;
81         $pageoptions['mode'] = 'statistics';
83         $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
85         $mform = new quiz_statistics_settings_form($reporturl);
87         $mform->set_data(array('whichattempts' => $whichattempts));
89         if ($fromform = $mform->get_data()) {
90             $whichattempts = $fromform->whichattempts;
91         }
93         if ($whichattempts != $quiz->grademethod) {
94             $reporturl->param('whichattempts', $whichattempts);
95         }
97         // Find out current groups mode.
98         $currentgroup = $this->get_current_group($cm, $course, $this->context);
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();
104         } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
105             $groupstudents = array();
106             $nostudentsingroup = true;
108         } else {
109             // All users who can attempt quizzes and who are in the currently selected group.
110             $groupstudents = get_users_by_capability($this->context,
111                     array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
112                     '', '', '', '', $currentgroup, '', false);
113             if (!$groupstudents) {
114                 $nostudentsingroup = true;
115             }
116         }
118         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
120         // If recalculate was requested, handle that.
121         if ($recalculate && confirm_sesskey()) {
122             $this->clear_cached_data($qubaids);
123             redirect($reporturl);
124         }
126         // Set up the main table.
127         $this->table = new quiz_statistics_table();
128         if ($everything) {
129             $report = get_string('completestatsfilename', 'quiz_statistics');
130         } else {
131             $report = get_string('questionstatsfilename', 'quiz_statistics');
132         }
133         $courseshortname = format_string($course->shortname, true,
134                 array('context' => context_course::instance($course->id)));
135         $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
136         $this->table->is_downloading($download, $filename,
137                 get_string('quizstructureanalysis', 'quiz_statistics'));
138         $questions = $this->load_and_initialise_questions_for_calculations($quiz);
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         }
145         if (!$nostudentsingroup) {
146             // Get the data to be displayed.
147             $progress = $this->get_progress_trace_instance();
148             list($quizstats, $questionstats, $subquestionstats) =
149                 $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
150         } else {
151             // Or create empty stats containers.
152             $quizstats = new \quiz_statistics\calculated($whichattempts);
153             $questionstats = array();
154             $subquestionstats = array();
155         }
157         // Set up the table, if there is data.
158         if ($quizstats->s()) {
159             $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
160         }
162         // Print the rest of the page header stuff (if not downloading.
163         if (!$this->table->is_downloading()) {
165             if (groups_get_activity_groupmode($cm)) {
166                 groups_print_activity_menu($cm, $reporturl->out());
167                 if ($currentgroup && !$groupstudents) {
168                     $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
169                 }
170             }
172             if (!$this->table->is_downloading() && $quizstats->s() == 0) {
173                 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
174             }
176             // Print display options form.
177             $mform->display();
178         }
180         if ($everything) { // Implies is downloading.
181             // Overall report, then the analysis of each question.
182             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
183             $this->download_quiz_info_table($quizinfo);
185             if ($quizstats->s()) {
186                 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
188                 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
189                     $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
190                 }
192                 foreach ($questions as $slot => $question) {
193                     if (question_bank::get_qtype(
194                             $question->qtype, false)->can_analyse_responses()) {
195                         $this->output_individual_question_response_analysis(
196                                 $question, $questionstats[$slot]->s, $reporturl, $qubaids);
198                     } else if (!empty($questionstats[$slot]->subquestions)) {
199                         $subitemstodisplay = explode(',', $questionstats[$slot]->subquestions);
200                         foreach ($subitemstodisplay as $subitemid) {
201                             $this->output_individual_question_response_analysis(
202                                 $subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->s, $reporturl, $qubaids);
203                         }
204                     }
205                 }
206             }
208             $this->table->export_class_instance()->finish_document();
210         } else if ($slot) {
211             // Report on an individual question indexed by position.
212             if (!isset($questions[$slot])) {
213                 print_error('questiondoesnotexist', 'question');
214             }
216             $this->output_individual_question_data($quiz, $questionstats[$slot]);
217             $this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids);
219             // Back to overview link.
220             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
221                     get_string('backtoquizreport', 'quiz_statistics') . '</a>',
222                     'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
224         } else if ($qid) {
225             // Report on an individual sub-question indexed questionid.
226             if (!isset($subquestionstats[$qid])) {
227                 print_error('questiondoesnotexist', 'question');
228             }
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);
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');
239         } else if ($this->table->is_downloading()) {
240             // Downloading overview report.
241             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
242             $this->download_quiz_info_table($quizinfo);
243             $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
244             $this->table->finish_output();
246         } else {
247             // On-screen display of overview report.
248             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
249             echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
250             echo $this->everything_download_options();
251             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
252             echo $this->output_quiz_info_table($quizinfo);
253             if ($quizstats->s()) {
254                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
255                 $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
256                 $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
257             }
258         }
260         return true;
261     }
263     /**
264      * Display the statistical and introductory information about a question.
265      * Only called when not downloading.
266      * @param object                                         $quiz         the quiz settings.
267      * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
268      */
269     protected function output_individual_question_data($quiz, $questionstat) {
270         global $OUTPUT;
272         // On-screen display. Show a summary of the question's place in the quiz,
273         // and the question statistics.
274         $datumfromtable = $this->table->format_row($questionstat);
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';
282         $questioninfotable->data = array();
283         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
284         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
285                 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
286         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
287                 $datumfromtable['icon'] . '&nbsp;' .
288                 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
289                 $datumfromtable['icon']);
290         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
291                 $questionstat->positions);
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';
299         unset($datumfromtable['number']);
300         unset($datumfromtable['icon']);
301         $actions = $datumfromtable['actions'];
302         unset($datumfromtable['actions']);
303         unset($datumfromtable['name']);
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         );
315         foreach ($datumfromtable as $item => $value) {
316             $questionstatstable->data[] = array($labels[$item], $value);
317         }
319         // Display the various bits.
320         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
321         echo html_writer::table($questioninfotable);
322         echo $this->render_question_text($questionstat->question);
323         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
324         echo html_writer::table($questionstatstable);
325     }
327     /**
328      * @param object $question question data.
329      * @return string HTML of question text, ready for display.
330      */
331     protected function render_question_text($question) {
332         global $OUTPUT;
334         $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
335                 $question->contextid, 'question', 'questiontext', $question->id,
336                 $this->context->id, 'quiz_statistics');
338         return $OUTPUT->box(format_text($text, $question->questiontextformat,
339                 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
340                 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
341     }
343     /**
344      * Display the response analysis for a question.
345      * @param object           $question  the question to report on.
346      * @param int              $s
347      * @param moodle_url       $reporturl the URL to redisplay this report.
348      * @param qubaid_condition $qubaids
349      */
350     protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
351         global $OUTPUT;
353         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
354             return;
355         }
357         $qtable = new quiz_statistics_question_table($question->id);
358         $exportclass = $this->table->export_class_instance();
359         $qtable->export_class_instance($exportclass);
360         if (!$this->table->is_downloading()) {
361             // Output an appropriate title.
362             echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
364         } else {
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') {
371                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
372             }
374             // Set up the table.
375             $exportclass->start_table($questiontabletitle);
377             if ($this->table->is_downloading() == 'xhtml') {
378                 echo $this->render_question_text($question);
379             }
380         }
382         $responesanalyser = new \core_question\statistics\responses\analyser($question);
383         $responseanalysis = $responesanalyser->load_cached($qubaids);
385         $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
386         if ($this->table->is_downloading()) {
387             $exportclass->output_headers($qtable->headers);
388         }
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));
396                 }
397             }
398         }
400         $qtable->finish_output(!$this->table->is_downloading());
401     }
403     /**
404      * Output the table that lists all the questions in the quiz with their statistics.
405      * @param int $s number of attempts.
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.
408      */
409     protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
410         if (!$s) {
411             return;
412         }
414         foreach ($questionstats as $questionstat) {
415             // Output the data for these question statistics.
416             $this->table->add_data_keyed($this->table->format_row($questionstat));
417             if (count($questionstat->variantstats) > 1) {
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;
423                 }
424                 $this->add_array_of_rows_to_table($statstoadd);
425             }
427             if (empty($questionstat->subquestions)) {
428                 continue;
429             }
431             // And its sub-questions, if it has any.
432             $subitemstodisplay = explode(',', $questionstat->subquestions);
434             // We need to get all variants out of sub-questions to count them and possibly find min, median and max.
435             $displayorder = 1;
436             $subqvariants = array();
437             foreach ($subitemstodisplay as $subitemid) {
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;
443                         $subqvariants[] = $variantstat;
444                     }
445                 }
446                 $displayorder++;
447             }
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             }
471         }
473         $this->table->finish_output(!$this->table->is_downloading());
474     }
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     }
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     }
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) {
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();
519         foreach ($quizinfo as $heading => $value) {
520              $quizinfotable->data[] = array($heading, $value);
521         }
523         return html_writer::table($quizinfotable);
524     }
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;
533         // XHTML download is a special case.
534         if ($this->table->is_downloading() == 'xhtml') {
535             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
536             echo $this->output_quiz_info_table($quizinfo);
537             return;
538         }
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         }
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     }
556     /**
557      * Output the HTML needed to show the statistics graph.
558      * @param $quizid
559      * @param $currentgroup
560      * @param $whichattempts
561      */
562     protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
563         global $PAGE;
565         $output = $PAGE->get_renderer('mod_quiz');
566         $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
567                                     compact('quizid', 'currentgroup', 'whichattempts'));
568         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
569         echo $output->graph($imageurl, $graphname);
570     }
572     /**
573      * Get the quiz and question statistics, either by loading the cached results,
574      * or by recomputing them.
575      *
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
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.
581      * @param array  $groupstudents      students in this group.
582      * @param array  $questions          full question data.
583      * @return array with 4 elements:
584      *     - $quizstats The statistics for overall attempt scores.
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.
587      */
588     public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
590         if ($progress === null) {
591             $progress = new \core\progress\null();
592         }
594         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
596         $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
598         $quizcalc = new \quiz_statistics\calculator($progress);
600         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
602             $progress->start_progress('', 3);
604             // Recalculate now.
605             list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
606             $progress->progress(1);
608             $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
609                                               $qcalc->get_sum_of_mark_variance());
610             $progress->progress(2);
611             if ($quizstats->s()) {
612                 $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress);
613             }
614             $progress->progress(3);
615             $progress->end_progress();
616         } else {
617             $quizstats = $quizcalc->get_cached($qubaids);
618             list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
619         }
621         return array($quizstats, $questionstats, $subquestionstats);
622     }
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     }
641     protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats,
642                                                                             $progress = null) {
644         if ($progress === null) {
645             $progress = new \core\progress\null();
646         }
648         // Starting response analysis tasks.
649         $progress->start_progress('', count($questions) + count($subquestionstats));
651         // Starting response analysis of main questions.
652         $progress->start_progress('', count($questions), count($questions));
654         $done = array();
655         $donecount = 1;
656         foreach ($questions as $question) {
657             $progress->progress($donecount);
658             $donecount++;
659             if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
660                 continue;
661             }
662             $done[$question->id] = 1;
664             $responesstats = new \core_question\statistics\responses\analyser($question);
665             $responesstats->calculate($qubaids);
666         }
667         $progress->end_progress();
669         // Starting response analysis of sub-questions.
670         $countsubquestions = count($subquestionstats);
671         $progress->start_progress('', $countsubquestions, $countsubquestions);
672         $donecount = 1;
673         foreach ($subquestionstats as $subquestionstat) {
674             $progress->progress($donecount);
675             $donecount++;
676             if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
677                     isset($done[$subquestionstat->question->id])) {
678                 continue;
679             }
680             $done[$subquestionstat->question->id] = 1;
682             $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
683             $responesstats->calculate($qubaids);
684         }
685         // Finished sub-question tasks.
686         $progress->end_progress();
688         // Finished all response analysis tasks.
689         $progress->end_progress();
690     }
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();
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') . '"/>';
704         $output = '<form action="'. $this->table->baseurl .'" method="post">';
705         $output .= '<div class="mdl-align">';
706         $output .= '<input type="hidden" name="everything" value="1"/>';
707         $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
708         $output .= '</div></form>';
710         return $output;
711     }
713     /**
714      * Generate the snipped of HTML that says when the stats were last caculated,
715      * with a recalcuate now button.
716      * @param object $quizstats      the overall quiz statistics.
717      * @param int    $quizid         the quiz id.
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.
723      * @param moodle_url $reporturl url for this report
724      * @return string a HTML snipped saying when the stats were last computed,
725      *      or blank if that is not appropriate.
726      */
727     protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
728         global $DB, $OUTPUT;
730         if (empty($quizstats->timemodified)) {
731             return '';
732         }
734         // Find the number of attempts since the cached statistics were computed.
735         list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
736         $count = $DB->count_records_sql("
737                 SELECT COUNT(1)
738                 FROM $fromqa
739                 WHERE $whereqa
740                 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
742         if (!$count) {
743             $count = 0;
744         }
746         // Generate the output.
747         $a = new stdClass();
748         $a->lastcalculated = format_time(time() - $quizstats->timemodified);
749         $a->count = $count;
751         $recalcualteurl = new moodle_url($reporturl,
752                 array('recalculate' => 1, 'sesskey' => sesskey()));
753         $output = '';
754         $output .= $OUTPUT->box_start(
755                 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
756         $output .= get_string('lastcalculated', 'quiz_statistics', $a);
757         $output .= $OUTPUT->single_button($recalcualteurl,
758                 get_string('recalculatenow', 'quiz_statistics'));
759         $output .= $OUTPUT->box_end(true);
761         return $output;
762     }
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.
767      * @param $qubaids qubaid_condition
768      */
769     protected function clear_cached_data($qubaids) {
770         global $DB;
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()));
774     }
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     }