2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Quiz statistics report class.
21 * @subpackage statistics
22 * @copyright 2008 Jamie Pratt
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') || die();
29 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
30 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
31 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
32 require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
33 require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php');
37 * The quiz statistics report provides summary information about each question in
38 * a quiz, compared to the whole quiz. It also provides a drill-down to more
39 * detailed information about each question.
41 * @copyright 2008 Jamie Pratt
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44 class quiz_statistics_report extends quiz_default_report {
45 /** @var integer Time after which statistics are automatically recomputed. */
46 const TIME_TO_CACHE_STATS = 900; // 15 minutes
48 /** @var object instance of table class used for main questions stats table. */
54 public function display($quiz, $cm, $course) {
55 global $CFG, $DB, $OUTPUT, $PAGE;
57 $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
59 // Work out the display options.
60 $download = optional_param('download', '', PARAM_ALPHA);
61 $everything = optional_param('everything', 0, PARAM_BOOL);
62 $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
63 // A qid paramter indicates we should display the detailed analysis of a question.
64 $qid = optional_param('qid', 0, PARAM_INT);
65 $slot = optional_param('slot', 0, PARAM_INT);
67 $pageoptions = array();
68 $pageoptions['id'] = $cm->id;
69 $pageoptions['mode'] = 'statistics';
71 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
73 $mform = new quiz_statistics_statistics_settings_form($reporturl);
74 if ($fromform = $mform->get_data()) {
75 $useallattempts = $fromform->useallattempts;
76 if ($fromform->useallattempts) {
77 set_user_preference('quiz_report_statistics_useallattempts',
78 $fromform->useallattempts);
80 unset_user_preference('quiz_report_statistics_useallattempts');
84 $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
87 // Find out current groups mode
88 $groupmode = groups_get_activity_groupmode($cm);
89 $currentgroup = groups_get_activity_group($cm, true);
90 $nostudentsingroup = false; // True if a group is selected and there is no one in it.
91 if (empty($currentgroup)) {
93 $groupstudents = array();
96 // All users who can attempt quizzes and who are in the currently selected group
97 $groupstudents = get_users_by_capability($this->context,
98 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
99 '', '', '', '', $currentgroup, '', false);
100 if (!$groupstudents) {
101 $nostudentsingroup = true;
105 // If recalculate was requested, handle that.
106 if ($recalculate && confirm_sesskey()) {
107 $this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
108 redirect($reporturl);
111 // Set up the main table.
112 $this->table = new quiz_report_statistics_table();
114 $report = get_string('completestatsfilename', 'quiz_statistics');
116 $report = get_string('questionstatsfilename', 'quiz_statistics');
118 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
119 $courseshortname = format_string($course->shortname, true,
120 array('context' => $coursecontext));
121 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
122 $this->table->is_downloading($download, $filename,
123 get_string('quizstructureanalysis', 'quiz_statistics'));
125 // Load the questions.
126 $questions = quiz_report_get_significant_questions($quiz);
127 $questionids = array();
128 foreach ($questions as $question) {
129 $questionids[] = $question->id;
131 $fullquestions = question_load_questions($questionids);
132 foreach ($questions as $qno => $question) {
133 $q = $fullquestions[$question->id];
134 $q->maxmark = $question->maxmark;
136 $q->number = $question->number;
137 $questions[$qno] = $q;
140 // Get the data to be displayed.
141 list($quizstats, $questions, $subquestions, $s) =
142 $this->get_quiz_and_questions_stats($quiz, $currentgroup,
143 $nostudentsingroup, $useallattempts, $groupstudents, $questions);
144 $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats);
146 // Set up the table, if there is data.
148 $this->table->setup($quiz, $cm->id, $reporturl, $s);
151 // Print the page header stuff (if not downloading.
152 if (!$this->table->is_downloading()) {
153 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
156 groups_print_activity_menu($cm, $reporturl->out());
157 if ($currentgroup && !$groupstudents) {
158 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
162 if (!quiz_questions_in_quiz($quiz->questions)) {
163 echo quiz_no_questions_message($quiz, $cm, $this->context);
164 } else if (!$this->table->is_downloading() && $s == 0) {
165 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
168 // Print display options form.
169 $mform->set_data(array('useallattempts' => $useallattempts));
173 if ($everything) { // Implies is downloading.
174 // Overall report, then the analysis of each question.
175 $this->download_quiz_info_table($quizinfo);
178 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
180 if ($this->table->is_downloading() == 'xhtml') {
181 $this->output_statistics_graph($quizstats->id, $s);
184 foreach ($questions as $question) {
185 if (question_bank::get_qtype(
186 $question->qtype, false)->can_analyse_responses()) {
187 $this->output_individual_question_response_analysis(
188 $question, $reporturl, $quizstats);
190 } else if (!empty($question->_stats->subquestions)) {
191 $subitemstodisplay = explode(',', $question->_stats->subquestions);
192 foreach ($subitemstodisplay as $subitemid) {
193 $this->output_individual_question_response_analysis(
194 $subquestions[$subitemid], $reporturl, $quizstats);
200 $this->table->export_class_instance()->finish_document();
203 // Report on an individual question indexed by position.
204 if (!isset($questions[$slot])) {
205 print_error('questiondoesnotexist', 'question');
208 $this->output_individual_question_data($quiz, $questions[$slot]);
209 $this->output_individual_question_response_analysis(
210 $questions[$slot], $reporturl, $quizstats);
212 // Back to overview link.
213 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
214 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
215 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
218 // Report on an individual sub-question indexed questionid.
219 if (!isset($subquestions[$qid])) {
220 print_error('questiondoesnotexist', 'question');
223 $this->output_individual_question_data($quiz, $subquestions[$qid]);
224 $this->output_individual_question_response_analysis(
225 $subquestions[$qid], $reporturl, $quizstats);
227 // Back to overview link.
228 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
229 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
230 'boxaligncenter generalbox boxwidthnormal mdl-align');
232 } else if ($this->table->is_downloading()) {
233 // Downloading overview report.
234 $this->download_quiz_info_table($quizinfo);
235 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
236 $this->table->finish_output();
239 // On-screen display of overview report.
240 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
241 echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
242 $groupstudents, $useallattempts, $reporturl);
243 echo $this->everything_download_options();
244 echo $this->output_quiz_info_table($quizinfo);
246 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
247 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
248 $this->output_statistics_graph($quizstats->id, $s);
256 * Display the statistical and introductory information about a question.
257 * Only called when not downloading.
258 * @param object $quiz the quiz settings.
259 * @param object $question the question to report on.
260 * @param moodle_url $reporturl the URL to resisplay this report.
261 * @param object $quizstats Holds the quiz statistics.
263 protected function output_individual_question_data($quiz, $question) {
266 // On-screen display. Show a summary of the question's place in the quiz,
267 // and the question statistics.
268 $datumfromtable = $this->table->format_row($question);
270 // Set up the question info table.
271 $questioninfotable = new html_table();
272 $questioninfotable->align = array('center', 'center');
273 $questioninfotable->width = '60%';
274 $questioninfotable->attributes['class'] = 'generaltable titlesleft';
276 $questioninfotable->data = array();
277 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
278 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
279 $question->name.' '.$datumfromtable['actions']);
280 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
281 $datumfromtable['icon'] . ' ' .
282 question_bank::get_qtype($question->qtype, false)->menu_name() . ' ' .
283 $datumfromtable['icon']);
284 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
285 $question->_stats->positions);
287 // Set up the question statistics table.
288 $questionstatstable = new html_table();
289 $questionstatstable->align = array('center', 'center');
290 $questionstatstable->width = '60%';
291 $questionstatstable->attributes['class'] = 'generaltable titlesleft';
293 unset($datumfromtable['number']);
294 unset($datumfromtable['icon']);
295 $actions = $datumfromtable['actions'];
296 unset($datumfromtable['actions']);
297 unset($datumfromtable['name']);
299 's' => get_string('attempts', 'quiz_statistics'),
300 'facility' => get_string('facility', 'quiz_statistics'),
301 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
302 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
303 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
304 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
305 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
306 'discriminative_efficiency' =>
307 get_string('discriminative_efficiency', 'quiz_statistics')
309 foreach ($datumfromtable as $item => $value) {
310 $questionstatstable->data[] = array($labels[$item], $value);
313 // Display the various bits.
314 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
315 echo html_writer::table($questioninfotable);
316 echo $this->render_question_text($question);
317 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
318 echo html_writer::table($questionstatstable);
320 public function format_text($text, $format, $qa, $component, $filearea, $itemid,
322 $formatoptions = new stdClass();
323 $formatoptions->noclean = !$clean;
324 $formatoptions->para = false;
325 $text = $qa->rewrite_pluginfile_urls($text, $component, $filearea, $itemid);
326 return format_text($text, $format, $formatoptions);
329 /** @return the result of applying {@link format_text()} to the question text. */
330 public function format_questiontext($qa) {
331 return $this->format_text($this->questiontext, $this->questiontextformat,
332 $qa, 'question', 'questiontext', $this->id);
336 * @param object $question question data.
337 * @return string HTML of question text, ready for display.
339 protected function render_question_text($question) {
342 $text = question_rewrite_questiontext_preview_urls($question->questiontext,
343 $this->context->id, 'quiz_statistics', $question->id);
345 return $OUTPUT->box(format_text($text, $question->questiontextformat,
346 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
347 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
351 * Display the response analysis for a question.
352 * @param object $question the question to report on.
353 * @param moodle_url $reporturl the URL to resisplay this report.
354 * @param object $quizstats Holds the quiz statistics.
356 protected function output_individual_question_response_analysis($question,
357 $reporturl, $quizstats) {
360 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
364 $qtable = new quiz_report_statistics_question_table($question->id);
365 $exportclass = $this->table->export_class_instance();
366 $qtable->export_class_instance($exportclass);
367 if (!$this->table->is_downloading()) {
368 // Output an appropriate title.
369 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
372 // Work out an appropriate title.
373 $questiontabletitle = '"' . $question->name . '"';
374 if (!empty($question->number)) {
375 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
377 if ($this->table->is_downloading() == 'xhtml') {
378 $questiontabletitle = get_string('analysisofresponsesfor',
379 'quiz_statistics', $questiontabletitle);
383 $exportclass->start_table($questiontabletitle);
385 if ($this->table->is_downloading() == 'xhtml') {
386 echo $this->render_question_text($question);
390 $responesstats = new quiz_statistics_response_analyser($question);
391 $responesstats->load_cached($quizstats->id);
393 $qtable->setup($reporturl, $question, $responesstats);
394 if ($this->table->is_downloading()) {
395 $exportclass->output_headers($qtable->headers);
398 foreach ($responesstats->responseclasses as $partid => $partclasses) {
399 $rowdata = new stdClass();
400 $rowdata->part = $partid;
401 foreach ($partclasses as $responseclassid => $responseclass) {
402 $rowdata->responseclass = $responseclass->responseclass;
404 $responsesdata = $responesstats->responses[$partid][$responseclassid];
405 if (empty($responsesdata)) {
406 if (!array_key_exists('responseclass', $qtable->columns)) {
407 $rowdata->response = $responseclass->responseclass;
409 $rowdata->response = '';
411 $rowdata->fraction = $responseclass->fraction;
413 $qtable->add_data_keyed($qtable->format_row($rowdata));
417 foreach ($responsesdata as $response => $data) {
418 $rowdata->response = $response;
419 $rowdata->fraction = $data->fraction;
420 $rowdata->count = $data->count;
421 $qtable->add_data_keyed($qtable->format_row($rowdata));
426 $qtable->finish_output(!$this->table->is_downloading());
430 * Output the table that lists all the questions in the quiz with their statistics.
431 * @param int $s number of attempts.
432 * @param array $questions the questions in the quiz.
433 * @param array $subquestions the subquestions of any random questions.
435 protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
440 foreach ($questions as $question) {
441 // Output the data for this questions.
442 $this->table->add_data_keyed($this->table->format_row($question));
444 if (empty($question->_stats->subquestions)) {
448 // And its subquestions, if it has any.
449 $subitemstodisplay = explode(',', $question->_stats->subquestions);
450 foreach ($subitemstodisplay as $subitemid) {
451 $subquestions[$subitemid]->maxmark = $question->maxmark;
452 $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
456 $this->table->finish_output(!$this->table->is_downloading());
459 protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
461 // You can edit this array to control which statistics are displayed.
462 $todisplay = array('firstattemptscount' => 'number',
463 'allattemptscount' => 'number',
464 'firstattemptsavg' => 'summarks_as_percentage',
465 'allattemptsavg' => 'summarks_as_percentage',
466 'median' => 'summarks_as_percentage',
467 'standarddeviation' => 'summarks_as_percentage',
468 'skewness' => 'number_format',
469 'kurtosis' => 'number_format',
470 'cic' => 'number_format_percent',
471 'errorratio' => 'number_format_percent',
472 'standarderror' => 'summarks_as_percentage');
474 // General information about the quiz.
476 $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
477 $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
479 $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
481 if ($quiz->timeopen) {
482 $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
484 if ($quiz->timeclose) {
485 $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
487 if ($quiz->timeopen && $quiz->timeclose) {
488 $quizinfo[get_string('duration', 'quiz_statistics')] =
489 format_time($quiz->timeclose - $quiz->timeopen);
493 foreach ($todisplay as $property => $format) {
494 if (!isset($quizstats->$property) || empty($format[$property])) {
497 $value = $quizstats->$property;
500 case 'summarks_as_percentage':
501 $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
503 case 'number_format_percent':
504 $formattedvalue = quiz_format_grade($quiz, $value) . '%';
506 case 'number_format':
507 // + 2 decimal places, since not a percentage,
508 // and we want the same number of sig figs.
509 $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
512 $formattedvalue = $value + 0;
515 $formattedvalue = $value;
518 $quizinfo[get_string($property, 'quiz_statistics',
519 $this->using_attempts_string(!empty($quizstats->allattempts)))] =
527 * Output the table of overall quiz statistics.
528 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
529 * @return string the HTML.
531 protected function output_quiz_info_table($quizinfo) {
533 $quizinfotable = new html_table();
534 $quizinfotable->align = array('center', 'center');
535 $quizinfotable->width = '60%';
536 $quizinfotable->attributes['class'] = 'generaltable titlesleft';
537 $quizinfotable->data = array();
539 foreach ($quizinfo as $heading => $value) {
540 $quizinfotable->data[] = array($heading, $value);
543 return html_writer::table($quizinfotable);
547 * Download the table of overall quiz statistics.
548 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
550 protected function download_quiz_info_table($quizinfo) {
553 // XHTML download is a special case.
554 if ($this->table->is_downloading() == 'xhtml') {
555 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
556 echo $this->output_quiz_info_table($quizinfo);
560 // Reformat the data ready for output.
563 foreach ($quizinfo as $heading => $value) {
564 $headers[] = $heading;
569 $exportclass = $this->table->export_class_instance();
570 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
571 $exportclass->output_headers($headers);
572 $exportclass->add_data($row);
573 $exportclass->finish_table();
577 * Output the HTML needed to show the statistics graph.
578 * @param int $quizstatsid the id of the statistics to show in the graph.
580 protected function output_statistics_graph($quizstatsid, $s) {
587 $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
588 array('id' => $quizstatsid));
589 $OUTPUT->heading(get_string('statisticsreportgraph', 'quiz_statistics'));
590 echo html_writer::tag('div', html_writer::empty_tag('img', array('src' => $imageurl,
591 'alt' => get_string('statisticsreportgraph', 'quiz_statistics'))),
592 array('class' => 'graph'));
596 * Return the stats data for when there are no stats to show.
598 * @param array $questions question definitions.
599 * @param int $firstattemptscount number of first attempts (optional).
600 * @param int $firstattemptscount total number of attempts (optional).
601 * @return array with three elements:
602 * - integer $s Number of attempts included in the stats (0).
603 * - array $quizstats The statistics for overall attempt scores.
604 * - array $qstats The statistics for each question.
606 protected function get_emtpy_stats($questions, $firstattemptscount = 0,
607 $allattemptscount = 0) {
608 $quizstats = new stdClass();
609 $quizstats->firstattemptscount = $firstattemptscount;
610 $quizstats->allattemptscount = $allattemptscount;
612 $qstats = new stdClass();
613 $qstats->questions = $questions;
614 $qstats->subquestions = array();
615 $qstats->responses = array();
617 return array(0, $quizstats, false);
621 * Compute the quiz statistics.
623 * @param object $quizid the quiz id.
624 * @param int $currentgroup the current group. 0 for none.
625 * @param bool $nostudentsingroup true if there a no students.
626 * @param bool $useallattempts use all attempts, or just first attempts.
627 * @param array $groupstudents students in this group.
628 * @param array $questions question definitions.
629 * @return array with three elements:
630 * - integer $s Number of attempts included in the stats.
631 * - array $quizstats The statistics for overall attempt scores.
632 * - array $qstats The statistics for each question.
634 protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
635 $useallattempts, $groupstudents, $questions) {
638 // Calculating MEAN of marks for all attempts by students
639 // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
640 // #Calculating_MEAN_of_grades_for_all_attempts_by_students
641 if ($nostudentsingroup) {
642 return $this->get_emtpy_stats($questions);
645 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
646 $quizid, $currentgroup, $groupstudents, true);
648 $attempttotals = $DB->get_records_sql("
650 CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
651 COUNT(1) AS countrecs,
652 SUM(sumgrades) AS total
655 GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
657 if (!$attempttotals) {
658 return $this->get_emtpy_stats($questions);
661 if (isset($attempttotals[1])) {
662 $firstattempts = $attempttotals[1];
663 $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
665 $firstattempts = new stdClass();
666 $firstattempts->countrecs = 0;
667 $firstattempts->total = 0;
668 $firstattempts->average = '-';
671 $allattempts = new stdClass();
672 if (isset($attempttotals[0])) {
673 $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
674 $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
676 $allattempts->countrecs = $firstattempts->countrecs;
677 $allattempts->total = $firstattempts->total;
680 if ($useallattempts) {
681 $usingattempts = $allattempts;
682 $usingattempts->sql = '';
684 $usingattempts = $firstattempts;
685 $usingattempts->sql = 'AND quiza.attempt = 1 ';
688 $s = $usingattempts->countrecs;
690 return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
691 $allattempts->countrecs);
693 $summarksavg = $usingattempts->total / $usingattempts->countrecs;
695 $quizstats = new stdClass();
696 $quizstats->allattempts = $useallattempts;
697 $quizstats->firstattemptscount = $firstattempts->countrecs;
698 $quizstats->allattemptscount = $allattempts->countrecs;
699 $quizstats->firstattemptsavg = $firstattempts->average;
700 $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
702 // Recalculate sql again this time possibly including test for first attempt.
703 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
704 $quizid, $currentgroup, $groupstudents, $useallattempts);
708 //even number of attempts
709 $limitoffset = $s/2 - 1;
712 $limitoffset = floor($s/2);
715 $sql = "SELECT id, sumgrades
720 $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
722 $quizstats->median = array_sum($medianmarks) / count($medianmarks);
724 //fetch sum of squared, cubed and power 4d
725 //differences between marks and mean mark
726 $mean = $usingattempts->total / $s;
728 SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
729 SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
730 SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
733 $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
735 $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
737 // Standard_Deviation
738 // see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
739 // #Standard_Deviation
741 $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
745 // see http://docs.moodle.org/dev/
746 // Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
747 $m2= $powers->power2 / $s;
748 $m3= $powers->power3 / $s;
749 $m4= $powers->power4 / $s;
752 $k3= $s*$s*$m3/(($s-1)*($s-2));
754 $quizstats->skewness = $k3 / (pow($k2, 3/2));
760 $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
762 $quizstats->kurtosis = $k4 / ($k2*$k2);
767 $qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
768 $qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
769 $qstats->compute_statistics();
772 $p = count($qstats->questions); // No of positions
773 if ($p > 1 && isset($k2)) {
774 $quizstats->cic = (100 * $p / ($p -1)) *
775 (1 - ($qstats->get_sum_of_mark_variance()) / $k2);
776 $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
777 $quizstats->standarderror = $quizstats->errorratio *
778 $quizstats->standarddeviation / 100;
782 return array($s, $quizstats, $qstats);
786 * Load the cached statistics from the database.
788 * @param object $quiz the quiz settings
789 * @param int $currentgroup the current group. 0 for none.
790 * @param bool $nostudentsingroup true if there a no students.
791 * @param bool $useallattempts use all attempts, or just first attempts.
792 * @param array $groupstudents students in this group.
793 * @param array $questions question definitions.
794 * @return array with 4 elements:
795 * - $quizstats The statistics for overall attempt scores.
796 * - $questions The questions, with an additional _stats field.
797 * - $subquestions The subquestions, if any, with an additional _stats field.
798 * - $s Number of attempts included in the stats.
799 * If there is no cached data in the database, returns an array of four nulls.
801 protected function try_loading_cached_stats($quiz, $currentgroup,
802 $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
805 $timemodified = time() - self::TIME_TO_CACHE_STATS;
806 $quizstats = $DB->get_record_select('quiz_statistics',
807 'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
808 array($quiz->id, $currentgroup, $useallattempts, $timemodified));
811 // No cached data found.
812 return array(null, $questions, null, null);
815 if ($useallattempts) {
816 $s = $quizstats->allattemptscount;
818 $s = $quizstats->firstattemptscount;
821 $subquestions = array();
822 $questionstats = $DB->get_records('quiz_question_statistics',
823 array('quizstatisticsid' => $quizstats->id));
825 $subquestionstats = array();
826 foreach ($questionstats as $stat) {
828 $questions[$stat->slot]->_stats = $stat;
830 $subquestionstats[$stat->questionid] = $stat;
834 if (!empty($subquestionstats)) {
835 $subqstofetch = array_keys($subquestionstats);
836 $subquestions = question_load_questions($subqstofetch);
837 foreach ($subquestions as $subqid => $subq) {
838 $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
839 $subquestions[$subqid]->maxmark = $subq->defaultmark;
843 return array($quizstats, $questions, $subquestions, $s);
847 * Store the statistics in the cache tables in the database.
849 * @param object $quizid the quiz id.
850 * @param int $currentgroup the current group. 0 for none.
851 * @param bool $useallattempts use all attempts, or just first attempts.
852 * @param object $quizstats The statistics for overall attempt scores.
853 * @param array $questions The questions, with an additional _stats field.
854 * @param array $subquestions The subquestions, if any, with an additional _stats field.
856 protected function cache_stats($quizid, $currentgroup,
857 $quizstats, $questions, $subquestions) {
860 $toinsert = clone($quizstats);
861 $toinsert->quizid = $quizid;
862 $toinsert->groupid = $currentgroup;
863 $toinsert->timemodified = time();
865 // Fix up some dodgy data.
866 if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
867 $toinsert->errorratio = null;
869 if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
870 $toinsert->standarderror = null;
874 $quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
876 foreach ($questions as $question) {
877 $question->_stats->quizstatisticsid = $quizstats->id;
878 $DB->insert_record('quiz_question_statistics', $question->_stats, false);
881 foreach ($subquestions as $subquestion) {
882 $subquestion->_stats->quizstatisticsid = $quizstats->id;
883 $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
886 return $quizstats->id;
890 * Get the quiz and question statistics, either by loading the cached results,
891 * or by recomputing them.
893 * @param object $quiz the quiz settings.
894 * @param int $currentgroup the current group. 0 for none.
895 * @param bool $nostudentsingroup true if there a no students.
896 * @param bool $useallattempts use all attempts, or just first attempts.
897 * @param array $groupstudents students in this group.
898 * @param array $questions question definitions.
899 * @return array with 4 elements:
900 * - $quizstats The statistics for overall attempt scores.
901 * - $questions The questions, with an additional _stats field.
902 * - $subquestions The subquestions, if any, with an additional _stats field.
903 * - $s Number of attempts included in the stats.
905 protected function get_quiz_and_questions_stats($quiz, $currentgroup,
906 $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
908 list($quizstats, $questions, $subquestions, $s) =
909 $this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
910 $useallattempts, $groupstudents, $questions);
912 if (is_null($quizstats)) {
913 list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
914 $currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
917 $questions = $qstats->questions;
918 $subquestions = $qstats->subquestions;
920 $quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
921 $quizstats, $questions, $subquestions);
923 $this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
924 $nostudentsingroup, $useallattempts, $groupstudents,
925 $questions, $subquestions);
929 return array($quizstats, $questions, $subquestions, $s);
932 protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
933 $nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
935 $qubaids = quiz_statistics_qubaids_condition(
936 $quizid, $currentgroup, $groupstudents, $useallattempts);
939 foreach ($questions as $question) {
940 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
943 $done[$question->id] = 1;
945 $responesstats = new quiz_statistics_response_analyser($question);
946 $responesstats->analyse($qubaids);
947 $responesstats->store_cached($quizstatisticsid);
950 foreach ($subquestions as $question) {
951 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
952 isset($done[$question->id])) {
955 $done[$question->id] = 1;
957 $responesstats = new quiz_statistics_response_analyser($question);
958 $responesstats->analyse($qubaids);
959 $responesstats->store_cached($quizstatisticsid);
964 * @return string HTML snipped for the Download full report as UI.
966 protected function everything_download_options() {
967 $downloadoptions = $this->table->get_download_menu();
969 $output = '<form action="'. $this->table->baseurl .'" method="post">';
970 $output .= '<div class="mdl-align">';
971 $output .= '<input type="hidden" name="everything" value="1"/>';
972 $output .= '<input type="submit" value="' .
973 get_string('downloadeverything', 'quiz_statistics') . '"/>';
974 $output .= html_writer::select($downloadoptions, 'download',
975 $this->table->defaultdownloadformat, false);
976 $output .= '</div></form>';
982 * Generate the snipped of HTML that says when the stats were last caculated,
983 * with a recalcuate now button.
984 * @param object $quizstats the overall quiz statistics.
985 * @param int $quizid the quiz id.
986 * @param int $currentgroup the id of the currently selected group, or 0.
987 * @param array $groupstudents ids of students in the group.
988 * @param bool $useallattempts whether to use all attempts, instead of just
990 * @return string a HTML snipped saying when the stats were last computed,
991 * or blank if that is not appropriate.
993 protected function output_caching_info($quizstats, $quizid, $currentgroup,
994 $groupstudents, $useallattempts, $reporturl) {
997 if (empty($quizstats->timemodified)) {
1001 // Find the number of attempts since the cached statistics were computed.
1002 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
1003 $quizid, $currentgroup, $groupstudents, $useallattempts, true);
1004 $count = $DB->count_records_sql("
1008 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
1014 // Generate the output.
1015 $a = new stdClass();
1016 $a->lastcalculated = format_time(time() - $quizstats->timemodified);
1019 $recalcualteurl = new moodle_url($reporturl,
1020 array('recalculate' => 1, 'sesskey' => sesskey()));
1022 $output .= $OUTPUT->box_start(
1023 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
1024 $output .= get_string('lastcalculated', 'quiz_statistics', $a);
1025 $output .= $OUTPUT->single_button($recalcualteurl,
1026 get_string('recalculatenow', 'quiz_statistics'));
1027 $output .= $OUTPUT->box_end(true);
1033 * Clear the cached data for a particular report configuration. This will
1034 * trigger a re-computation the next time the report is displayed.
1035 * @param int $quizid the quiz id.
1036 * @param int $currentgroup a group id, or 0.
1037 * @param bool $useallattempts whether all attempts, or just first attempts are included.
1039 protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
1042 $todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
1043 'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
1049 list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
1051 $DB->delete_records_select('quiz_question_statistics',
1052 'quizstatisticsid ' . $todeletesql, $todeleteparams);
1053 $DB->delete_records_select('quiz_question_response_stats',
1054 'quizstatisticsid ' . $todeletesql, $todeleteparams);
1055 $DB->delete_records_select('quiz_statistics',
1056 'id ' . $todeletesql, $todeleteparams);
1060 * @param bool $useallattempts whether we are using all attempts.
1061 * @return the appropriate lang string to describe this option.
1063 protected function using_attempts_string($useallattempts) {
1064 if ($useallattempts) {
1065 return get_string('allattempts', 'quiz_statistics');
1067 return get_string('firstattempts', 'quiz_statistics');
1072 function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
1073 $allattempts = true, $includeungraded = false) {
1076 $fromqa = '{quiz_attempts} quiza ';
1078 $whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.timefinish <> 0';
1079 $qaparams = array('quizid' => $quizid);
1081 if (!empty($currentgroup) && $groupstudents) {
1082 list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
1083 SQL_PARAMS_NAMED, 'u');
1084 $whereqa .= " AND quiza.userid $grpsql";
1085 $qaparams += $grpparams;
1088 if (!$allattempts) {
1089 $whereqa .= ' AND quiza.attempt = 1';
1092 if (!$includeungraded) {
1093 $whereqa .= ' AND quiza.sumgrades IS NOT NULL';
1096 return array($fromqa, $whereqa, $qaparams);
1100 * Return a {@link qubaid_condition} from the values returned by
1101 * {@link quiz_statistics_attempts_sql}
1102 * @param string $fromqa from quiz_statistics_attempts_sql.
1103 * @param string $whereqa from quiz_statistics_attempts_sql.
1105 function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
1106 $allattempts = true, $includeungraded = false) {
1107 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
1108 $groupstudents, $allattempts, $includeungraded);
1109 return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);