ae3d206242b78d2bc2f6ee4961b3b8efb0e12853
[moodle.git] / question / classes / statistics / responses / analyser.php
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/>.
17 /**
18  * This file contains the code to analyse all the responses to a particular
19  * question.
20  *
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
26  */
28 namespace core_question\statistics\responses;
29 defined('MOODLE_INTERNAL') || die();
31 /**
32  * This class can store and compute the analysis of the responses to a particular
33  * question.
34  *
35  * @copyright 2013 Open University
36  * @author    Jamie Pratt <me@jamiep.org>
37  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class analyser {
40     /** @var object the data from the database that defines the question. */
41     protected $questiondata;
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();
55     /**
56      * @var array $this->fractions[$subpartid][$responseclassid] is an object
57      * with two fields, ->responseclass and ->fraction.
58      */
59     public $responseclasses = array();
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;
69         $this->responseclasses = \question_bank::get_qtype($questiondata->qtype)->get_possible_responses($questiondata);
70         foreach ($this->responseclasses as $subpartid => $responseclasses) {
71             foreach ($responseclasses as $responseclassid => $notused) {
72                 $this->responses[$subpartid][$responseclassid] = array();
73             }
74         }
75     }
77     /**
78      * @return bool whether this analysis has more than one subpart.
79      */
80     public function has_subparts() {
81         return count($this->responseclasses) > 1;
82     }
84     /**
85      * @return bool whether this analysis has (a subpart with) more than one
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     }
97     /**
98      * @return bool whether this analysis has a response class more than one
99      *      different acutal response, or if the actual response is different from
100      *      the model response.
101      */
102     public function has_actual_responses() {
103         foreach ($this->responseclasses as $subpartid => $partclasses) {
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) {
111                     return true;
112                 }
113             }
114         }
115         return false;
116     }
118     /**
119      * Analyse all the response data for for all the specified attempts at
120      * this question.
121      * @param \qubaid_condition $qubaids which attempts to consider.
122      */
123     public function calculate($qubaids) {
124         // Load data.
125         $dm = new \question_engine_data_mapper();
126         $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
128         // Analyse it.
129         foreach ($questionattempts as $qa) {
130             $this->add_data_from_one_attempt($qa);
131         }
132         $this->store_cached($qubaids);
134     }
136     /**
137      * Analyse the data from one question attempt.
138      * @param \question_attempt $qa the data to analyse.
139      */
140     protected function add_data_from_one_attempt(\question_attempt $qa) {
141         $blankresponse = \question_classified_response::no_response();
143         $partresponses = $qa->classify_response();
144         foreach ($partresponses as $subpartid => $partresponse) {
145             if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
146                     [$partresponse->response])) {
147                 $resp = new \stdClass();
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                 }
156                 $this->responses[$subpartid][$partresponse->responseclassid]
157                         [$partresponse->response] = $resp;
158             }
160             $this->responses[$subpartid][$partresponse->responseclassid]
161                     [$partresponse->response]->count += 1;
162         }
163     }
165     /**
166      * Store the computed response analysis in the question_response_analysis table.
167      * @param \qubaid_condition $qubaids
168      * data corresponding to.
169      * @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
170      */
171     public function load_cached($qubaids) {
172         global $DB;
174         $rows = $DB->get_records('question_response_analysis',
175                 array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
176         if (!$rows) {
177             return false;
178         }
180         foreach ($rows as $row) {
181             $this->responses[$row->subqid][$row->aid][$row->response] = new \stdClass();
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         }
185         return true;
186     }
188     /**
189      * Store the computed response analysis in the question_response_analysis table.
190      * @param \qubaid_condition $qubaids
191      */
192     public function store_cached($qubaids) {
193         global $DB;
195         $cachetime = time();
196         foreach ($this->responses as $subpartid => $partdata) {
197             foreach ($partdata as $responseclassid => $classdata) {
198                 foreach ($classdata as $response => $data) {
199                     $row = new \stdClass();
200                     $row->hashcode = $qubaids->get_hash_code();
201                     $row->questionid = $this->questiondata->id;
202                     $row->subqid = $subpartid;
203                     if ($responseclassid === '') {
204                         $row->aid = null;
205                     } else {
206                         $row->aid = $responseclassid;
207                     }
208                     $row->response = $response;
209                     $row->rcount = $data->count;
210                     $row->credit = $data->fraction;
211                     $row->timemodified = $cachetime;
212                     $DB->insert_record('question_response_analysis', $row, false);
213                 }
214             }
215         }
216     }