MDL-75576 quiz/question statistics: don't expire by time
[moodle.git] / question / classes / statistics / responses / analysis_for_question.php
blob4e1fd67033299b8a63ef8f248fff960545748b04
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.
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/>.
17 /**
18 * This file contains the code to analyse all the responses to a particular
19 * question.
21 * @package core_question
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
27 namespace core_question\statistics\responses;
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * Analysis for possible responses for parts of a question. It is up to a question type designer to decide on how many parts their
32 * question has. See {@link \question_type::get_possible_responses()} and sub classes where the sub parts and response classes are
33 * defined.
35 * A sub part might represent a sub question embedded in the question for example in a matching question there are
36 * several sub parts. A numeric question with a unit might be divided into two sub parts for the purposes of response analysis
37 * or the question type designer might decide to treat the answer, both the numeric and unit part,
38 * as a whole for the purposes of response analysis.
40 * - There is a separate data structure for each question or sub question's analysis
41 * {@link \core_question\statistics\responses\analysis_for_question}
42 * or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
43 * - There are separate analysis for each variant in this top level instance.
44 * - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
45 * {@link \core_question\statistics\responses\analysis_for_subpart}.
46 * - Then within the sub part analysis there are response class analysis
47 * {@link \core_question\statistics\responses\analysis_for_class}.
48 * - Then within each class analysis there are analysis for each actual response
49 * {@link \core_question\statistics\responses\analysis_for_actual_response}.
51 * @package core_question
52 * @copyright 2014 The Open University
53 * @author James Pratt me@jamiep.org
54 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
56 class analysis_for_question {
58 /**
59 * Constructor method.
61 * @param array[] Two index array, first index is unique string for each sub question part,
62 * the second string index is the 'class' that sub-question part can be classified into.
63 * Value in array is instance of {@link \question_possible_response}
64 * This is the return value from {@link \question_type::get_possible_responses()}
65 * see that method for fuller documentation.
67 public function __construct(array $possiblereponses = null) {
68 if ($possiblereponses !== null) {
69 $this->possibleresponses = $possiblereponses;
73 /**
74 * @var array[] See description above in constructor method.
76 protected $possibleresponses = array();
78 /**
79 * A multidimensional array whose first index is variant no and second index is subpart id, array contents are of type
80 * {@link analysis_for_subpart}.
82 * @var array[]
84 protected $subparts = array();
86 /**
87 * Initialise data structure for response analysis of one variant.
89 * @param int $variantno
91 protected function initialise_stats_for_variant($variantno) {
92 $this->subparts[$variantno] = array();
93 foreach ($this->possibleresponses as $subpartid => $classes) {
94 $this->subparts[$variantno][$subpartid] = new analysis_for_subpart($classes);
98 /**
99 * Variant nos found in this question's attempt data.
101 * @return int[]
103 public function get_variant_nos() {
104 return array_keys($this->subparts);
108 * Unique ids for sub parts.
110 * @param int $variantno
111 * @return string[]
113 public function get_subpart_ids($variantno) {
114 return array_keys($this->subparts[$variantno]);
118 * Get the response counts etc. for variant $variantno, question sub part $subpartid.
120 * Or if there is no recorded analysis yet then initialise the data structure for that part of the analysis and return the
121 * initialised analysis objects.
123 * @param int $variantno
124 * @param string $subpartid id for sub part.
125 * @return analysis_for_subpart
127 public function get_analysis_for_subpart($variantno, $subpartid) {
128 if (!isset($this->subparts[$variantno])) {
129 $this->initialise_stats_for_variant($variantno);
131 if (!isset($this->subparts[$variantno][$subpartid])) {
132 debugging('Unexpected sub-part id ' . $subpartid .
133 ' encountered.');
134 $this->subparts[$variantno][$subpartid] = new analysis_for_subpart();
136 return $this->subparts[$variantno][$subpartid];
140 * Used to work out what kind of table is needed to display stats.
142 * @return bool whether this question has (a subpart with) more than one response class.
144 public function has_multiple_response_classes() {
145 foreach ($this->get_variant_nos() as $variantno) {
146 foreach ($this->get_subpart_ids($variantno) as $subpartid) {
147 if ($this->get_analysis_for_subpart($variantno, $subpartid)->has_multiple_response_classes()) {
148 return true;
152 return false;
156 * Used to work out what kind of table is needed to display stats.
158 * @return bool whether this analysis has more than one subpart.
160 public function has_subparts() {
161 foreach ($this->get_variant_nos() as $variantno) {
162 if (count($this->get_subpart_ids($variantno)) > 1) {
163 return true;
166 return false;
170 * @return bool Does this response analysis include counts for responses for multiple tries of the question?
172 public function has_multiple_tries_data() {
173 return false;
177 * What is the highest number of tries at this question?
179 * @return int always 1 as this class is for analysing only one try.
181 public function get_maximum_tries() {
182 return 1;
187 * Takes an array of {@link \question_classified_response} and adds counts of the responses to the sub parts and classes.
189 * @param int $variantno
190 * @param \question_classified_response[] $responseparts keys are sub-part id.
192 public function count_response_parts($variantno, $responseparts) {
193 foreach ($responseparts as $subpartid => $responsepart) {
194 $this->get_analysis_for_subpart($variantno, $subpartid)->count_response($responsepart);
199 * Save the analysis to the DB, first cleaning up any old ones.
201 * @param \qubaid_condition $qubaids which question usages have been analysed.
202 * @param string $whichtries which tries have been analysed?
203 * @param int $questionid which question.
204 * @param int|null $calculationtime time when the analysis was done. (Defaults to time()).
206 public function cache($qubaids, $whichtries, $questionid, $calculationtime = null) {
207 global $DB;
209 $transaction = $DB->start_delegated_transaction();
211 $DB->delete_records_select('question_response_count',
212 'analysisid IN (
213 SELECT id
214 FROM {question_response_analysis}
215 WHERE hashcode= ? AND whichtries = ? AND questionid = ?
216 )', [$qubaids->get_hash_code(), $whichtries, $questionid]);
218 $DB->delete_records('question_response_analysis',
219 ['hashcode' => $qubaids->get_hash_code(), 'whichtries' => $whichtries, 'questionid' => $questionid]);
221 foreach ($this->get_variant_nos() as $variantno) {
222 foreach ($this->get_subpart_ids($variantno) as $subpartid) {
223 $analysisforsubpart = $this->get_analysis_for_subpart($variantno, $subpartid);
224 $analysisforsubpart->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $calculationtime);
228 $transaction->allow_commit();
232 * @return bool whether this analysis has a response class with more than one
233 * different actual response, or if the actual response is different from
234 * the model response.
236 public function has_actual_responses() {
237 foreach ($this->get_variant_nos() as $variantno) {
238 foreach ($this->get_subpart_ids($variantno) as $subpartid) {
239 if ($this->get_analysis_for_subpart($variantno, $subpartid)->has_actual_responses()) {
240 return true;
244 return false;