weekly release 2.6dev
[moodle.git] / mod / quiz / report / statistics / report.php
CommitLineData
0c1c764e 1<?php
04853f27
TH
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/>.
16
0c1c764e 17/**
04853f27 18 * Quiz statistics report class.
0c1c764e 19 *
8d76124c
TH
20 * @package quiz_statistics
21 * @copyright 2008 Jamie Pratt
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
23 */
24
0c1c764e 25
a17b297d
TH
26defined('MOODLE_INTERNAL') || die();
27
04853f27
TH
28require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
29require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
30require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
e68e4ccf
JP
31require_once($CFG->dirroot . '/question/engine/statistics.php');
32require_once($CFG->dirroot . '/question/engine/responseanalysis.php');
33require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
a17b297d 34
04853f27
TH
35/**
36 * The quiz statistics report provides summary information about each question in
37 * a quiz, compared to the whole quiz. It also provides a drill-down to more
38 * detailed information about each question.
39 *
8d76124c
TH
40 * @copyright 2008 Jamie Pratt
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 42 */
c386eaa3 43class quiz_statistics_report extends quiz_default_report {
04853f27 44 /** @var integer Time after which statistics are automatically recomputed. */
768a7588 45 const TIME_TO_CACHE_STATS = 900; // 15 minutes.
3b1d5cc4 46
04853f27
TH
47 /** @var object instance of table class used for main questions stats table. */
48 protected $table;
0c1c764e 49
50 /**
51 * Display the report.
52 */
04853f27
TH
53 public function display($quiz, $cm, $course) {
54 global $CFG, $DB, $OUTPUT, $PAGE;
0c1c764e 55
26aded55 56 $this->context = context_module::instance($cm->id);
0c1c764e 57
04853f27 58 // Work out the display options.
0c1c764e 59 $download = optional_param('download', '', PARAM_ALPHA);
869309b8 60 $everything = optional_param('everything', 0, PARAM_BOOL);
d1789d5d 61 $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
04853f27 62 // A qid paramter indicates we should display the detailed analysis of a question.
43ec99aa 63 $qid = optional_param('qid', 0, PARAM_INT);
04853f27
TH
64 $slot = optional_param('slot', 0, PARAM_INT);
65
0c1c764e 66 $pageoptions = array();
67 $pageoptions['id'] = $cm->id;
0c1c764e 68 $pageoptions['mode'] = 'statistics';
69
a6855934 70 $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
0c1c764e 71
6b4e2d76 72 $mform = new quiz_statistics_settings_form($reporturl);
04853f27 73 if ($fromform = $mform->get_data()) {
0c1c764e 74 $useallattempts = $fromform->useallattempts;
04853f27 75 if ($fromform->useallattempts) {
25a03faa
TH
76 set_user_preference('quiz_report_statistics_useallattempts',
77 $fromform->useallattempts);
0c1c764e 78 } else {
79 unset_user_preference('quiz_report_statistics_useallattempts');
80 }
04853f27 81
0c1c764e 82 } else {
83 $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
84 }
85
768a7588 86 // Find out current groups mode.
490668bb 87 $currentgroup = $this->get_current_group($cm, $course, $this->context);
04853f27
TH
88 $nostudentsingroup = false; // True if a group is selected and there is no one in it.
89 if (empty($currentgroup)) {
90 $currentgroup = 0;
91 $groupstudents = array();
3b1d5cc4 92
e4977ba5
TH
93 } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
94 $groupstudents = array();
95 $nostudentsingroup = true;
96
04853f27 97 } else {
768a7588 98 // All users who can attempt quizzes and who are in the currently selected group.
fdb5bc03 99 $groupstudents = get_users_by_capability($this->context,
04853f27
TH
100 array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
101 '', '', '', '', $currentgroup, '', false);
102 if (!$groupstudents) {
e72efdd4 103 $nostudentsingroup = true;
0c1c764e 104 }
0c1c764e 105 }
3b1d5cc4 106
e68e4ccf
JP
107 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
108
109
04853f27 110 // If recalculate was requested, handle that.
db77f410 111 if ($recalculate && confirm_sesskey()) {
e68e4ccf 112 $this->clear_cached_data($qubaids);
e42f153c 113 redirect($reporturl);
43ec99aa 114 }
3b1d5cc4 115
04853f27 116 // Set up the main table.
59ea8176 117 $this->table = new quiz_statistics_table();
04853f27
TH
118 if ($everything) {
119 $report = get_string('completestatsfilename', 'quiz_statistics');
120 } else {
121 $report = get_string('questionstatsfilename', 'quiz_statistics');
122 }
0eafc988 123 $courseshortname = format_string($course->shortname, true,
26aded55 124 array('context' => context_course::instance($course->id)));
8ebbb06a 125 $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
25a03faa
TH
126 $this->table->is_downloading($download, $filename,
127 get_string('quizstructureanalysis', 'quiz_statistics'));
3652dddd 128 $questions = $this->load_and_initialise_questions_for_calculations($quiz);
04853f27
TH
129
130 // Get the data to be displayed.
131 list($quizstats, $questions, $subquestions, $s) =
132 $this->get_quiz_and_questions_stats($quiz, $currentgroup,
133 $nostudentsingroup, $useallattempts, $groupstudents, $questions);
134 $quizinfo = $this->get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats);
135
04853f27 136 // Set up the table, if there is data.
b0e4fa41 137 if ($s) {
90fbad64 138 $this->table->statistics_setup($quiz, $cm->id, $reporturl, $s);
43ec99aa 139 }
3b1d5cc4 140
04853f27
TH
141 // Print the page header stuff (if not downloading.
142 if (!$this->table->is_downloading()) {
143 $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
144
e4977ba5 145 if (groups_get_activity_groupmode($cm)) {
04853f27
TH
146 groups_print_activity_menu($cm, $reporturl->out());
147 if ($currentgroup && !$groupstudents) {
148 $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
b0e4fa41
TH
149 }
150 }
151
3c6185e9 152 if (!quiz_questions_in_quiz($quiz->questions)) {
fdb5bc03 153 echo quiz_no_questions_message($quiz, $cm, $this->context);
3c6185e9
TH
154 } else if (!$this->table->is_downloading() && $s == 0) {
155 echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
156 }
157
04853f27
TH
158 // Print display options form.
159 $mform->set_data(array('useallattempts' => $useallattempts));
160 $mform->display();
161 }
162
163 if ($everything) { // Implies is downloading.
164 // Overall report, then the analysis of each question.
165 $this->download_quiz_info_table($quizinfo);
166
167 if ($s) {
168 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
169
e68e4ccf
JP
170 if ($this->table->is_downloading() == 'xhtml' && $s != 0) {
171 $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
869309b8 172 }
04853f27
TH
173
174 foreach ($questions as $question) {
25a03faa
TH
175 if (question_bank::get_qtype(
176 $question->qtype, false)->can_analyse_responses()) {
04853f27 177 $this->output_individual_question_response_analysis(
e68e4ccf 178 $question, $reporturl, $qubaids);
04853f27
TH
179
180 } else if (!empty($question->_stats->subquestions)) {
181 $subitemstodisplay = explode(',', $question->_stats->subquestions);
182 foreach ($subitemstodisplay as $subitemid) {
183 $this->output_individual_question_response_analysis(
e68e4ccf 184 $subquestions[$subitemid], $reporturl, $qubaids);
869309b8 185 }
186 }
869309b8 187 }
188 }
04853f27
TH
189
190 $this->table->export_class_instance()->finish_document();
191
192 } else if ($slot) {
193 // Report on an individual question indexed by position.
194 if (!isset($questions[$slot])) {
195 print_error('questiondoesnotexist', 'question');
869309b8 196 }
04853f27
TH
197
198 $this->output_individual_question_data($quiz, $questions[$slot]);
199 $this->output_individual_question_response_analysis(
e68e4ccf 200 $questions[$slot], $reporturl, $qubaids);
04853f27
TH
201
202 // Back to overview link.
203 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
204 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
2f1ba2ae 205 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
04853f27
TH
206
207 } else if ($qid) {
208 // Report on an individual sub-question indexed questionid.
209 if (!isset($subquestions[$qid])) {
43ec99aa 210 print_error('questiondoesnotexist', 'question');
71a2b878 211 }
04853f27
TH
212
213 $this->output_individual_question_data($quiz, $subquestions[$qid]);
214 $this->output_individual_question_response_analysis(
e68e4ccf 215 $subquestions[$qid], $reporturl, $qubaids);
04853f27
TH
216
217 // Back to overview link.
218 echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
219 get_string('backtoquizreport', 'quiz_statistics') . '</a>',
220 'boxaligncenter generalbox boxwidthnormal mdl-align');
221
222 } else if ($this->table->is_downloading()) {
223 // Downloading overview report.
224 $this->download_quiz_info_table($quizinfo);
225 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
226 $this->table->finish_output();
227
228 } else {
229 // On-screen display of overview report.
230 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
25a03faa
TH
231 echo $this->output_caching_info($quizstats, $quiz->id, $currentgroup,
232 $groupstudents, $useallattempts, $reporturl);
04853f27
TH
233 echo $this->everything_download_options();
234 echo $this->output_quiz_info_table($quizinfo);
235 if ($s) {
236 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
237 $this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
e68e4ccf 238 $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
b0e4fa41 239 }
43ec99aa 240 }
04853f27 241
43ec99aa 242 return true;
243 }
3b1d5cc4 244
04853f27
TH
245 /**
246 * Display the statistical and introductory information about a question.
247 * Only called when not downloading.
248 * @param object $quiz the quiz settings.
249 * @param object $question the question to report on.
250 * @param moodle_url $reporturl the URL to resisplay this report.
251 * @param object $quizstats Holds the quiz statistics.
252 */
253 protected function output_individual_question_data($quiz, $question) {
254 global $OUTPUT;
255
256 // On-screen display. Show a summary of the question's place in the quiz,
257 // and the question statistics.
258 $datumfromtable = $this->table->format_row($question);
259
260 // Set up the question info table.
261 $questioninfotable = new html_table();
262 $questioninfotable->align = array('center', 'center');
263 $questioninfotable->width = '60%';
264 $questioninfotable->attributes['class'] = 'generaltable titlesleft';
265
266 $questioninfotable->data = array();
267 $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
268 $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
269 $question->name.'&nbsp;'.$datumfromtable['actions']);
270 $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
271 $datumfromtable['icon'] . '&nbsp;' .
272 question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
273 $datumfromtable['icon']);
274 $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
275 $question->_stats->positions);
276
277 // Set up the question statistics table.
278 $questionstatstable = new html_table();
279 $questionstatstable->align = array('center', 'center');
280 $questionstatstable->width = '60%';
281 $questionstatstable->attributes['class'] = 'generaltable titlesleft';
282
283 unset($datumfromtable['number']);
284 unset($datumfromtable['icon']);
285 $actions = $datumfromtable['actions'];
286 unset($datumfromtable['actions']);
287 unset($datumfromtable['name']);
25a03faa
TH
288 $labels = array(
289 's' => get_string('attempts', 'quiz_statistics'),
290 'facility' => get_string('facility', 'quiz_statistics'),
291 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
292 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
293 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
294 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
295 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
296 'discriminative_efficiency' =>
297 get_string('discriminative_efficiency', 'quiz_statistics')
298 );
04853f27
TH
299 foreach ($datumfromtable as $item => $value) {
300 $questionstatstable->data[] = array($labels[$item], $value);
869309b8 301 }
04853f27
TH
302
303 // Display the various bits.
304 echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
305 echo html_writer::table($questioninfotable);
edfa0d80 306 echo $this->render_question_text($question);
04853f27
TH
307 echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
308 echo html_writer::table($questionstatstable);
869309b8 309 }
3b1d5cc4 310
edfa0d80
TH
311 /**
312 * @param object $question question data.
313 * @return string HTML of question text, ready for display.
314 */
fdb5bc03 315 protected function render_question_text($question) {
edfa0d80 316 global $OUTPUT;
fdb5bc03 317
68d2f6a0
TH
318 $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
319 $question->contextid, 'question', 'questiontext', $question->id,
320 $this->context->id, 'quiz_statistics');
fdb5bc03
TH
321
322 return $OUTPUT->box(format_text($text, $question->questiontextformat,
323 array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
edfa0d80
TH
324 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
325 }
326
04853f27
TH
327 /**
328 * Display the response analysis for a question.
e68e4ccf 329 * @param object $question the question to report on.
04853f27 330 * @param moodle_url $reporturl the URL to resisplay this report.
e68e4ccf 331 * @param qubaid_condition $qubaids
04853f27 332 */
25a03faa 333 protected function output_individual_question_response_analysis($question,
e68e4ccf 334 $reporturl, $qubaids) {
04853f27 335 global $OUTPUT;
3b1d5cc4 336
04853f27
TH
337 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
338 return;
339 }
3b1d5cc4 340
59ea8176 341 $qtable = new quiz_statistics_question_table($question->id);
04853f27
TH
342 $exportclass = $this->table->export_class_instance();
343 $qtable->export_class_instance($exportclass);
344 if (!$this->table->is_downloading()) {
345 // Output an appropriate title.
346 echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'));
3b1d5cc4 347
869309b8 348 } else {
04853f27
TH
349 // Work out an appropriate title.
350 $questiontabletitle = '"' . $question->name . '"';
351 if (!empty($question->number)) {
352 $questiontabletitle = '(' . $question->number . ') ' . $questiontabletitle;
353 }
354 if ($this->table->is_downloading() == 'xhtml') {
25a03faa
TH
355 $questiontabletitle = get_string('analysisofresponsesfor',
356 'quiz_statistics', $questiontabletitle);
869309b8 357 }
04853f27
TH
358
359 // Set up the table.
869309b8 360 $exportclass->start_table($questiontabletitle);
edfa0d80
TH
361
362 if ($this->table->is_downloading() == 'xhtml') {
363 echo $this->render_question_text($question);
364 }
869309b8 365 }
3b1d5cc4 366
e68e4ccf
JP
367 $responesstats = new question_response_analyser($question);
368 $responesstats->load_cached($qubaids);
04853f27 369
90fbad64 370 $qtable->question_setup($reporturl, $question, $responesstats);
04853f27
TH
371 if ($this->table->is_downloading()) {
372 $exportclass->output_headers($qtable->headers);
373 }
374
375 foreach ($responesstats->responseclasses as $partid => $partclasses) {
0ff4bd08 376 $rowdata = new stdClass();
04853f27
TH
377 $rowdata->part = $partid;
378 foreach ($partclasses as $responseclassid => $responseclass) {
379 $rowdata->responseclass = $responseclass->responseclass;
380
381 $responsesdata = $responesstats->responses[$partid][$responseclassid];
382 if (empty($responsesdata)) {
383 if (!array_key_exists('responseclass', $qtable->columns)) {
384 $rowdata->response = $responseclass->responseclass;
869309b8 385 } else {
04853f27 386 $rowdata->response = '';
869309b8 387 }
04853f27
TH
388 $rowdata->fraction = $responseclass->fraction;
389 $rowdata->count = 0;
390 $qtable->add_data_keyed($qtable->format_row($rowdata));
391 continue;
392 }
393
394 foreach ($responsesdata as $response => $data) {
395 $rowdata->response = $response;
396 $rowdata->fraction = $data->fraction;
397 $rowdata->count = $data->count;
398 $qtable->add_data_keyed($qtable->format_row($rowdata));
869309b8 399 }
869309b8 400 }
43ec99aa 401 }
04853f27
TH
402
403 $qtable->finish_output(!$this->table->is_downloading());
869309b8 404 }
3b1d5cc4 405
04853f27
TH
406 /**
407 * Output the table that lists all the questions in the quiz with their statistics.
f7970e3c 408 * @param int $s number of attempts.
04853f27
TH
409 * @param array $questions the questions in the quiz.
410 * @param array $subquestions the subquestions of any random questions.
411 */
412 protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
413 if (!$s) {
414 return;
415 }
416
417 foreach ($questions as $question) {
418 // Output the data for this questions.
419 $this->table->add_data_keyed($this->table->format_row($question));
420
421 if (empty($question->_stats->subquestions)) {
422 continue;
71a2b878 423 }
43ec99aa 424
04853f27
TH
425 // And its subquestions, if it has any.
426 $subitemstodisplay = explode(',', $question->_stats->subquestions);
427 foreach ($subitemstodisplay as $subitemid) {
428 $subquestions[$subitemid]->maxmark = $question->maxmark;
429 $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
430 }
0c1c764e 431 }
04853f27
TH
432
433 $this->table->finish_output(!$this->table->is_downloading());
43ec99aa 434 }
3b1d5cc4 435
04853f27
TH
436 protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
437
438 // You can edit this array to control which statistics are displayed.
439 $todisplay = array('firstattemptscount' => 'number',
440 'allattemptscount' => 'number',
441 'firstattemptsavg' => 'summarks_as_percentage',
442 'allattemptsavg' => 'summarks_as_percentage',
443 'median' => 'summarks_as_percentage',
444 'standarddeviation' => 'summarks_as_percentage',
445 'skewness' => 'number_format',
446 'kurtosis' => 'number_format',
447 'cic' => 'number_format_percent',
448 'errorratio' => 'number_format_percent',
449 'standarderror' => 'summarks_as_percentage');
450
451 // General information about the quiz.
452 $quizinfo = array();
453 $quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
454 $quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
455 if ($cm->idnumber) {
456 $quizinfo[get_string('idnumbermod')] = $cm->idnumber;
457 }
458 if ($quiz->timeopen) {
459 $quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
460 }
461 if ($quiz->timeclose) {
462 $quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
463 }
464 if ($quiz->timeopen && $quiz->timeclose) {
25a03faa
TH
465 $quizinfo[get_string('duration', 'quiz_statistics')] =
466 format_time($quiz->timeclose - $quiz->timeopen);
04853f27
TH
467 }
468
469 // The statistics.
470 foreach ($todisplay as $property => $format) {
9413953f 471 if (!isset($quizstats->$property) || !$format) {
43ec99aa 472 continue;
71a2b878 473 }
04853f27
TH
474 $value = $quizstats->$property;
475
476 switch ($format) {
477 case 'summarks_as_percentage':
478 $formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
479 break;
480 case 'number_format_percent':
481 $formattedvalue = quiz_format_grade($quiz, $value) . '%';
482 break;
483 case 'number_format':
768a7588 484 // 2 extra decimal places, since not a percentage,
25a03faa 485 // and we want the same number of sig figs.
04853f27
TH
486 $formattedvalue = format_float($value, $quiz->decimalpoints + 2);
487 break;
488 case 'number':
489 $formattedvalue = $value + 0;
490 break;
491 default:
492 $formattedvalue = $value;
71a2b878 493 }
04853f27
TH
494
495 $quizinfo[get_string($property, 'quiz_statistics',
496 $this->using_attempts_string(!empty($quizstats->allattempts)))] =
497 $formattedvalue;
43ec99aa 498 }
04853f27
TH
499
500 return $quizinfo;
501 }
502
503 /**
504 * Output the table of overall quiz statistics.
505 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
506 * @return string the HTML.
507 */
508 protected function output_quiz_info_table($quizinfo) {
509
510 $quizinfotable = new html_table();
511 $quizinfotable->align = array('center', 'center');
512 $quizinfotable->width = '60%';
513 $quizinfotable->attributes['class'] = 'generaltable titlesleft';
514 $quizinfotable->data = array();
515
516 foreach ($quizinfo as $heading => $value) {
517 $quizinfotable->data[] = array($heading, $value);
71a2b878 518 }
04853f27
TH
519
520 return html_writer::table($quizinfotable);
71a2b878 521 }
43ec99aa 522
04853f27
TH
523 /**
524 * Download the table of overall quiz statistics.
525 * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
526 */
527 protected function download_quiz_info_table($quizinfo) {
528 global $OUTPUT;
3b1d5cc4 529
04853f27
TH
530 // XHTML download is a special case.
531 if ($this->table->is_downloading() == 'xhtml') {
532 echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'));
533 echo $this->output_quiz_info_table($quizinfo);
534 return;
535 }
3b1d5cc4 536
04853f27
TH
537 // Reformat the data ready for output.
538 $headers = array();
539 $row = array();
540 foreach ($quizinfo as $heading => $value) {
541 $headers[] = $heading;
542 $row[] = $value;
543 }
3b1d5cc4 544
04853f27
TH
545 // Do the output.
546 $exportclass = $this->table->export_class_instance();
547 $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
548 $exportclass->output_headers($headers);
549 $exportclass->add_data($row);
550 $exportclass->finish_table();
551 }
552
553 /**
554 * Output the HTML needed to show the statistics graph.
e68e4ccf
JP
555 * @param $quizid
556 * @param $currentgroup
557 * @param $useallattempts
04853f27 558 */
e68e4ccf 559 protected function output_statistics_graph($quizid, $currentgroup, $useallattempts) {
2cdcb905 560 global $PAGE;
04853f27 561
2cdcb905 562 $output = $PAGE->get_renderer('mod_quiz');
04853f27 563 $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
e68e4ccf 564 compact('quizid', 'currentgroup', 'useallattempts'));
2cdcb905
TH
565 $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
566 echo $output->graph($imageurl, $graphname);
04853f27
TH
567 }
568
569 /**
570 * Return the stats data for when there are no stats to show.
571 *
f7970e3c 572 * @param int $firstattemptscount number of first attempts (optional).
e68e4ccf
JP
573 * @param int $allattemptscount total number of attempts (optional).
574 * @return array with two elements:
04853f27 575 * - integer $s Number of attempts included in the stats (0).
e68e4ccf 576 * - object $quizstats The statistics for overall attempt scores.
04853f27 577 */
e68e4ccf 578 protected function get_empty_stats($firstattemptscount = 0, $allattemptscount = 0) {
0ff4bd08 579 $quizstats = new stdClass();
04853f27
TH
580 $quizstats->firstattemptscount = $firstattemptscount;
581 $quizstats->allattemptscount = $allattemptscount;
582
e68e4ccf 583 return array(0, $quizstats);
04853f27
TH
584 }
585
586 /**
587 * Compute the quiz statistics.
588 *
e68e4ccf
JP
589 * @param int $quizid the quiz id.
590 * @param int $currentgroup the current group. 0 for none.
591 * @param bool $useallattempts use all attempts, or just first attempts.
592 * @param array $groupstudents students in this group.
593 * @param int $p number of positions (slots).
594 * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
595 * @return array with two elements:
04853f27 596 * - integer $s Number of attempts included in the stats.
e68e4ccf 597 * - object $quizstats The statistics for overall attempt scores.
04853f27 598 */
e68e4ccf 599 protected function calculate_quiz_stats($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
04853f27
TH
600 global $DB;
601
602 // Calculating MEAN of marks for all attempts by students
728ebac7 603 // http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
768a7588 604 // #Calculating_MEAN_of_grades_for_all_attempts_by_students.
04853f27
TH
605 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
606 $quizid, $currentgroup, $groupstudents, true);
607
608 $attempttotals = $DB->get_records_sql("
609 SELECT
610 CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
611 COUNT(1) AS countrecs,
612 SUM(sumgrades) AS total
613 FROM $fromqa
614 WHERE $whereqa
1ccfd8dc 615 GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
04853f27
TH
616
617 if (!$attempttotals) {
e68e4ccf 618 return $this->get_empty_stats();
04853f27
TH
619 }
620
621 if (isset($attempttotals[1])) {
622 $firstattempts = $attempttotals[1];
623 $firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
0c1c764e 624 } else {
0ff4bd08 625 $firstattempts = new stdClass();
04853f27
TH
626 $firstattempts->countrecs = 0;
627 $firstattempts->total = 0;
41d5cbc1 628 $firstattempts->average = null;
0c1c764e 629 }
04853f27 630
0ff4bd08 631 $allattempts = new stdClass();
04853f27
TH
632 if (isset($attempttotals[0])) {
633 $allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
634 $allattempts->total = $firstattempts->total + $attempttotals[0]->total;
71a2b878 635 } else {
04853f27
TH
636 $allattempts->countrecs = $firstattempts->countrecs;
637 $allattempts->total = $firstattempts->total;
0c1c764e 638 }
3b1d5cc4 639
04853f27
TH
640 if ($useallattempts) {
641 $usingattempts = $allattempts;
642 $usingattempts->sql = '';
643 } else {
644 $usingattempts = $firstattempts;
645 $usingattempts->sql = 'AND quiza.attempt = 1 ';
646 }
e72efdd4 647
04853f27
TH
648 $s = $usingattempts->countrecs;
649 if ($s == 0) {
e68e4ccf 650 return $this->get_empty_stats($firstattempts->countrecs, $allattempts->countrecs);
04853f27 651 }
3b1d5cc4 652
0ff4bd08 653 $quizstats = new stdClass();
04853f27
TH
654 $quizstats->allattempts = $useallattempts;
655 $quizstats->firstattemptscount = $firstattempts->countrecs;
656 $quizstats->allattemptscount = $allattempts->countrecs;
657 $quizstats->firstattemptsavg = $firstattempts->average;
658 $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
3b1d5cc4 659
04853f27
TH
660 // Recalculate sql again this time possibly including test for first attempt.
661 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
662 $quizid, $currentgroup, $groupstudents, $useallattempts);
6f51ed72 663
768a7588 664 // Median ...
04853f27 665 if ($s % 2 == 0) {
768a7588 666 // An even number of attempts.
04853f27
TH
667 $limitoffset = $s/2 - 1;
668 $limit = 2;
669 } else {
670 $limitoffset = floor($s/2);
671 $limit = 1;
672 }
673 $sql = "SELECT id, sumgrades
674 FROM $fromqa
675 WHERE $whereqa
676 ORDER BY sumgrades";
3b1d5cc4 677
04853f27 678 $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
3b1d5cc4 679
04853f27
TH
680 $quizstats->median = array_sum($medianmarks) / count($medianmarks);
681 if ($s > 1) {
768a7588
TH
682 // Fetch the sum of squared, cubed and power 4d
683 // differences between marks and mean mark.
04853f27 684 $mean = $usingattempts->total / $s;
0ff4bd08 685 $sql = "SELECT
25a03faa
TH
686 SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
687 SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
688 SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
04853f27
TH
689 FROM $fromqa
690 WHERE $whereqa";
691 $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
3b1d5cc4 692
04853f27 693 $powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
3b1d5cc4 694
768a7588 695 // Standard_Deviation:
728ebac7 696 // see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
768a7588 697 // #Standard_Deviation.
3b1d5cc4 698
04853f27
TH
699 $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
700
768a7588 701 // Skewness.
04853f27 702 if ($s > 2) {
768a7588
TH
703 // See http://docs.moodle.org/dev/
704 // Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
04853f27
TH
705 $m2= $powers->power2 / $s;
706 $m3= $powers->power3 / $s;
707 $m4= $powers->power4 / $s;
708
709 $k2= $s*$m2/($s-1);
710 $k3= $s*$s*$m3/(($s-1)*($s-2));
711 if ($k2) {
712 $quizstats->skewness = $k3 / (pow($k2, 3/2));
6f51ed72 713 }
04853f27 714
e68e4ccf
JP
715 // Kurtosis.
716 if ($s > 3) {
717 $k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
718 if ($k2) {
719 $quizstats->kurtosis = $k4 / ($k2*$k2);
720 }
43ec99aa 721 }
4f5ffac0 722 }
08a7ead5 723 }
04853f27 724
04853f27 725 if ($s > 1) {
04853f27 726 if ($p > 1 && isset($k2)) {
25a03faa 727 $quizstats->cic = (100 * $p / ($p -1)) *
e68e4ccf 728 (1 - ($sumofmarkvariance / $k2));
25a03faa
TH
729 $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
730 $quizstats->standarderror = $quizstats->errorratio *
731 $quizstats->standarddeviation / 100;
04853f27
TH
732 }
733 }
734
e68e4ccf
JP
735 $this->cache_stats(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts), $quizstats);
736
737 return array($s, $quizstats);
0c1c764e 738 }
3b1d5cc4 739
04853f27
TH
740 /**
741 * Load the cached statistics from the database.
742 *
e68e4ccf
JP
743 * @param $qubaids qubaid_condition
744 * @return The statistics for overall attempt scores or false if not cached.
04853f27 745 */
e68e4ccf 746 protected function get_cached_quiz_stats($qubaids) {
43ec99aa 747 global $DB;
04853f27
TH
748
749 $timemodified = time() - self::TIME_TO_CACHE_STATS;
e68e4ccf
JP
750 return $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
751 array($qubaids->get_hash_code(), $timemodified));
04853f27
TH
752 }
753
754 /**
e68e4ccf
JP
755 * @param $qubaids qubaid_condition
756 * @param $quizstats object the quiz stats to cache
04853f27 757 */
e68e4ccf 758 protected function cache_stats($qubaids, $quizstats) {
04853f27
TH
759 global $DB;
760
761 $toinsert = clone($quizstats);
e68e4ccf 762 $toinsert->hashcode = $qubaids->get_hash_code();
04853f27
TH
763 $toinsert->timemodified = time();
764
765 // Fix up some dodgy data.
766 if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
767 $toinsert->errorratio = null;
768 }
769 if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
770 $toinsert->standarderror = null;
771 }
772
773 // Store the data.
e68e4ccf 774 $DB->insert_record('quiz_statistics', $toinsert);
04853f27 775
04853f27
TH
776 }
777
778 /**
779 * Get the quiz and question statistics, either by loading the cached results,
780 * or by recomputing them.
781 *
782 * @param object $quiz the quiz settings.
f7970e3c
TH
783 * @param int $currentgroup the current group. 0 for none.
784 * @param bool $nostudentsingroup true if there a no students.
785 * @param bool $useallattempts use all attempts, or just first attempts.
04853f27
TH
786 * @param array $groupstudents students in this group.
787 * @param array $questions question definitions.
788 * @return array with 4 elements:
789 * - $quizstats The statistics for overall attempt scores.
790 * - $questions The questions, with an additional _stats field.
791 * - $subquestions The subquestions, if any, with an additional _stats field.
792 * - $s Number of attempts included in the stats.
793 */
794 protected function get_quiz_and_questions_stats($quiz, $currentgroup,
795 $nostudentsingroup, $useallattempts, $groupstudents, $questions) {
796
e68e4ccf 797 $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
04853f27 798
e68e4ccf 799 $quizstats = $this->get_cached_quiz_stats($qubaids);
04853f27 800
e68e4ccf 801 $qstats = new question_statistics($questions);
04853f27 802
e68e4ccf
JP
803 if (empty($quizstats)) {
804 // Recalculate now.
805 $qstats->calculate($qubaids);
04853f27 806
e68e4ccf
JP
807 if ($nostudentsingroup) {
808 list($s, $quizstats) = $this->get_empty_stats();
809 } else {
810 list($s, $quizstats) = $this->calculate_quiz_stats($quiz->id, $currentgroup, $useallattempts,
811 $groupstudents, count($questions), $qstats->get_sum_of_mark_variance());
43ec99aa 812 }
e68e4ccf
JP
813
814 $questions = $qstats->questions;
815 $subquestions = $qstats->subquestions;
816
817 if ($s) {
818 $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions);
819 }
820 } else {
821 if ($useallattempts) {
822 $s = $quizstats->allattemptscount;
823 } else {
824 $s = $quizstats->firstattemptscount;
825 }
826 $qstats->get_cached($qubaids);
827 $questions = $qstats->questions;
828 $subquestions = $qstats->subquestions;
829
04853f27
TH
830 }
831
832 return array($quizstats, $questions, $subquestions, $s);
833 }
834
e68e4ccf 835 protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions) {
04853f27
TH
836
837 $done = array();
838 foreach ($questions as $question) {
839 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
840 continue;
43ec99aa 841 }
04853f27
TH
842 $done[$question->id] = 1;
843
e68e4ccf
JP
844 $responesstats = new question_response_analyser($question);
845 $responesstats->calculate($qubaids);
04853f27
TH
846 }
847
848 foreach ($subquestions as $question) {
849 if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
850 isset($done[$question->id])) {
851 continue;
43ec99aa 852 }
04853f27
TH
853 $done[$question->id] = 1;
854
e68e4ccf
JP
855 $responesstats = new question_response_analyser($question);
856 $responesstats->calculate($qubaids);
04853f27
TH
857 }
858 }
859
860 /**
861 * @return string HTML snipped for the Download full report as UI.
862 */
863 protected function everything_download_options() {
864 $downloadoptions = $this->table->get_download_menu();
865
0465ef6e
RT
866 $downloadelements = new stdClass();
867 $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
868 $this->table->defaultdownloadformat, false);
869 $downloadelements->downloadbutton = '<input type="submit" value="' .
870 get_string('download') . '"/>';
871
04853f27
TH
872 $output = '<form action="'. $this->table->baseurl .'" method="post">';
873 $output .= '<div class="mdl-align">';
874 $output .= '<input type="hidden" name="everything" value="1"/>';
0465ef6e 875 $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
04853f27
TH
876 $output .= '</div></form>';
877
878 return $output;
879 }
880
881 /**
882 * Generate the snipped of HTML that says when the stats were last caculated,
883 * with a recalcuate now button.
e68e4ccf
JP
884 * @param object $quizstats the overall quiz statistics.
885 * @param int $quizid the quiz id.
886 * @param int $currentgroup the id of the currently selected group, or 0.
887 * @param array $groupstudents ids of students in the group.
888 * @param bool $useallattempts whether to use all attempts, instead of just
889 * first attempts.
890 * @param moodle_url $reporturl url for this report
25a03faa
TH
891 * @return string a HTML snipped saying when the stats were last computed,
892 * or blank if that is not appropriate.
04853f27
TH
893 */
894 protected function output_caching_info($quizstats, $quizid, $currentgroup,
895 $groupstudents, $useallattempts, $reporturl) {
896 global $DB, $OUTPUT;
897
898 if (empty($quizstats->timemodified)) {
899 return '';
900 }
901
902 // Find the number of attempts since the cached statistics were computed.
903 list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
904 $quizid, $currentgroup, $groupstudents, $useallattempts, true);
905 $count = $DB->count_records_sql("
906 SELECT COUNT(1)
907 FROM $fromqa
908 WHERE $whereqa
909 AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
910
911 if (!$count) {
912 $count = 0;
913 }
914
915 // Generate the output.
0ff4bd08 916 $a = new stdClass();
04853f27
TH
917 $a->lastcalculated = format_time(time() - $quizstats->timemodified);
918 $a->count = $count;
919
25a03faa
TH
920 $recalcualteurl = new moodle_url($reporturl,
921 array('recalculate' => 1, 'sesskey' => sesskey()));
04853f27 922 $output = '';
25a03faa
TH
923 $output .= $OUTPUT->box_start(
924 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
04853f27 925 $output .= get_string('lastcalculated', 'quiz_statistics', $a);
25a03faa
TH
926 $output .= $OUTPUT->single_button($recalcualteurl,
927 get_string('recalculatenow', 'quiz_statistics'));
04853f27
TH
928 $output .= $OUTPUT->box_end(true);
929
930 return $output;
931 }
932
933 /**
934 * Clear the cached data for a particular report configuration. This will
935 * trigger a re-computation the next time the report is displayed.
e68e4ccf 936 * @param $qubaids qubaid_condition
04853f27 937 */
e68e4ccf 938 protected function clear_cached_data($qubaids) {
04853f27 939 global $DB;
e68e4ccf
JP
940 $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
941 $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
942 $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
04853f27
TH
943 }
944
945 /**
f7970e3c 946 * @param bool $useallattempts whether we are using all attempts.
04853f27
TH
947 * @return the appropriate lang string to describe this option.
948 */
949 protected function using_attempts_string($useallattempts) {
950 if ($useallattempts) {
951 return get_string('allattempts', 'quiz_statistics');
952 } else {
953 return get_string('firstattempts', 'quiz_statistics');
43ec99aa 954 }
43ec99aa 955 }
3652dddd
JP
956
957 /**
958 * @param object $quiz the quiz.
959 * @return array of questions for this quiz.
960 */
961 public function load_and_initialise_questions_for_calculations($quiz) {
962 // Load the questions.
963 $questions = quiz_report_get_significant_questions($quiz);
964 $questionids = array();
965 foreach ($questions as $question) {
966 $questionids[] = $question->id;
967 }
968 $fullquestions = question_load_questions($questionids);
969 foreach ($questions as $qno => $question) {
970 $q = $fullquestions[$question->id];
971 $q->maxmark = $question->maxmark;
972 $q->slot = $qno;
973 $q->number = $question->number;
974 $questions[$qno] = $q;
975 }
976 return $questions;
977 }
0c1c764e 978}
04853f27 979