Rev 1 | AutorÃa | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Quiz statistics report class.** @package quiz_statistics* @copyright 2014 Open University* @author James Pratt <me@jamiep.org>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();use core_question\statistics\responses\analyser;use mod_quiz\local\reports\report_base;use core_question\statistics\questions\all_calculated_for_qubaid_condition;require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');/*** The quiz statistics report provides summary information about each question in* a quiz, compared to the whole quiz. It also provides a drill-down to more* detailed information about each question.** @copyright 2008 Jamie Pratt* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class quiz_statistics_report extends report_base {/** @var context_module context of this quiz.*/protected $context;/** @var quiz_statistics_table instance of table class used for main questions stats table. */protected $table;/** @var \core\progress\base|null $progress Handles progress reporting or not. */protected $progress = null;/*** Display the report.*/public function display($quiz, $cm, $course) {global $OUTPUT, $DB;raise_memory_limit(MEMORY_HUGE);$this->context = context_module::instance($cm->id);if (!quiz_has_questions($quiz->id)) {$this->print_header_and_tabs($cm, $course, $quiz, 'statistics');echo quiz_no_questions_message($quiz, $cm, $this->context);return true;}// Work out the display options.$download = optional_param('download', '', PARAM_ALPHA);$everything = optional_param('everything', 0, PARAM_BOOL);$recalculate = optional_param('recalculate', 0, PARAM_BOOL);// A qid paramter indicates we should display the detailed analysis of a sub question.$qid = optional_param('qid', 0, PARAM_INT);$slot = optional_param('slot', 0, PARAM_INT);$variantno = optional_param('variant', null, PARAM_INT);$whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);$whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);$pageoptions = [];$pageoptions['id'] = $cm->id;$pageoptions['mode'] = 'statistics';$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);$mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));$mform->set_data(['whichattempts' => $whichattempts, 'whichtries' => $whichtries]);if ($whichattempts != $quiz->grademethod) {$reporturl->param('whichattempts', $whichattempts);}if ($whichtries != question_attempt::LAST_TRY) {$reporturl->param('whichtries', $whichtries);}// Find out current groups mode.$currentgroup = $this->get_current_group($cm, $course, $this->context);$nostudentsingroup = false; // True if a group is selected and there is no one in it.if (empty($currentgroup)) {$currentgroup = 0;$groupstudentsjoins = new \core\dml\sql_join();} else if ($currentgroup == self::NO_GROUPS_ALLOWED) {$groupstudentsjoins = new \core\dml\sql_join();$nostudentsingroup = true;} else {// All users who can attempt quizzes and who are in the currently selected group.$groupstudentsjoins = get_enrolled_with_capabilities_join($this->context, '',['mod/quiz:reviewmyattempts', 'mod/quiz:attempt'], $currentgroup);if (!empty($groupstudentsjoins->joins)) {$sql = "SELECT DISTINCT u.idFROM {user} u{$groupstudentsjoins->joins}WHERE {$groupstudentsjoins->wheres}";if (!$DB->record_exists_sql($sql, $groupstudentsjoins->params)) {$nostudentsingroup = true;}}}$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);// If recalculate was requested, handle that.if ($recalculate && confirm_sesskey()) {$this->clear_cached_data($qubaids);redirect($reporturl);}// Set up the main table.$this->table = new quiz_statistics_table();if ($everything) {$report = get_string('completestatsfilename', 'quiz_statistics');} else {$report = get_string('questionstatsfilename', 'quiz_statistics');}$courseshortname = format_string($course->shortname, true,['context' => context_course::instance($course->id)]);$filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);$this->table->is_downloading($download, $filename,get_string('quizstructureanalysis', 'quiz_statistics'));$questions = $this->load_and_initialise_questions_for_calculations($quiz);// Print the page header stuff (if not downloading.if (!$this->table->is_downloading()) {$this->print_header_and_tabs($cm, $course, $quiz, 'statistics');}if (!$nostudentsingroup) {// Get the data to be displayed.$progress = $this->get_progress_trace_instance();list($quizstats, $questionstats) =$this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudentsjoins, $questions, $progress);if (is_null($quizstats)) {echo $OUTPUT->notification(get_string('nostats', 'quiz_statistics'), 'error');return true;}} else {// Or create empty stats containers.$quizstats = new \quiz_statistics\calculated($whichattempts);$questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();}// Set up the table.$this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());// Print the rest of the page header stuff (if not downloading.if (!$this->table->is_downloading()) {if (groups_get_activity_groupmode($cm)) {groups_print_activity_menu($cm, $reporturl->out());if ($currentgroup && $nostudentsingroup) {$OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));}}if (!$this->table->is_downloading() && $quizstats->s() == 0) {echo $OUTPUT->notification(get_string('nogradedattempts', 'quiz_statistics'));}foreach ($questionstats->any_error_messages() as $errormessage) {echo $OUTPUT->notification($errormessage);}// Print display options form.$mform->display();}if ($everything) { // Implies is downloading.// Overall report, then the analysis of each question.$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);$this->download_quiz_info_table($quizinfo);if ($quizstats->s()) {$this->output_quiz_structure_analysis_table($questionstats);if ($this->table->is_downloading() == 'html' && $quizstats->s() != 0) {$this->output_statistics_graph($quiz, $qubaids);}$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);}$this->table->export_class_instance()->finish_document();} else if ($qid) {// Report on an individual sub-question indexed questionid.if (!$questionstats->has_subq($qid, $variantno)) {throw new \moodle_exception('questiondoesnotexist', 'question');}$this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));$this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,$variantno,$questionstats->for_subq($qid, $variantno)->s,$reporturl,$qubaids,$whichtries);// Back to overview link.echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .get_string('backtoquizreport', 'quiz_statistics') . '</a>','boxaligncenter generalbox boxwidthnormal mdl-align');} else if ($slot) {// Report on an individual question indexed by position.if (!isset($questions[$slot])) {throw new \moodle_exception('questiondoesnotexist', 'question');}if ($variantno === null &&($questionstats->for_slot($slot)->get_sub_question_ids()|| $questionstats->for_slot($slot)->get_variants())) {if (!$this->table->is_downloading()) {$number = $questionstats->for_slot($slot)->question->number;echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);}$this->table->define_baseurl(new moodle_url($reporturl, ['slot' => $slot]));$this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));} else {$this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));$this->output_individual_question_response_analysis($questions[$slot],$variantno,$questionstats->for_slot($slot, $variantno)->s,$reporturl,$qubaids,$whichtries);}if (!$this->table->is_downloading()) {// Back to overview link.echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .get_string('backtoquizreport', 'quiz_statistics') . '</a>','backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');} else {$this->table->finish_output();}} else if ($this->table->is_downloading()) {// Downloading overview report.$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);$this->download_quiz_info_table($quizinfo);if ($quizstats->s()) {$this->output_quiz_structure_analysis_table($questionstats);}$this->table->export_class_instance()->finish_document();} else {// On-screen display of overview report.echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudentsjoins, $whichattempts, $reporturl);echo $this->everything_download_options($reporturl);$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);echo $this->output_quiz_info_table($quizinfo);if ($quizstats->s()) {echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);$this->output_quiz_structure_analysis_table($questionstats);$this->output_statistics_graph($quiz, $qubaids);}}return true;}/*** Display the statistical and introductory information about a question.* Only called when not downloading.** @param stdClass $quiz the quiz settings.* @param \core_question\statistics\questions\calculated $questionstat the question to report on.*/protected function output_individual_question_data($quiz, $questionstat) {global $OUTPUT;// On-screen display. Show a summary of the question's place in the quiz,// and the question statistics.$datumfromtable = $this->table->format_row($questionstat);// Set up the question info table.$questioninfotable = new html_table();$questioninfotable->align = ['center', 'center'];$questioninfotable->width = '60%';$questioninfotable->attributes['class'] = 'generaltable titlesleft';$questioninfotable->data = [];$questioninfotable->data[] = [get_string('modulename', 'quiz'), $quiz->name];$questioninfotable->data[] = [get_string('questionname', 'quiz_statistics'),$questionstat->question->name.' '.$datumfromtable['actions']];if ($questionstat->variant !== null) {$questioninfotable->data[] = [get_string('variant', 'quiz_statistics'), $questionstat->variant];}$questioninfotable->data[] = [get_string('questiontype', 'quiz_statistics'),$datumfromtable['icon'] . ' ' .question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . ' ' .$datumfromtable['icon']];$questioninfotable->data[] = [get_string('positions', 'quiz_statistics'),$questionstat->positions];// Set up the question statistics table.$questionstatstable = new html_table();$questionstatstable->align = ['center', 'center'];$questionstatstable->width = '60%';$questionstatstable->attributes['class'] = 'generaltable titlesleft';unset($datumfromtable['number']);unset($datumfromtable['icon']);$actions = $datumfromtable['actions'];unset($datumfromtable['actions']);unset($datumfromtable['name']);$labels = ['s' => get_string('attempts', 'quiz_statistics'),'facility' => get_string('facility', 'quiz_statistics'),'sd' => get_string('standarddeviationq', 'quiz_statistics'),'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),'intended_weight' => get_string('intended_weight', 'quiz_statistics'),'effective_weight' => get_string('effective_weight', 'quiz_statistics'),'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),'discriminative_efficiency' =>get_string('discriminative_efficiency', 'quiz_statistics')];foreach ($datumfromtable as $item => $value) {$questionstatstable->data[] = [$labels[$item], $value];}// Display the various bits.echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);echo html_writer::table($questioninfotable);echo $this->render_question_text($questionstat->question);echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);echo html_writer::table($questionstatstable);}/*** Output question text in a box with urls appropriate for a preview of the question.** @param stdClass $question question data.* @return string HTML of question text, ready for display.*/protected function render_question_text($question) {global $OUTPUT;$text = question_rewrite_question_preview_urls($question->questiontext, $question->id,$question->contextid, 'question', 'questiontext', $question->id,$this->context->id, 'quiz_statistics');return $OUTPUT->box(format_text($text, $question->questiontextformat,['noclean' => true, 'para' => false, 'overflowdiv' => true]),'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');}/*** Display the response analysis for a question.** @param stdClass $question the question to report on.* @param int|null $variantno the variant* @param int $s* @param moodle_url $reporturl the URL to redisplay this report.* @param qubaid_condition $qubaids* @param string $whichtries*/protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,$whichtries = question_attempt::LAST_TRY) {global $OUTPUT;if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {return;}$qtable = new quiz_statistics_question_table($question->id);$exportclass = $this->table->export_class_instance();$qtable->export_class_instance($exportclass);if (!$this->table->is_downloading()) {// Output an appropriate title.echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);} else {// Work out an appropriate title.$a = clone($question);$a->variant = $variantno;if (!empty($question->number) && !is_null($variantno)) {$questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a);} else if (!empty($question->number)) {$questiontabletitle = get_string('analysisno', 'quiz_statistics', $a);} else if (!is_null($variantno)) {$questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a);} else {$questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a);}if ($this->table->is_downloading() == 'html') {$questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);}// Set up the table.$exportclass->start_table($questiontabletitle);if ($this->table->is_downloading() == 'html') {echo $this->render_question_text($question);}}$responesanalyser = new analyser($question, $whichtries);$responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);$qtable->question_setup($reporturl, $question, $s, $responseanalysis);if ($this->table->is_downloading()) {$exportclass->output_headers($qtable->headers);}// Where no variant no is specified the variant no is actually one.if ($variantno === null) {$variantno = 1;}foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {$subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);foreach ($subpart->get_response_class_ids() as $responseclassid) {$responseclass = $subpart->get_response_class($responseclassid);$tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);foreach ($tabledata as $row) {$qtable->add_data_keyed($qtable->format_row($row));}}}$qtable->finish_output(!$this->table->is_downloading());}/*** Output the table that lists all the questions in the quiz with their statistics.** @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in* the quiz including subqs and* variants.*/protected function output_quiz_structure_analysis_table($questionstats) {$limitvariants = !$this->table->is_downloading();foreach ($questionstats->get_all_slots() as $slot) {// Output the data for these question statistics.$structureanalysis = $questionstats->structure_analysis_for_one_slot($slot, $limitvariants);if (is_null($structureanalysis)) {$this->table->add_separator();} else {foreach ($structureanalysis as $row) {$bgcssclass = '';// The only way to identify in this point of the report if a row is a summary row// is checking if it's a instance of calculated_question_summary class.if ($row instanceof \core_question\statistics\questions\calculated_question_summary) {// Apply a custom css class to summary row to remove border and reduce paddings.$bgcssclass = 'quiz_statistics-summaryrow';// For question that contain a summary row, we add a "hidden" row in between so the report// display both rows with same background color.$this->table->add_data_keyed([], 'd-none hidden');}$this->table->add_data_keyed($this->table->format_row($row), $bgcssclass);}}}$this->table->finish_output(!$this->table->is_downloading());}/*** Return HTML for table of overall quiz statistics.** @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.* @return string the HTML.*/protected function output_quiz_info_table($quizinfo) {$quizinfotable = new html_table();$quizinfotable->align = ['center', 'center'];$quizinfotable->width = '60%';$quizinfotable->attributes['class'] = 'generaltable titlesleft';$quizinfotable->data = [];foreach ($quizinfo as $heading => $value) {$quizinfotable->data[] = [$heading, $value];}return html_writer::table($quizinfotable);}/*** Download the table of overall quiz statistics.** @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.*/protected function download_quiz_info_table($quizinfo) {global $OUTPUT;// HTML download is a special case.if ($this->table->is_downloading() == 'html') {echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);echo $this->output_quiz_info_table($quizinfo);return;}// Reformat the data ready for output.$headers = [];$row = [];foreach ($quizinfo as $heading => $value) {$headers[] = $heading;$row[] = $value;}// Do the output.$exportclass = $this->table->export_class_instance();$exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));$exportclass->output_headers($headers);$exportclass->add_data($row);$exportclass->finish_table();}/*** Output the HTML needed to show the statistics graph.** @param stdClass $quiz the quiz.* @param qubaid_condition $qubaids the question usages whose responses to analyse.*/protected function output_statistics_graph($quiz, $qubaids) {global $DB, $PAGE;// Load the rest of the required data.$questions = quiz_report_get_significant_questions($quiz);// Only load main question not sub questions.$questionstatistics = $DB->get_records_select('question_statistics','hashcode = ? AND slot IS NOT NULL AND variant IS NULL',[$qubaids->get_hash_code()]);// Configure what to display.$fieldstoplot = ['facility' => get_string('facility', 'quiz_statistics'),'discriminativeefficiency' => get_string('discriminative_efficiency', 'quiz_statistics')];$fieldstoplotfactor = ['facility' => 100, 'discriminativeefficiency' => 1];// Prepare the arrays to hold the data.$xdata = [];foreach (array_keys($fieldstoplot) as $fieldtoplot) {$ydata[$fieldtoplot] = [];}// Fill in the data for each question.foreach ($questionstatistics as $questionstatistic) {$number = $questions[$questionstatistic->slot]->number;$xdata[$number] = $number;foreach ($fieldstoplot as $fieldtoplot => $notused) {$value = $questionstatistic->$fieldtoplot;if (is_null($value)) {$value = 0;}$value *= $fieldstoplotfactor[$fieldtoplot];$ydata[$fieldtoplot][$number] = number_format($value, 2);}}// Create the chart.sort($xdata);$chart = new \core\chart_bar();$chart->get_xaxis(0, true)->set_label(get_string('position', 'quiz_statistics'));$chart->set_labels(array_values($xdata));foreach ($fieldstoplot as $fieldtoplot => $notused) {ksort($ydata[$fieldtoplot]);$series = new \core\chart_series($fieldstoplot[$fieldtoplot], array_values($ydata[$fieldtoplot]));$chart->add_series($series);}// Find max.$max = 0;foreach ($fieldstoplot as $fieldtoplot => $notused) {$max = max($max, max($ydata[$fieldtoplot]));}// Set Y properties.$yaxis = $chart->get_yaxis(0, true);$yaxis->set_stepsize(10);$yaxis->set_label('%');$output = $PAGE->get_renderer('mod_quiz');$graphname = get_string('statisticsreportgraph', 'quiz_statistics');echo $output->chart($chart, $graphname);}/*** Get the quiz and question statistics, either by loading the cached results,* or by recomputing them.** @param stdClass $quiz the quiz settings.* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in* $quiz->grademethod ie.* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST* we calculate stats based on which attempts would affect the grade for each student.* @param string $whichtries which tries to analyse for response analysis. Will be one of* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.* @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.* @param array $questions full question data.* @param \core\progress\base|null $progress* @param bool $calculateifrequired if true (the default) the stats will be calculated if not already stored.* If false, [null, null] will be returned if the stats are not already available.* @param bool $performanalysis if true (the default) and there are calculated stats, analysis will be performed* for each question.* @return array with 2 elements: - $quizstats The statistics for overall attempt scores.* - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition* Both may be null, if $calculateifrequired is false.*/public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, \core\dml\sql_join $groupstudentsjoins,$questions, $progress = null, bool $calculateifrequired = true, bool $performanalysis = true) {if ($progress === null) {$progress = new \core\progress\none();}$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudentsjoins, $whichattempts);$qcalc = new \core_question\statistics\questions\calculator($questions, $progress);$quizcalc = new \quiz_statistics\calculator($progress);$progress->start_progress('', 4);// Get a lock on this set of qubaids before performing calculations. This prevents the same calculation running// concurrently and causing database deadlocks. We use a long timeout here as a big quiz with lots of attempts may// take a long time to process.$lockfactory = \core\lock\lock_config::get_lock_factory('quiz_statistics_get_stats');$lock = $lockfactory->get_lock($qubaids->get_hash_code(), 0);if (!$lock) {if (!$calculateifrequired) {// We're not going to do the calculation in this request anyway, so just give up here.$progress->progress(4);$progress->end_progress();return [null, null];}$locktimeout = get_config('quiz_statistics', 'getstatslocktimeout');$lock = \core\lock\lock_utils::wait_for_lock_with_progress($lockfactory,$qubaids->get_hash_code(),$progress,$locktimeout,get_string('getstatslockprogress', 'quiz_statistics'),);if (!$lock) {// Lock attempt timed out.$progress->progress(4);$progress->end_progress();debugging('Could not get lock on ' .$qubaids->get_hash_code() . ' (Quiz ID ' . $quiz->id . ') after ' .$locktimeout . ' seconds');return [null, null];}}try {if ($quizcalc->get_last_calculated_time($qubaids) === false) {if (!$calculateifrequired) {$progress->progress(4);$progress->end_progress();$lock->release();return [null, null];}// Recalculate now.$questionstats = $qcalc->calculate($qubaids);$progress->progress(2);$quizstats = $quizcalc->calculate($quiz->id,$whichattempts,$groupstudentsjoins,count($questions),$qcalc->get_sum_of_mark_variance());$progress->progress(3);} else {$quizstats = $quizcalc->get_cached($qubaids);$progress->progress(2);$questionstats = $qcalc->get_cached($qubaids);$progress->progress(3);}if ($quizstats->s() && $performanalysis) {$subquestions = $questionstats->get_sub_questions();$this->analyse_responses_for_all_questions_and_subquestions($questions,$subquestions,$qubaids,$whichtries,$progress);}$progress->progress(4);$progress->end_progress();} finally {$lock->release();}return [$quizstats, $questionstats];}/*** Appropriate instance depending if we want html output for the user or not.** @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.*/protected function get_progress_trace_instance() {if ($this->progress === null) {if (!$this->table->is_downloading()) {$this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));$this->progress->set_display_names();} else {$this->progress = new \core\progress\none();}}return $this->progress;}/*** Analyse responses for all questions and sub questions in this quiz.** @param stdClass[] $questions as returned by self::load_and_initialise_questions_for_calculations* @param stdClass[] $subquestions full question objects.* @param qubaid_condition $qubaids the question usages whose responses to analyse.* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.* @param null|\core\progress\base $progress Used to indicate progress of task.*/protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,$whichtries, $progress = null) {if ($progress === null) {$progress = new \core\progress\none();}// Starting response analysis tasks.$progress->start_progress('', count($questions) + count($subquestions));$done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);$this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);// Finished all response analysis tasks.$progress->end_progress();}/*** Analyse responses for an array of questions or sub questions.** @param stdClass[] $questions as returned by self::load_and_initialise_questions_for_calculations.* @param qubaid_condition $qubaids the question usages whose responses to analyse.* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.* @param null|\core\progress\base $progress Used to indicate progress of task.* @param int[] $done array keys are ids of questions that have been analysed before calling method.* @return array array keys are ids of questions that were analysed after this method call.*/protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = []) {$countquestions = count($questions);if (!$countquestions) {return [];}if ($progress === null) {$progress = new \core\progress\none();}$progress->start_progress('', $countquestions, $countquestions);foreach ($questions as $question) {$progress->increment_progress();if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) {$responesstats = new analyser($question, $whichtries);$responesstats->calculate($qubaids, $whichtries);}$done[$question->id] = 1;}$progress->end_progress();return $done;}/*** Return a little form for the user to request to download the full report, including quiz stats and response analysis for* all questions and sub-questions.** @param moodle_url $reporturl the base URL of the report.* @return string HTML.*/protected function everything_download_options(moodle_url $reporturl) {global $OUTPUT;return $OUTPUT->download_dataformat_selector(get_string('downloadeverything', 'quiz_statistics'),$reporturl->out_omit_querystring(), 'download', $reporturl->params() + ['everything' => 1]);}/*** Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.** @param int $lastcachetime the time the stats were last cached.* @param int $quizid the quiz id.* @param \core\dml\sql_join $groupstudentsjoins (joins, wheres, params) for students in the group* or empty array if groups not used.* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in* $quiz->grademethod ie.* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST* we calculate stats based on which attempts would affect the grade for each student.* @param moodle_url $reporturl url for this report* @return string HTML.*/protected function output_caching_info($lastcachetime, $quizid, $groupstudentsjoins, $whichattempts, $reporturl) {global $DB, $OUTPUT;if (empty($lastcachetime)) {return '';}// Find the number of attempts since the cached statistics were computed.list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts, true);$count = $DB->count_records_sql("SELECT COUNT(1)FROM $fromqaWHERE $whereqaAND quiza.timefinish > {$lastcachetime}", $qaparams);if (!$count) {$count = 0;}// Generate the output.$a = new stdClass();$a->lastcalculated = format_time(time() - $lastcachetime);$a->count = $count;$recalcualteurl = new moodle_url($reporturl,['recalculate' => 1, 'sesskey' => sesskey()]);$output = '';$output .= $OUTPUT->box_start('boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');$output .= get_string('lastcalculated', 'quiz_statistics', $a);$output .= $OUTPUT->single_button($recalcualteurl,get_string('recalculatenow', 'quiz_statistics'));$output .= $OUTPUT->box_end(true);return $output;}/*** Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report* is displayed.** @param $qubaids qubaid_condition*/public function clear_cached_data($qubaids) {global $DB;$DB->delete_records('quiz_statistics', ['hashcode' => $qubaids->get_hash_code()]);$DB->delete_records('question_statistics', ['hashcode' => $qubaids->get_hash_code()]);$DB->delete_records('question_response_analysis', ['hashcode' => $qubaids->get_hash_code()]);}/*** Load the questions in this quiz and add some properties to the objects needed in the reports.** @param stdClass $quiz the quiz.* @return array of questions for this quiz.*/public function load_and_initialise_questions_for_calculations($quiz) {// Load the questions.$questions = quiz_report_get_significant_questions($quiz);$questiondata = [];foreach ($questions as $qs => $question) {$displaynumber = $question->displaynumber;if ($question->qtype === 'random') {$question->id = 0;$question->name = get_string('random', 'quiz');$question->questiontext = get_string('random', 'quiz');$question->parenttype = 'random';$question->number = $displaynumber;$questiondata[$question->slot] = $question;} else if ($question->qtype === 'missingtype') {$question->id = is_numeric($question->id) ? (int) $question->id : 0;$questiondata[$question->slot] = $question;$question->name = get_string('deletedquestion', 'qtype_missingtype');$question->questiontext = get_string('deletedquestiontext', 'qtype_missingtype');} else {$q = question_bank::load_question_data($question->id);$q->maxmark = $question->maxmark;$q->slot = $question->slot;$q->number = $displaynumber;$q->parenttype = null;$questiondata[$question->slot] = $q;}}return $questiondata;}/*** Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.** @param $qubaids* @param $questions* @param $questionstats* @param $reporturl* @param $whichtries string*/protected function output_all_question_response_analysis($qubaids,$questions,$questionstats,$reporturl,$whichtries = question_attempt::LAST_TRY) {foreach ($questions as $slot => $question) {if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {if ($questionstats->for_slot($slot)->get_variants()) {foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {$this->output_individual_question_response_analysis($question,$variantno,$questionstats->for_slot($slot, $variantno)->s,$reporturl,$qubaids,$whichtries);}} else {$this->output_individual_question_response_analysis($question,null,$questionstats->for_slot($slot)->s,$reporturl,$qubaids,$whichtries);}} else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {foreach ($subqids as $subqid) {if ($variants = $questionstats->for_subq($subqid)->get_variants()) {foreach ($variants as $variantno) {$this->output_individual_question_response_analysis($questionstats->for_subq($subqid, $variantno)->question,$variantno,$questionstats->for_subq($subqid, $variantno)->s,$reporturl,$qubaids,$whichtries);}} else {$this->output_individual_question_response_analysis($questionstats->for_subq($subqid)->question,null,$questionstats->for_subq($subqid)->s,$reporturl,$qubaids,$whichtries);}}}}}/*** Load question stats for a quiz** @param int $quizid question usage* @param bool $calculateifrequired if true (the default) the stats will be calculated if not already stored.* If false, null will be returned if the stats are not already available.* @param bool $performanalysis if true (the default) and there are calculated stats, analysis will be performed* for each question.* @return ?all_calculated_for_qubaid_condition question stats*/public function calculate_questions_stats_for_question_bank(int $quizid,bool $calculateifrequired = true,bool $performanalysis = true,): ?all_calculated_for_qubaid_condition {global $DB;$quiz = $DB->get_record('quiz', ['id' => $quizid], '*', MUST_EXIST);$questions = $this->load_and_initialise_questions_for_calculations($quiz);[, $questionstats] = $this->get_all_stats_and_analysis($quiz,$quiz->grademethod, question_attempt::ALL_TRIES, new \core\dml\sql_join(),$questions, null, $calculateifrequired, $performanalysis);return $questionstats;}}