MDL-43479 quiz response analysis : suppress break down by variants
[moodle.git] / question / classes / statistics / responses / analyser.php
CommitLineData
04853f27 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
17/**
18 * This file contains the code to analyse all the responses to a particular
19 * question.
20 *
472c06f8 21 * @package core_question
e68e4ccf
JP
22 * @copyright 2013 Open University
23 * @author Jamie Pratt <me@jamiep.org>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
25 */
26
515b3ae6 27namespace core_question\statistics\responses;
a17b297d
TH
28defined('MOODLE_INTERNAL') || die();
29
04853f27
TH
30/**
31 * This class can store and compute the analysis of the responses to a particular
32 * question.
33 *
e68e4ccf
JP
34 * @copyright 2013 Open University
35 * @author Jamie Pratt <me@jamiep.org>
8d76124c 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 37 */
515b3ae6 38class analyser {
d50b05e6 39 /** @var object full question data from db. */
04853f27 40 protected $questiondata;
04853f27
TH
41
42 /**
d50b05e6 43 * @var analysis_for_question
04853f27 44 */
d50b05e6 45 public $analysis;
04853f27
TH
46
47 /**
4922e79f
JP
48 * @var array Two index array first index is unique string for each sub question part, the second string index is the 'class'
49 * that sub-question part can be classified into.
50 *
51 * This is the return value from {@link \question_type::get_possible_responses()} see that method for fuller documentation.
04853f27
TH
52 */
53 public $responseclasses = array();
54
3d6f2466
JP
55 /**
56 * @var bool whether to break down response analysis by variant. This only applies to questions that have variants and is
57 * used to suppress the break down of analysis by variant when there are going to be very many variants.
58 */
59 protected $breakdownbyvariant;
60
04853f27
TH
61 /**
62 * Create a new instance of this class for holding/computing the statistics
63 * for a particular question.
d50b05e6
JP
64 *
65 * @param object $questiondata the full question data from the database defining this question.
04853f27
TH
66 */
67 public function __construct($questiondata) {
68 $this->questiondata = $questiondata;
d50b05e6
JP
69 $qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
70 $this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
3d6f2466 71 $this->breakdownbyvariant = $qtypeobj->break_down_stats_and_response_analysis_by_variant($this->questiondata);
04853f27
TH
72 }
73
74 /**
f7970e3c 75 * @return bool whether this analysis has more than one subpart.
04853f27
TH
76 */
77 public function has_subparts() {
78 return count($this->responseclasses) > 1;
79 }
80
81 /**
d50b05e6 82 * @return bool whether this analysis has (a subpart with) more than one response class.
04853f27
TH
83 */
84 public function has_response_classes() {
85 foreach ($this->responseclasses as $partclasses) {
86 if (count($partclasses) > 1) {
87 return true;
88 }
89 }
90 return false;
91 }
92
93 /**
f7970e3c 94 * @return bool whether this analysis has a response class more than one
e5b0920e
TH
95 * different acutal response, or if the actual response is different from
96 * the model response.
04853f27
TH
97 */
98 public function has_actual_responses() {
99 foreach ($this->responseclasses as $subpartid => $partclasses) {
e5b0920e
TH
100 foreach ($partclasses as $responseclassid => $modelresponse) {
101 $numresponses = count($this->responses[$subpartid][$responseclassid]);
102 if ($numresponses > 1) {
103 return true;
104 }
105 $actualresponse = key($this->responses[$subpartid][$responseclassid]);
106 if ($numresponses == 1 && $actualresponse != $modelresponse->responseclass) {
04853f27
TH
107 return true;
108 }
109 }
110 }
111 return false;
112 }
113
114 /**
115 * Analyse all the response data for for all the specified attempts at
116 * this question.
515b3ae6 117 * @param \qubaid_condition $qubaids which attempts to consider.
d50b05e6 118 * @return analysis_for_question
04853f27 119 */
e68e4ccf 120 public function calculate($qubaids) {
04853f27 121 // Load data.
515b3ae6 122 $dm = new \question_engine_data_mapper();
04853f27
TH
123 $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
124
125 // Analyse it.
126 foreach ($questionattempts as $qa) {
d50b05e6 127 $responseparts = $qa->classify_response();
3d6f2466
JP
128 if ($this->breakdownbyvariant) {
129 $this->analysis->count_response_parts($qa->get_variant(), $responseparts);
130 } else {
131 $this->analysis->count_response_parts(1, $responseparts);
132 }
133
04853f27 134 }
d50b05e6
JP
135 $this->analysis->cache($qubaids, $this->questiondata->id);
136 return $this->analysis;
04853f27
TH
137 }
138
d50b05e6
JP
139 /** @var integer Time after which responses are automatically reanalysed. */
140 const TIME_TO_CACHE = 900; // 15 minutes.
04853f27 141
04853f27
TH
142
143 /**
d50b05e6
JP
144 * Retrieve the computed response analysis from the question_response_analysis table.
145 *
146 * @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
147 * @return analysis_for_question|boolean analysis or false if no cached analysis found.
04853f27 148 */
e68e4ccf 149 public function load_cached($qubaids) {
04853f27
TH
150 global $DB;
151
d50b05e6 152 $timemodified = time() - self::TIME_TO_CACHE;
e4b17111 153 $rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
d50b05e6 154 array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
04853f27
TH
155 if (!$rows) {
156 return false;
157 }
158
159 foreach ($rows as $row) {
4922e79f 160 $class = $this->analysis->get_analysis_for_subpart($row->variant, $row->subqid)->get_response_class($row->aid);
d50b05e6 161 $class->add_response_and_count($row->response, $row->credit, $row->rcount);
04853f27 162 }
d50b05e6 163 return $this->analysis;
04853f27
TH
164 }
165
d50b05e6 166
04853f27 167 /**
d50b05e6
JP
168 * Find time of non-expired analysis in the database.
169 *
170 * @param $qubaids \qubaid_condition
171 * @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
04853f27 172 */
d50b05e6 173 public function get_last_analysed_time($qubaids) {
04853f27
TH
174 global $DB;
175
d50b05e6 176 $timemodified = time() - self::TIME_TO_CACHE;
487e7e66
JP
177 return $DB->get_field_select('question_response_analysis', 'timemodified',
178 'hashcode = ? AND questionid = ? AND timemodified > ?',
e4b17111 179 array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified), IGNORE_MULTIPLE);
04853f27
TH
180 }
181}