MDL-44054 quiz_statistics fix lang string concatenation.
[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;
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             $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, $whichtries, $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, $whichtries);
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                                                                 $whichtries);
212             // Back to overview link.
213             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
214                               get_string('backtoquizreport', 'quiz_statistics') . '</a>',
215                               'boxaligncenter generalbox boxwidthnormal mdl-align');
216         } else if ($slot) {
217             // Report on an individual question indexed by position.
218             if (!isset($questions[$slot])) {
219                 print_error('questiondoesnotexist', 'question');
220             }
222             if ($variantno === null &&
223                                 ($questionstats->for_slot($slot)->get_sub_question_ids()
224                                 || $questionstats->for_slot($slot)->get_variants())) {
225                 if (!$this->table->is_downloading()) {
226                     $number = $questionstats->for_slot($slot)->question->number;
227                     echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
228                 }
229                 $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot)));
230                 $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
231             } else {
232                 $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
233                 $this->output_individual_question_response_analysis($questions[$slot],
234                                                                     $variantno,
235                                                                     $questionstats->for_slot($slot, $variantno)->s,
236                                                                     $reporturl,
237                                                                     $qubaids,
238                                                                     $whichtries);
239             }
240             if (!$this->table->is_downloading()) {
241                 // Back to overview link.
242                 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
243                         get_string('backtoquizreport', 'quiz_statistics') . '</a>',
244                         'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
245             } else {
246                 $this->table->finish_output();
247             }
249         } else if ($this->table->is_downloading()) {
250             // Downloading overview report.
251             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
252             $this->download_quiz_info_table($quizinfo);
253             if ($quizstats->s()) {
254                 $this->output_quiz_structure_analysis_table($questionstats);
255             }
256             $this->table->finish_output();
258         } else {
259             // On-screen display of overview report.
260             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
261             echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudents, $whichattempts, $reporturl);
262             echo $this->everything_download_options();
263             $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
264             echo $this->output_quiz_info_table($quizinfo);
265             if ($quizstats->s()) {
266                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
267                 $this->output_quiz_structure_analysis_table($questionstats);
268                 $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
269             }
270         }
272         return true;
273     }
275     /**
276      * Display the statistical and introductory information about a question.
277      * Only called when not downloading.
278      *
279      * @param object                                         $quiz         the quiz settings.
280      * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
281      */
282     protected function output_individual_question_data($quiz, $questionstat) {
283         global $OUTPUT;
285         // On-screen display. Show a summary of the question's place in the quiz,
286         // and the question statistics.
287         $datumfromtable = $this->table->format_row($questionstat);
289         // Set up the question info table.
290         $questioninfotable = new html_table();
291         $questioninfotable->align = array('center', 'center');
292         $questioninfotable->width = '60%';
293         $questioninfotable->attributes['class'] = 'generaltable titlesleft';
295         $questioninfotable->data = array();
296         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
297         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
298                 $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
300         if ($questionstat->variant !== null) {
301             $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
303         }
304         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
305                 $datumfromtable['icon'] . '&nbsp;' .
306                 question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
307                 $datumfromtable['icon']);
308         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
309                 $questionstat->positions);
311         // Set up the question statistics table.
312         $questionstatstable = new html_table();
313         $questionstatstable->align = array('center', 'center');
314         $questionstatstable->width = '60%';
315         $questionstatstable->attributes['class'] = 'generaltable titlesleft';
317         unset($datumfromtable['number']);
318         unset($datumfromtable['icon']);
319         $actions = $datumfromtable['actions'];
320         unset($datumfromtable['actions']);
321         unset($datumfromtable['name']);
322         $labels = array(
323             's' => get_string('attempts', 'quiz_statistics'),
324             'facility' => get_string('facility', 'quiz_statistics'),
325             'sd' => get_string('standarddeviationq', 'quiz_statistics'),
326             'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
327             'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
328             'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
329             'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
330             'discriminative_efficiency' =>
331                                 get_string('discriminative_efficiency', 'quiz_statistics')
332         );
333         foreach ($datumfromtable as $item => $value) {
334             $questionstatstable->data[] = array($labels[$item], $value);
335         }
337         // Display the various bits.
338         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
339         echo html_writer::table($questioninfotable);
340         echo $this->render_question_text($questionstat->question);
341         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
342         echo html_writer::table($questionstatstable);
343     }
345     /**
346      * Output question text in a box with urls appropriate for a preview of the question.
347      *
348      * @param object $question question data.
349      * @return string HTML of question text, ready for display.
350      */
351     protected function render_question_text($question) {
352         global $OUTPUT;
354         $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
355                 $question->contextid, 'question', 'questiontext', $question->id,
356                 $this->context->id, 'quiz_statistics');
358         return $OUTPUT->box(format_text($text, $question->questiontextformat,
359                 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
360                 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
361     }
363     /**
364      * Display the response analysis for a question.
365      *
366      * @param object           $question  the question to report on.
367      * @param int|null         $variantno the variant
368      * @param int              $s
369      * @param moodle_url       $reporturl the URL to redisplay this report.
370      * @param qubaid_condition $qubaids
371      * @param string           $whichtries
372      */
373     protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
374                                                                     $whichtries = question_attempt::LAST_TRY) {
375         global $OUTPUT;
377         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
378             return;
379         }
381         $qtable = new quiz_statistics_question_table($question->id);
382         $exportclass = $this->table->export_class_instance();
383         $qtable->export_class_instance($exportclass);
384         if (!$this->table->is_downloading()) {
385             // Output an appropriate title.
386             echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
388         } else {
389             // Work out an appropriate title.
390             $a = clone($question);
391             $a->variant = $variantno;
393             if (!empty($question->number) && !is_null($variantno)) {
394                 $questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a);
395             } else if (!empty($question->number)) {
396                 $questiontabletitle = get_string('analysisno', 'quiz_statistics', $a);
397             } else if (!is_null($variantno)) {
398                 $questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a);
399             } else {
400                 $questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a);
401             }
403             if ($this->table->is_downloading() == 'xhtml') {
404                 $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
405             }
407             // Set up the table.
408             $exportclass->start_table($questiontabletitle);
410             if ($this->table->is_downloading() == 'xhtml') {
411                 echo $this->render_question_text($question);
412             }
413         }
415         $responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
416         $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
418         $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
419         if ($this->table->is_downloading()) {
420             $exportclass->output_headers($qtable->headers);
421         }
423         // Where no variant no is specified the variant no is actually one.
424         if ($variantno === null) {
425             $variantno = 1;
426         }
427         foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
428             $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
429             foreach ($subpart->get_response_class_ids() as $responseclassid) {
430                 $responseclass = $subpart->get_response_class($responseclassid);
431                 $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
432                 foreach ($tabledata as $row) {
433                     $qtable->add_data_keyed($qtable->format_row($row));
434                 }
435             }
436         }
438         $qtable->finish_output(!$this->table->is_downloading());
439     }
441     /**
442      * Output the table that lists all the questions in the quiz with their statistics.
443      *
444      * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
445      *                                                                                               the quiz including subqs and
446      *                                                                                               variants.
447      */
448     protected function output_quiz_structure_analysis_table($questionstats) {
449         $tooutput = array();
450         $limitvariants = !$this->table->is_downloading();
451         foreach ($questionstats->get_all_slots() as $slot) {
452             // Output the data for these question statistics.
453             $tooutput = array_merge($tooutput, $questionstats->structure_analysis_for_one_slot($slot, $limitvariants));
454         }
455         $this->table->format_and_add_array_of_rows($tooutput);
456     }
458     /**
459      * Return HTML for table of overall quiz statistics.
460      *
461      * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
462      * @return string the HTML.
463      */
464     protected function output_quiz_info_table($quizinfo) {
466         $quizinfotable = new html_table();
467         $quizinfotable->align = array('center', 'center');
468         $quizinfotable->width = '60%';
469         $quizinfotable->attributes['class'] = 'generaltable titlesleft';
470         $quizinfotable->data = array();
472         foreach ($quizinfo as $heading => $value) {
473              $quizinfotable->data[] = array($heading, $value);
474         }
476         return html_writer::table($quizinfotable);
477     }
479     /**
480      * Download the table of overall quiz statistics.
481      *
482      * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
483      */
484     protected function download_quiz_info_table($quizinfo) {
485         global $OUTPUT;
487         // XHTML download is a special case.
488         if ($this->table->is_downloading() == 'xhtml') {
489             echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
490             echo $this->output_quiz_info_table($quizinfo);
491             return;
492         }
494         // Reformat the data ready for output.
495         $headers = array();
496         $row = array();
497         foreach ($quizinfo as $heading => $value) {
498             $headers[] = $heading;
499             $row[] = $value;
500         }
502         // Do the output.
503         $exportclass = $this->table->export_class_instance();
504         $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
505         $exportclass->output_headers($headers);
506         $exportclass->add_data($row);
507         $exportclass->finish_table();
508     }
510     /**
511      * Output the HTML needed to show the statistics graph.
512      *
513      * @param $quizid
514      * @param $currentgroup
515      * @param $whichattempts
516      */
517     protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
518         global $PAGE;
520         $output = $PAGE->get_renderer('mod_quiz');
521         $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
522                                     compact('quizid', 'currentgroup', 'whichattempts'));
523         $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
524         echo $output->graph($imageurl, $graphname);
525     }
527     /**
528      * Get the quiz and question statistics, either by loading the cached results,
529      * or by recomputing them.
530      *
531      * @param object $quiz               the quiz settings.
532      * @param string $whichattempts      which attempts to use, represented internally as one of the constants as used in
533      *                                   $quiz->grademethod ie.
534      *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
535      *                                   we calculate stats based on which attempts would affect the grade for each student.
536      * @param string $whichtries         which tries to analyse for response analysis. Will be one of
537      *                                   question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
538      * @param array  $groupstudents      students in this group.
539      * @param array  $questions          full question data.
540      * @param \core\progress\base|null   $progress
541      * @return array with 2 elements:    - $quizstats The statistics for overall attempt scores.
542      *                                   - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
543      */
544     public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress = null) {
546         if ($progress === null) {
547             $progress = new \core\progress\null();
548         }
550         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
552         $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
554         $quizcalc = new \quiz_statistics\calculator($progress);
556         $progress->start_progress('', 3);
557         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
559             // Recalculate now.
560             $questionstats = $qcalc->calculate($qubaids);
561             $progress->progress(1);
563             $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
564                                               $qcalc->get_sum_of_mark_variance());
565             $progress->progress(2);
566         } else {
567             $quizstats = $quizcalc->get_cached($qubaids);
568             $progress->progress(1);
569             $questionstats = $qcalc->get_cached($qubaids);
570             $progress->progress(2);
571         }
573         if ($quizstats->s()) {
574             $subquestions = $questionstats->get_sub_questions();
575             $this->analyse_responses_for_all_questions_and_subquestions($questions,
576                                                                         $subquestions,
577                                                                         $qubaids,
578                                                                         $whichtries,
579                                                                         $progress);
580         }
581         $progress->progress(3);
582         $progress->end_progress();
584         return array($quizstats, $questionstats);
585     }
587     /**
588      * Appropriate instance depending if we want html output for the user or not.
589      *
590      * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
591      */
592     protected function get_progress_trace_instance() {
593         if ($this->progress === null) {
594             if (!$this->table->is_downloading()) {
595                 $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
596                 $this->progress->set_display_names();
597             } else {
598                 $this->progress = new \core\progress\null();
599             }
600         }
601         return $this->progress;
602     }
604     /**
605      * Analyse responses for all questions and sub questions in this quiz.
606      *
607      * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
608      * @param object[] $subquestions full question objects.
609      * @param qubaid_condition $qubaids the question usages whose responses to analyse.
610      * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
611      * @param null|\core\progress\base $progress Used to indicate progress of task.
612      */
613     protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
614                                                                             $whichtries, $progress = null) {
615         if ($progress === null) {
616             $progress = new \core\progress\null();
617         }
619         // Starting response analysis tasks.
620         $progress->start_progress('', count($questions) + count($subquestions));
622         $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
624         $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
626         // Finished all response analysis tasks.
627         $progress->end_progress();
628     }
630     /**
631      * Analyse responses for an array of questions or sub questions.
632      *
633      * @param object[] $questions  as returned by self::load_and_initialise_questions_for_calculations.
634      * @param qubaid_condition $qubaids the question usages whose responses to analyse.
635      * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
636      * @param null|\core\progress\base $progress Used to indicate progress of task.
637      * @param int[] $done array keys are ids of questions that have been analysed before calling method.
638      * @return array array keys are ids of questions that were analysed after this method call.
639      */
640     protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
641         $countquestions = count($questions);
642         if (!$countquestions) {
643             return array();
644         }
645         if ($progress === null) {
646             $progress = new \core\progress\null();
647         }
648         $progress->start_progress('', $countquestions, $countquestions);
649         foreach ($questions as $question) {
650             $progress->increment_progress();
651             if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses()  && !isset($done[$question->id])) {
652                 $responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
653                 if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
654                     $responesstats->calculate($qubaids, $whichtries);
655                 }
656             }
657             $done[$question->id] = 1;
658         }
659         $progress->end_progress();
660         return $done;
661     }
663     /**
664      * Return a little form for the user to request to download the full report, including quiz stats and response analysis for
665      * all questions and sub-questions.
666      *
667      * @return string HTML.
668      */
669     protected function everything_download_options() {
670         $downloadoptions = $this->table->get_download_menu();
672         $downloadelements = new stdClass();
673         $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
674                 $this->table->defaultdownloadformat, false);
675         $downloadelements->downloadbutton = '<input type="submit" value="' .
676                 get_string('download') . '"/>';
678         $output = '<form action="'. $this->table->baseurl .'" method="post">';
679         $output .= '<div class="mdl-align">';
680         $output .= '<input type="hidden" name="everything" value="1"/>';
681         $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
682         $output .= '</div></form>';
684         return $output;
685     }
687     /**
688      * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
689      *
690      * @param int    $lastcachetime  the time the stats were last cached.
691      * @param int    $quizid         the quiz id.
692      * @param array  $groupstudents  ids of students in the group or empty array if groups not used.
693      * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
694      *                                   $quiz->grademethod ie.
695      *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
696      *                                   we calculate stats based on which attempts would affect the grade for each student.
697      * @param moodle_url $reporturl url for this report
698      * @return string HTML.
699      */
700     protected function output_caching_info($lastcachetime, $quizid, $groupstudents, $whichattempts, $reporturl) {
701         global $DB, $OUTPUT;
703         if (empty($lastcachetime)) {
704             return '';
705         }
707         // Find the number of attempts since the cached statistics were computed.
708         list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
709         $count = $DB->count_records_sql("
710                 SELECT COUNT(1)
711                 FROM $fromqa
712                 WHERE $whereqa
713                 AND quiza.timefinish > {$lastcachetime}", $qaparams);
715         if (!$count) {
716             $count = 0;
717         }
719         // Generate the output.
720         $a = new stdClass();
721         $a->lastcalculated = format_time(time() - $lastcachetime);
722         $a->count = $count;
724         $recalcualteurl = new moodle_url($reporturl,
725                 array('recalculate' => 1, 'sesskey' => sesskey()));
726         $output = '';
727         $output .= $OUTPUT->box_start(
728                 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
729         $output .= get_string('lastcalculated', 'quiz_statistics', $a);
730         $output .= $OUTPUT->single_button($recalcualteurl,
731                 get_string('recalculatenow', 'quiz_statistics'));
732         $output .= $OUTPUT->box_end(true);
734         return $output;
735     }
737     /**
738      * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
739      * is displayed.
740      *
741      * @param $qubaids qubaid_condition
742      */
743     protected function clear_cached_data($qubaids) {
744         global $DB;
745         $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
746         $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
747         $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
748     }
750     /**
751      * Load the questions in this quiz and add some properties to the objects needed in the reports.
752      *
753      * @param object $quiz the quiz.
754      * @return array of questions for this quiz.
755      */
756     public function load_and_initialise_questions_for_calculations($quiz) {
757         // Load the questions.
758         $questions = quiz_report_get_significant_questions($quiz);
759         $questionids = array();
760         foreach ($questions as $question) {
761             $questionids[] = $question->id;
762         }
763         $fullquestions = question_load_questions($questionids);
764         foreach ($questions as $qno => $question) {
765             $q = $fullquestions[$question->id];
766             $q->maxmark = $question->maxmark;
767             $q->slot = $qno;
768             $q->number = $question->number;
769             $questions[$qno] = $q;
770         }
771         return $questions;
772     }
774     /**
775      * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
776      *
777      * @param $qubaids
778      * @param $questions
779      * @param $questionstats
780      * @param $reporturl
781      * @param $whichtries string
782      */
783     protected function output_all_question_response_analysis($qubaids,
784                                                              $questions,
785                                                              $questionstats,
786                                                              $reporturl,
787                                                              $whichtries = question_attempt::LAST_TRY) {
788         foreach ($questions as $slot => $question) {
789             if (question_bank::get_qtype(
790                 $question->qtype, false)->can_analyse_responses()
791             ) {
792                 if ($questionstats->for_slot($slot)->get_variants()) {
793                     foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
794                         $this->output_individual_question_response_analysis($question,
795                                                                             $variantno,
796                                                                             $questionstats->for_slot($slot, $variantno)->s,
797                                                                             $reporturl,
798                                                                             $qubaids,
799                                                                             $whichtries);
800                     }
801                 } else {
802                     $this->output_individual_question_response_analysis($question,
803                                                                         null,
804                                                                         $questionstats->for_slot($slot)->s,
805                                                                         $reporturl,
806                                                                         $qubaids,
807                                                                         $whichtries);
808                 }
809             } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
810                 foreach ($subqids as $subqid) {
811                     if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
812                         foreach ($variants as $variantno) {
813                             $this->output_individual_question_response_analysis(
814                                 $questionstats->for_subq($subqid, $variantno)->question,
815                                 $variantno,
816                                 $questionstats->for_subq($subqid, $variantno)->s,
817                                 $reporturl,
818                                 $qubaids,
819                                 $whichtries);
820                         }
821                     } else {
822                         $this->output_individual_question_response_analysis(
823                             $questionstats->for_subq($subqid)->question,
824                             null,
825                             $questionstats->for_subq($subqid)->s,
826                             $reporturl,
827                             $qubaids,
828                             $whichtries);
830                     }
831                 }
832             }
833         }
834     }