96fdc4178a89439bbfffe6621fd0f6dac161e374
[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_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
25  */
27 namespace core_question\statistics\responses;
28 defined('MOODLE_INTERNAL') || die();
30 /**
31  * This class can store and compute the analysis of the responses to a particular
32  * question.
33  *
34  * @copyright 2013 Open University
35  * @author    Jamie Pratt <me@jamiep.org>
36  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class analyser {
39     /** @var object full question data from db. */
40     protected $questiondata;
42     /**
43      * @var analysis_for_question
44      */
45     public $analysis;
47     /**
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.
52      */
53     public $responseclasses = array();
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;
61     /**
62      * Create a new instance of this class for holding/computing the statistics
63      * for a particular question.
64      *
65      * @param object $questiondata the full question data from the database defining this question.
66      */
67     public function __construct($questiondata) {
68         $this->questiondata = $questiondata;
69         $qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
70         $this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
71         $this->breakdownbyvariant = $qtypeobj->break_down_stats_and_response_analysis_by_variant($this->questiondata);
72     }
74     /**
75      * @return bool whether this analysis has more than one subpart.
76      */
77     public function has_subparts() {
78         return count($this->responseclasses) > 1;
79     }
81     /**
82      * @return bool whether this analysis has (a subpart with) more than one response class.
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     }
93     /**
94      * @return bool whether this analysis has a response class more than one
95      *      different acutal response, or if the actual response is different from
96      *      the model response.
97      */
98     public function has_actual_responses() {
99         foreach ($this->responseclasses as $subpartid => $partclasses) {
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) {
107                     return true;
108                 }
109             }
110         }
111         return false;
112     }
114     /**
115      * Analyse all the response data for for all the specified attempts at
116      * this question.
117      * @param \qubaid_condition $qubaids which attempts to consider.
118      * @return analysis_for_question
119      */
120     public function calculate($qubaids) {
121         // Load data.
122         $dm = new \question_engine_data_mapper();
123         $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
125         // Analyse it.
126         foreach ($questionattempts as $qa) {
127             $responseparts = $qa->classify_response();
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             }
134         }
135         $this->analysis->cache($qubaids, $this->questiondata->id);
136         return $this->analysis;
137     }
139     /** @var integer Time after which responses are automatically reanalysed. */
140     const TIME_TO_CACHE = 900; // 15 minutes.
143     /**
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.
148      */
149     public function load_cached($qubaids) {
150         global $DB;
152         $timemodified = time() - self::TIME_TO_CACHE;
153         $rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
154                                         array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
155         if (!$rows) {
156             return false;
157         }
159         foreach ($rows as $row) {
160             $class = $this->analysis->get_analysis_for_subpart($row->variant, $row->subqid)->get_response_class($row->aid);
161             $class->add_response_and_count($row->response, $row->credit, $row->rcount);
162         }
163         return $this->analysis;
164     }
167     /**
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.
172      */
173     public function get_last_analysed_time($qubaids) {
174         global $DB;
176         $timemodified = time() - self::TIME_TO_CACHE;
177         return $DB->get_field_select('question_response_analysis', 'timemodified',
178                                      'hashcode = ? AND questionid = ? AND timemodified > ?',
179                                      array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified), IGNORE_MULTIPLE);
180     }