MDL-41888 More comments
[moodle.git] / question / engine / responseanalysis.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 *
e68e4ccf
JP
21 * @package core
22 * @subpackage questionbank
23 * @copyright 2013 Open University
24 * @author Jamie Pratt <me@jamiep.org>
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27
TH
26 */
27
28
a17b297d
TH
29defined('MOODLE_INTERNAL') || die();
30
31
04853f27
TH
32/**
33 * This class can store and compute the analysis of the responses to a particular
34 * question.
35 *
e68e4ccf
JP
36 * @copyright 2013 Open University
37 * @author Jamie Pratt <me@jamiep.org>
8d76124c 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 39 */
e68e4ccf 40class question_response_analyser {
04853f27
TH
41 /** @var object the data from the database that defines the question. */
42 protected $questiondata;
04853f27
TH
43
44 /**
45 * @var array This is a multi-dimensional array that stores the results of
46 * the analysis.
47 *
48 * The description of {@link question_type::get_possible_responses()} should
49 * help understand this description.
50 *
51 * $this->responses[$subpartid][$responseclassid][$response] is an
52 * object with two fields, ->count and ->fraction.
53 */
54 public $responses = array();
55
56 /**
0ff4bd08
TH
57 * @var array $this->fractions[$subpartid][$responseclassid] is an object
58 * with two fields, ->responseclass and ->fraction.
04853f27
TH
59 */
60 public $responseclasses = array();
61
62 /**
63 * Create a new instance of this class for holding/computing the statistics
64 * for a particular question.
65 * @param object $questiondata the data from the database defining this question.
66 */
67 public function __construct($questiondata) {
68 $this->questiondata = $questiondata;
69
55ca80ed
TH
70 $this->responseclasses =
71 question_bank::get_qtype($questiondata->qtype)->get_possible_responses(
72 $questiondata);
04853f27
TH
73 foreach ($this->responseclasses as $subpartid => $responseclasses) {
74 foreach ($responseclasses as $responseclassid => $notused) {
75 $this->responses[$subpartid][$responseclassid] = array();
76 }
77 }
78 }
79
80 /**
f7970e3c 81 * @return bool whether this analysis has more than one subpart.
04853f27
TH
82 */
83 public function has_subparts() {
84 return count($this->responseclasses) > 1;
85 }
86
87 /**
f7970e3c 88 * @return bool whether this analysis has (a subpart with) more than one
04853f27
TH
89 * response class.
90 */
91 public function has_response_classes() {
92 foreach ($this->responseclasses as $partclasses) {
93 if (count($partclasses) > 1) {
94 return true;
95 }
96 }
97 return false;
98 }
99
100 /**
f7970e3c 101 * @return bool whether this analysis has a response class more than one
e5b0920e
TH
102 * different acutal response, or if the actual response is different from
103 * the model response.
04853f27
TH
104 */
105 public function has_actual_responses() {
106 foreach ($this->responseclasses as $subpartid => $partclasses) {
e5b0920e
TH
107 foreach ($partclasses as $responseclassid => $modelresponse) {
108 $numresponses = count($this->responses[$subpartid][$responseclassid]);
109 if ($numresponses > 1) {
110 return true;
111 }
112 $actualresponse = key($this->responses[$subpartid][$responseclassid]);
113 if ($numresponses == 1 && $actualresponse != $modelresponse->responseclass) {
04853f27
TH
114 return true;
115 }
116 }
117 }
118 return false;
119 }
120
121 /**
122 * Analyse all the response data for for all the specified attempts at
123 * this question.
e68e4ccf 124 * @param qubaid_condition $qubaids which attempts to consider.
04853f27 125 */
e68e4ccf 126 public function calculate($qubaids) {
04853f27
TH
127 // Load data.
128 $dm = new question_engine_data_mapper();
129 $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
130
131 // Analyse it.
132 foreach ($questionattempts as $qa) {
133 $this->add_data_from_one_attempt($qa);
134 }
e68e4ccf 135 $this->store_cached($qubaids);
04853f27 136
04853f27
TH
137 }
138
139 /**
140 * Analyse the data from one question attempt.
141 * @param question_attempt $qa the data to analyse.
142 */
143 protected function add_data_from_one_attempt(question_attempt $qa) {
144 $blankresponse = question_classified_response::no_response();
145
146 $partresponses = $qa->classify_response();
147 foreach ($partresponses as $subpartid => $partresponse) {
55ca80ed
TH
148 if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
149 [$partresponse->response])) {
0ff4bd08 150 $resp = new stdClass();
04853f27
TH
151 $resp->count = 0;
152 if (!is_null($partresponse->fraction)) {
153 $resp->fraction = $partresponse->fraction;
154 } else {
155 $resp->fraction = $this->responseclasses[$subpartid]
156 [$partresponse->responseclassid]->fraction;
157 }
158
159 $this->responses[$subpartid][$partresponse->responseclassid]
160 [$partresponse->response] = $resp;
161 }
162
163 $this->responses[$subpartid][$partresponse->responseclassid]
164 [$partresponse->response]->count += 1;
165 }
166 }
167
168 /**
e68e4ccf
JP
169 * Store the computed response analysis in the question_response_analysis table.
170 * @param qubaid_condition $qubaids
04853f27 171 * data corresponding to.
e68e4ccf 172 * @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
04853f27 173 */
e68e4ccf 174 public function load_cached($qubaids) {
04853f27
TH
175 global $DB;
176
e68e4ccf
JP
177 $rows = $DB->get_records('question_response_analysis',
178 array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
04853f27
TH
179 if (!$rows) {
180 return false;
181 }
182
183 foreach ($rows as $row) {
25123418 184 $this->responses[$row->subqid][$row->aid][$row->response] = new stdClass();
04853f27
TH
185 $this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
186 $this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
187 }
04853f27
TH
188 return true;
189 }
190
191 /**
e68e4ccf
JP
192 * Store the computed response analysis in the question_response_analysis table.
193 * @param qubaid_condition $qubaids
04853f27 194 */
e68e4ccf 195 public function store_cached($qubaids) {
04853f27
TH
196 global $DB;
197
e68e4ccf 198 $cachetime = time();
04853f27
TH
199 foreach ($this->responses as $subpartid => $partdata) {
200 foreach ($partdata as $responseclassid => $classdata) {
201 foreach ($classdata as $response => $data) {
0ff4bd08 202 $row = new stdClass();
e68e4ccf 203 $row->hashcode = $qubaids->get_hash_code();
04853f27
TH
204 $row->questionid = $this->questiondata->id;
205 $row->subqid = $subpartid;
206 if ($responseclassid === '') {
207 $row->aid = null;
208 } else {
209 $row->aid = $responseclassid;
210 }
56b0df7e 211 $row->response = $response;
04853f27
TH
212 $row->rcount = $data->count;
213 $row->credit = $data->fraction;
e68e4ccf
JP
214 $row->timemodified = $cachetime;
215 $DB->insert_record('question_response_analysis', $row, false);
04853f27
TH
216 }
217 }
218 }
219 }
220}