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