PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/mod/quiz/report/statistics/report.php

https://github.com/bezborodow/moodle
PHP | 835 lines | 523 code | 109 blank | 203 comment | 91 complexity | 14b5a53a7a7112a428ca99a746957ad3 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1, GPL-3.0, Apache-2.0
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Quiz statistics report class.
  18. *
  19. * @package quiz_statistics
  20. * @copyright 2014 Open University
  21. * @author James Pratt <me@jamiep.org>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
  26. require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
  27. require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
  28. require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
  29. /**
  30. * The quiz statistics report provides summary information about each question in
  31. * a quiz, compared to the whole quiz. It also provides a drill-down to more
  32. * detailed information about each question.
  33. *
  34. * @copyright 2008 Jamie Pratt
  35. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36. */
  37. class quiz_statistics_report extends quiz_default_report {
  38. /** @var context_module context of this quiz.*/
  39. protected $context;
  40. /** @var quiz_statistics_table instance of table class used for main questions stats table. */
  41. protected $table;
  42. /** @var \core\progress\base|null $progress Handles progress reporting or not. */
  43. protected $progress = null;
  44. /**
  45. * Display the report.
  46. */
  47. public function display($quiz, $cm, $course) {
  48. global $OUTPUT;
  49. raise_memory_limit(MEMORY_HUGE);
  50. $this->context = context_module::instance($cm->id);
  51. if (!quiz_has_questions($quiz->id)) {
  52. $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
  53. echo quiz_no_questions_message($quiz, $cm, $this->context);
  54. return true;
  55. }
  56. // Work out the display options.
  57. $download = optional_param('download', '', PARAM_ALPHA);
  58. $everything = optional_param('everything', 0, PARAM_BOOL);
  59. $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
  60. // A qid paramter indicates we should display the detailed analysis of a sub question.
  61. $qid = optional_param('qid', 0, PARAM_INT);
  62. $slot = optional_param('slot', 0, PARAM_INT);
  63. $variantno = optional_param('variant', null, PARAM_INT);
  64. $whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
  65. $whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);
  66. $pageoptions = array();
  67. $pageoptions['id'] = $cm->id;
  68. $pageoptions['mode'] = 'statistics';
  69. $reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
  70. $mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));
  71. $mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries));
  72. if ($whichattempts != $quiz->grademethod) {
  73. $reporturl->param('whichattempts', $whichattempts);
  74. }
  75. if ($whichtries != question_attempt::LAST_TRY) {
  76. $reporturl->param('whichtries', $whichtries);
  77. }
  78. // Find out current groups mode.
  79. $currentgroup = $this->get_current_group($cm, $course, $this->context);
  80. $nostudentsingroup = false; // True if a group is selected and there is no one in it.
  81. if (empty($currentgroup)) {
  82. $currentgroup = 0;
  83. $groupstudents = array();
  84. } else if ($currentgroup == self::NO_GROUPS_ALLOWED) {
  85. $groupstudents = array();
  86. $nostudentsingroup = true;
  87. } else {
  88. // All users who can attempt quizzes and who are in the currently selected group.
  89. $groupstudents = get_users_by_capability($this->context,
  90. array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
  91. '', '', '', '', $currentgroup, '', false);
  92. if (!$groupstudents) {
  93. $nostudentsingroup = true;
  94. }
  95. }
  96. $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
  97. // If recalculate was requested, handle that.
  98. if ($recalculate && confirm_sesskey()) {
  99. $this->clear_cached_data($qubaids);
  100. redirect($reporturl);
  101. }
  102. // Set up the main table.
  103. $this->table = new quiz_statistics_table();
  104. if ($everything) {
  105. $report = get_string('completestatsfilename', 'quiz_statistics');
  106. } else {
  107. $report = get_string('questionstatsfilename', 'quiz_statistics');
  108. }
  109. $courseshortname = format_string($course->shortname, true,
  110. array('context' => context_course::instance($course->id)));
  111. $filename = quiz_report_download_filename($report, $courseshortname, $quiz->name);
  112. $this->table->is_downloading($download, $filename,
  113. get_string('quizstructureanalysis', 'quiz_statistics'));
  114. $questions = $this->load_and_initialise_questions_for_calculations($quiz);
  115. // Print the page header stuff (if not downloading.
  116. if (!$this->table->is_downloading()) {
  117. $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
  118. }
  119. if (!$nostudentsingroup) {
  120. // Get the data to be displayed.
  121. $progress = $this->get_progress_trace_instance();
  122. list($quizstats, $questionstats) =
  123. $this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress);
  124. } else {
  125. // Or create empty stats containers.
  126. $quizstats = new \quiz_statistics\calculated($whichattempts);
  127. $questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
  128. }
  129. // Set up the table, if there is data.
  130. if ($quizstats->s()) {
  131. $this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
  132. }
  133. // Print the rest of the page header stuff (if not downloading.
  134. if (!$this->table->is_downloading()) {
  135. if (groups_get_activity_groupmode($cm)) {
  136. groups_print_activity_menu($cm, $reporturl->out());
  137. if ($currentgroup && !$groupstudents) {
  138. $OUTPUT->notification(get_string('nostudentsingroup', 'quiz_statistics'));
  139. }
  140. }
  141. if (!$this->table->is_downloading() && $quizstats->s() == 0) {
  142. echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
  143. }
  144. foreach ($questionstats->any_error_messages() as $errormessage) {
  145. echo $OUTPUT->notification($errormessage);
  146. }
  147. // Print display options form.
  148. $mform->display();
  149. }
  150. if ($everything) { // Implies is downloading.
  151. // Overall report, then the analysis of each question.
  152. $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
  153. $this->download_quiz_info_table($quizinfo);
  154. if ($quizstats->s()) {
  155. $this->output_quiz_structure_analysis_table($questionstats);
  156. if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
  157. $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
  158. }
  159. $this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
  160. }
  161. $this->table->export_class_instance()->finish_document();
  162. } else if ($qid) {
  163. // Report on an individual sub-question indexed questionid.
  164. if (is_null($questionstats->for_subq($qid, $variantno))) {
  165. print_error('questiondoesnotexist', 'question');
  166. }
  167. $this->output_individual_question_data($quiz, $questionstats->for_subq($qid, $variantno));
  168. $this->output_individual_question_response_analysis($questionstats->for_subq($qid, $variantno)->question,
  169. $variantno,
  170. $questionstats->for_subq($qid, $variantno)->s,
  171. $reporturl,
  172. $qubaids,
  173. $whichtries);
  174. // Back to overview link.
  175. echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
  176. get_string('backtoquizreport', 'quiz_statistics') . '</a>',
  177. 'boxaligncenter generalbox boxwidthnormal mdl-align');
  178. } else if ($slot) {
  179. // Report on an individual question indexed by position.
  180. if (!isset($questions[$slot])) {
  181. print_error('questiondoesnotexist', 'question');
  182. }
  183. if ($variantno === null &&
  184. ($questionstats->for_slot($slot)->get_sub_question_ids()
  185. || $questionstats->for_slot($slot)->get_variants())) {
  186. if (!$this->table->is_downloading()) {
  187. $number = $questionstats->for_slot($slot)->question->number;
  188. echo $OUTPUT->heading(get_string('slotstructureanalysis', 'quiz_statistics', $number), 3);
  189. }
  190. $this->table->define_baseurl(new moodle_url($reporturl, array('slot' => $slot)));
  191. $this->table->format_and_add_array_of_rows($questionstats->structure_analysis_for_one_slot($slot));
  192. } else {
  193. $this->output_individual_question_data($quiz, $questionstats->for_slot($slot, $variantno));
  194. $this->output_individual_question_response_analysis($questions[$slot],
  195. $variantno,
  196. $questionstats->for_slot($slot, $variantno)->s,
  197. $reporturl,
  198. $qubaids,
  199. $whichtries);
  200. }
  201. if (!$this->table->is_downloading()) {
  202. // Back to overview link.
  203. echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
  204. get_string('backtoquizreport', 'quiz_statistics') . '</a>',
  205. 'backtomainstats boxaligncenter generalbox boxwidthnormal mdl-align');
  206. } else {
  207. $this->table->finish_output();
  208. }
  209. } else if ($this->table->is_downloading()) {
  210. // Downloading overview report.
  211. $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
  212. $this->download_quiz_info_table($quizinfo);
  213. if ($quizstats->s()) {
  214. $this->output_quiz_structure_analysis_table($questionstats);
  215. }
  216. $this->table->finish_output();
  217. } else {
  218. // On-screen display of overview report.
  219. echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
  220. echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudents, $whichattempts, $reporturl);
  221. echo $this->everything_download_options();
  222. $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
  223. echo $this->output_quiz_info_table($quizinfo);
  224. if ($quizstats->s()) {
  225. echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'), 3);
  226. $this->output_quiz_structure_analysis_table($questionstats);
  227. $this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
  228. }
  229. }
  230. return true;
  231. }
  232. /**
  233. * Display the statistical and introductory information about a question.
  234. * Only called when not downloading.
  235. *
  236. * @param object $quiz the quiz settings.
  237. * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
  238. */
  239. protected function output_individual_question_data($quiz, $questionstat) {
  240. global $OUTPUT;
  241. // On-screen display. Show a summary of the question's place in the quiz,
  242. // and the question statistics.
  243. $datumfromtable = $this->table->format_row($questionstat);
  244. // Set up the question info table.
  245. $questioninfotable = new html_table();
  246. $questioninfotable->align = array('center', 'center');
  247. $questioninfotable->width = '60%';
  248. $questioninfotable->attributes['class'] = 'generaltable titlesleft';
  249. $questioninfotable->data = array();
  250. $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
  251. $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
  252. $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
  253. if ($questionstat->variant !== null) {
  254. $questioninfotable->data[] = array(get_string('variant', 'quiz_statistics'), $questionstat->variant);
  255. }
  256. $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
  257. $datumfromtable['icon'] . '&nbsp;' .
  258. question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
  259. $datumfromtable['icon']);
  260. $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
  261. $questionstat->positions);
  262. // Set up the question statistics table.
  263. $questionstatstable = new html_table();
  264. $questionstatstable->align = array('center', 'center');
  265. $questionstatstable->width = '60%';
  266. $questionstatstable->attributes['class'] = 'generaltable titlesleft';
  267. unset($datumfromtable['number']);
  268. unset($datumfromtable['icon']);
  269. $actions = $datumfromtable['actions'];
  270. unset($datumfromtable['actions']);
  271. unset($datumfromtable['name']);
  272. $labels = array(
  273. 's' => get_string('attempts', 'quiz_statistics'),
  274. 'facility' => get_string('facility', 'quiz_statistics'),
  275. 'sd' => get_string('standarddeviationq', 'quiz_statistics'),
  276. 'random_guess_score' => get_string('random_guess_score', 'quiz_statistics'),
  277. 'intended_weight' => get_string('intended_weight', 'quiz_statistics'),
  278. 'effective_weight' => get_string('effective_weight', 'quiz_statistics'),
  279. 'discrimination_index' => get_string('discrimination_index', 'quiz_statistics'),
  280. 'discriminative_efficiency' =>
  281. get_string('discriminative_efficiency', 'quiz_statistics')
  282. );
  283. foreach ($datumfromtable as $item => $value) {
  284. $questionstatstable->data[] = array($labels[$item], $value);
  285. }
  286. // Display the various bits.
  287. echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'), 3);
  288. echo html_writer::table($questioninfotable);
  289. echo $this->render_question_text($questionstat->question);
  290. echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'), 3);
  291. echo html_writer::table($questionstatstable);
  292. }
  293. /**
  294. * Output question text in a box with urls appropriate for a preview of the question.
  295. *
  296. * @param object $question question data.
  297. * @return string HTML of question text, ready for display.
  298. */
  299. protected function render_question_text($question) {
  300. global $OUTPUT;
  301. $text = question_rewrite_question_preview_urls($question->questiontext, $question->id,
  302. $question->contextid, 'question', 'questiontext', $question->id,
  303. $this->context->id, 'quiz_statistics');
  304. return $OUTPUT->box(format_text($text, $question->questiontextformat,
  305. array('noclean' => true, 'para' => false, 'overflowdiv' => true)),
  306. 'questiontext boxaligncenter generalbox boxwidthnormal mdl-align');
  307. }
  308. /**
  309. * Display the response analysis for a question.
  310. *
  311. * @param object $question the question to report on.
  312. * @param int|null $variantno the variant
  313. * @param int $s
  314. * @param moodle_url $reporturl the URL to redisplay this report.
  315. * @param qubaid_condition $qubaids
  316. * @param string $whichtries
  317. */
  318. protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
  319. $whichtries = question_attempt::LAST_TRY) {
  320. global $OUTPUT;
  321. if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
  322. return;
  323. }
  324. $qtable = new quiz_statistics_question_table($question->id);
  325. $exportclass = $this->table->export_class_instance();
  326. $qtable->export_class_instance($exportclass);
  327. if (!$this->table->is_downloading()) {
  328. // Output an appropriate title.
  329. echo $OUTPUT->heading(get_string('analysisofresponses', 'quiz_statistics'), 3);
  330. } else {
  331. // Work out an appropriate title.
  332. $a = clone($question);
  333. $a->variant = $variantno;
  334. if (!empty($question->number) && !is_null($variantno)) {
  335. $questiontabletitle = get_string('analysisnovariant', 'quiz_statistics', $a);
  336. } else if (!empty($question->number)) {
  337. $questiontabletitle = get_string('analysisno', 'quiz_statistics', $a);
  338. } else if (!is_null($variantno)) {
  339. $questiontabletitle = get_string('analysisvariant', 'quiz_statistics', $a);
  340. } else {
  341. $questiontabletitle = get_string('analysisnameonly', 'quiz_statistics', $a);
  342. }
  343. if ($this->table->is_downloading() == 'xhtml') {
  344. $questiontabletitle = get_string('analysisofresponsesfor', 'quiz_statistics', $questiontabletitle);
  345. }
  346. // Set up the table.
  347. $exportclass->start_table($questiontabletitle);
  348. if ($this->table->is_downloading() == 'xhtml') {
  349. echo $this->render_question_text($question);
  350. }
  351. }
  352. $responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
  353. $responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
  354. $qtable->question_setup($reporturl, $question, $s, $responseanalysis);
  355. if ($this->table->is_downloading()) {
  356. $exportclass->output_headers($qtable->headers);
  357. }
  358. // Where no variant no is specified the variant no is actually one.
  359. if ($variantno === null) {
  360. $variantno = 1;
  361. }
  362. foreach ($responseanalysis->get_subpart_ids($variantno) as $partid) {
  363. $subpart = $responseanalysis->get_analysis_for_subpart($variantno, $partid);
  364. foreach ($subpart->get_response_class_ids() as $responseclassid) {
  365. $responseclass = $subpart->get_response_class($responseclassid);
  366. $tabledata = $responseclass->data_for_question_response_table($subpart->has_multiple_response_classes(), $partid);
  367. foreach ($tabledata as $row) {
  368. $qtable->add_data_keyed($qtable->format_row($row));
  369. }
  370. }
  371. }
  372. $qtable->finish_output(!$this->table->is_downloading());
  373. }
  374. /**
  375. * Output the table that lists all the questions in the quiz with their statistics.
  376. *
  377. * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
  378. * the quiz including subqs and
  379. * variants.
  380. */
  381. protected function output_quiz_structure_analysis_table($questionstats) {
  382. $tooutput = array();
  383. $limitvariants = !$this->table->is_downloading();
  384. foreach ($questionstats->get_all_slots() as $slot) {
  385. // Output the data for these question statistics.
  386. $tooutput = array_merge($tooutput, $questionstats->structure_analysis_for_one_slot($slot, $limitvariants));
  387. }
  388. $this->table->format_and_add_array_of_rows($tooutput);
  389. }
  390. /**
  391. * Return HTML for table of overall quiz statistics.
  392. *
  393. * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
  394. * @return string the HTML.
  395. */
  396. protected function output_quiz_info_table($quizinfo) {
  397. $quizinfotable = new html_table();
  398. $quizinfotable->align = array('center', 'center');
  399. $quizinfotable->width = '60%';
  400. $quizinfotable->attributes['class'] = 'generaltable titlesleft';
  401. $quizinfotable->data = array();
  402. foreach ($quizinfo as $heading => $value) {
  403. $quizinfotable->data[] = array($heading, $value);
  404. }
  405. return html_writer::table($quizinfotable);
  406. }
  407. /**
  408. * Download the table of overall quiz statistics.
  409. *
  410. * @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
  411. */
  412. protected function download_quiz_info_table($quizinfo) {
  413. global $OUTPUT;
  414. // XHTML download is a special case.
  415. if ($this->table->is_downloading() == 'xhtml') {
  416. echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
  417. echo $this->output_quiz_info_table($quizinfo);
  418. return;
  419. }
  420. // Reformat the data ready for output.
  421. $headers = array();
  422. $row = array();
  423. foreach ($quizinfo as $heading => $value) {
  424. $headers[] = $heading;
  425. $row[] = $value;
  426. }
  427. // Do the output.
  428. $exportclass = $this->table->export_class_instance();
  429. $exportclass->start_table(get_string('quizinformation', 'quiz_statistics'));
  430. $exportclass->output_headers($headers);
  431. $exportclass->add_data($row);
  432. $exportclass->finish_table();
  433. }
  434. /**
  435. * Output the HTML needed to show the statistics graph.
  436. *
  437. * @param $quizid
  438. * @param $currentgroup
  439. * @param $whichattempts
  440. */
  441. protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
  442. global $PAGE;
  443. $output = $PAGE->get_renderer('mod_quiz');
  444. $imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
  445. compact('quizid', 'currentgroup', 'whichattempts'));
  446. $graphname = get_string('statisticsreportgraph', 'quiz_statistics');
  447. echo $output->graph($imageurl, $graphname);
  448. }
  449. /**
  450. * Get the quiz and question statistics, either by loading the cached results,
  451. * or by recomputing them.
  452. *
  453. * @param object $quiz the quiz settings.
  454. * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
  455. * $quiz->grademethod ie.
  456. * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
  457. * we calculate stats based on which attempts would affect the grade for each student.
  458. * @param string $whichtries which tries to analyse for response analysis. Will be one of
  459. * question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
  460. * @param array $groupstudents students in this group.
  461. * @param array $questions full question data.
  462. * @param \core\progress\base|null $progress
  463. * @return array with 2 elements: - $quizstats The statistics for overall attempt scores.
  464. * - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
  465. */
  466. public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress = null) {
  467. if ($progress === null) {
  468. $progress = new \core\progress\null();
  469. }
  470. $qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
  471. $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
  472. $quizcalc = new \quiz_statistics\calculator($progress);
  473. $progress->start_progress('', 3);
  474. if ($quizcalc->get_last_calculated_time($qubaids) === false) {
  475. // Recalculate now.
  476. $questionstats = $qcalc->calculate($qubaids);
  477. $progress->progress(1);
  478. $quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
  479. $qcalc->get_sum_of_mark_variance());
  480. $progress->progress(2);
  481. } else {
  482. $quizstats = $quizcalc->get_cached($qubaids);
  483. $progress->progress(1);
  484. $questionstats = $qcalc->get_cached($qubaids);
  485. $progress->progress(2);
  486. }
  487. if ($quizstats->s()) {
  488. $subquestions = $questionstats->get_sub_questions();
  489. $this->analyse_responses_for_all_questions_and_subquestions($questions,
  490. $subquestions,
  491. $qubaids,
  492. $whichtries,
  493. $progress);
  494. }
  495. $progress->progress(3);
  496. $progress->end_progress();
  497. return array($quizstats, $questionstats);
  498. }
  499. /**
  500. * Appropriate instance depending if we want html output for the user or not.
  501. *
  502. * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
  503. */
  504. protected function get_progress_trace_instance() {
  505. if ($this->progress === null) {
  506. if (!$this->table->is_downloading()) {
  507. $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
  508. $this->progress->set_display_names();
  509. } else {
  510. $this->progress = new \core\progress\null();
  511. }
  512. }
  513. return $this->progress;
  514. }
  515. /**
  516. * Analyse responses for all questions and sub questions in this quiz.
  517. *
  518. * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
  519. * @param object[] $subquestions full question objects.
  520. * @param qubaid_condition $qubaids the question usages whose responses to analyse.
  521. * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
  522. * @param null|\core\progress\base $progress Used to indicate progress of task.
  523. */
  524. protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
  525. $whichtries, $progress = null) {
  526. if ($progress === null) {
  527. $progress = new \core\progress\null();
  528. }
  529. // Starting response analysis tasks.
  530. $progress->start_progress('', count($questions) + count($subquestions));
  531. $done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
  532. $this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
  533. // Finished all response analysis tasks.
  534. $progress->end_progress();
  535. }
  536. /**
  537. * Analyse responses for an array of questions or sub questions.
  538. *
  539. * @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations.
  540. * @param qubaid_condition $qubaids the question usages whose responses to analyse.
  541. * @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
  542. * @param null|\core\progress\base $progress Used to indicate progress of task.
  543. * @param int[] $done array keys are ids of questions that have been analysed before calling method.
  544. * @return array array keys are ids of questions that were analysed after this method call.
  545. */
  546. protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
  547. $countquestions = count($questions);
  548. if (!$countquestions) {
  549. return array();
  550. }
  551. if ($progress === null) {
  552. $progress = new \core\progress\null();
  553. }
  554. $progress->start_progress('', $countquestions, $countquestions);
  555. foreach ($questions as $question) {
  556. $progress->increment_progress();
  557. if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) {
  558. $responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
  559. if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
  560. $responesstats->calculate($qubaids, $whichtries);
  561. }
  562. }
  563. $done[$question->id] = 1;
  564. }
  565. $progress->end_progress();
  566. return $done;
  567. }
  568. /**
  569. * Return a little form for the user to request to download the full report, including quiz stats and response analysis for
  570. * all questions and sub-questions.
  571. *
  572. * @return string HTML.
  573. */
  574. protected function everything_download_options() {
  575. $downloadoptions = $this->table->get_download_menu();
  576. $downloadelements = new stdClass();
  577. $downloadelements->formatsmenu = html_writer::select($downloadoptions, 'download',
  578. $this->table->defaultdownloadformat, false);
  579. $downloadelements->downloadbutton = '<input type="submit" value="' .
  580. get_string('download') . '"/>';
  581. $output = '<form action="'. $this->table->baseurl .'" method="post">';
  582. $output .= '<div class="mdl-align">';
  583. $output .= '<input type="hidden" name="everything" value="1"/>';
  584. $output .= html_writer::tag('label', get_string('downloadeverything', 'quiz_statistics', $downloadelements));
  585. $output .= '</div></form>';
  586. return $output;
  587. }
  588. /**
  589. * Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
  590. *
  591. * @param int $lastcachetime the time the stats were last cached.
  592. * @param int $quizid the quiz id.
  593. * @param array $groupstudents ids of students in the group or empty array if groups not used.
  594. * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
  595. * $quiz->grademethod ie.
  596. * QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
  597. * we calculate stats based on which attempts would affect the grade for each student.
  598. * @param moodle_url $reporturl url for this report
  599. * @return string HTML.
  600. */
  601. protected function output_caching_info($lastcachetime, $quizid, $groupstudents, $whichattempts, $reporturl) {
  602. global $DB, $OUTPUT;
  603. if (empty($lastcachetime)) {
  604. return '';
  605. }
  606. // Find the number of attempts since the cached statistics were computed.
  607. list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts, true);
  608. $count = $DB->count_records_sql("
  609. SELECT COUNT(1)
  610. FROM $fromqa
  611. WHERE $whereqa
  612. AND quiza.timefinish > {$lastcachetime}", $qaparams);
  613. if (!$count) {
  614. $count = 0;
  615. }
  616. // Generate the output.
  617. $a = new stdClass();
  618. $a->lastcalculated = format_time(time() - $lastcachetime);
  619. $a->count = $count;
  620. $recalcualteurl = new moodle_url($reporturl,
  621. array('recalculate' => 1, 'sesskey' => sesskey()));
  622. $output = '';
  623. $output .= $OUTPUT->box_start(
  624. 'boxaligncenter generalbox boxwidthnormal mdl-align', 'cachingnotice');
  625. $output .= get_string('lastcalculated', 'quiz_statistics', $a);
  626. $output .= $OUTPUT->single_button($recalcualteurl,
  627. get_string('recalculatenow', 'quiz_statistics'));
  628. $output .= $OUTPUT->box_end(true);
  629. return $output;
  630. }
  631. /**
  632. * Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
  633. * is displayed.
  634. *
  635. * @param $qubaids qubaid_condition
  636. */
  637. protected function clear_cached_data($qubaids) {
  638. global $DB;
  639. $DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
  640. $DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
  641. $DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
  642. }
  643. /**
  644. * Load the questions in this quiz and add some properties to the objects needed in the reports.
  645. *
  646. * @param object $quiz the quiz.
  647. * @return array of questions for this quiz.
  648. */
  649. public function load_and_initialise_questions_for_calculations($quiz) {
  650. // Load the questions.
  651. $questions = quiz_report_get_significant_questions($quiz);
  652. $questionids = array();
  653. foreach ($questions as $question) {
  654. $questionids[] = $question->id;
  655. }
  656. $fullquestions = question_load_questions($questionids);
  657. foreach ($questions as $qno => $question) {
  658. $q = $fullquestions[$question->id];
  659. $q->maxmark = $question->maxmark;
  660. $q->slot = $qno;
  661. $q->number = $question->number;
  662. $questions[$qno] = $q;
  663. }
  664. return $questions;
  665. }
  666. /**
  667. * Output all response analysis for all questions, sub-questions and variants. For download in a number of formats.
  668. *
  669. * @param $qubaids
  670. * @param $questions
  671. * @param $questionstats
  672. * @param $reporturl
  673. * @param $whichtries string
  674. */
  675. protected function output_all_question_response_analysis($qubaids,
  676. $questions,
  677. $questionstats,
  678. $reporturl,
  679. $whichtries = question_attempt::LAST_TRY) {
  680. foreach ($questions as $slot => $question) {
  681. if (question_bank::get_qtype(
  682. $question->qtype, false)->can_analyse_responses()
  683. ) {
  684. if ($questionstats->for_slot($slot)->get_variants()) {
  685. foreach ($questionstats->for_slot($slot)->get_variants() as $variantno) {
  686. $this->output_individual_question_response_analysis($question,
  687. $variantno,
  688. $questionstats->for_slot($slot, $variantno)->s,
  689. $reporturl,
  690. $qubaids,
  691. $whichtries);
  692. }
  693. } else {
  694. $this->output_individual_question_response_analysis($question,
  695. null,
  696. $questionstats->for_slot($slot)->s,
  697. $reporturl,
  698. $qubaids,
  699. $whichtries);
  700. }
  701. } else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
  702. foreach ($subqids as $subqid) {
  703. if ($variants = $questionstats->for_subq($subqid)->get_variants()) {
  704. foreach ($variants as $variantno) {
  705. $this->output_individual_question_response_analysis(
  706. $questionstats->for_subq($subqid, $variantno)->question,
  707. $variantno,
  708. $questionstats->for_subq($subqid, $variantno)->s,
  709. $reporturl,
  710. $qubaids,
  711. $whichtries);
  712. }
  713. } else {
  714. $this->output_individual_question_response_analysis(
  715. $questionstats->for_subq($subqid)->question,
  716. null,
  717. $questionstats->for_subq($subqid)->s,
  718. $reporturl,
  719. $qubaids,
  720. $whichtries);
  721. }
  722. }
  723. }
  724. }
  725. }
  726. }