MDL-42957 quiz statistics : Hardcoded sql LIMIT clause
[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 *
472c06f8 21 * @package core_question
e68e4ccf
JP
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
04853f27
TH
25 */
26
515b3ae6 27namespace core_question\statistics\responses;
a17b297d
TH
28defined('MOODLE_INTERNAL') || die();
29
04853f27
TH
30/**
31 * This class can store and compute the analysis of the responses to a particular
32 * question.
33 *
e68e4ccf
JP
34 * @copyright 2013 Open University
35 * @author Jamie Pratt <me@jamiep.org>
8d76124c 36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
04853f27 37 */
515b3ae6 38class analyser {
d50b05e6 39 /** @var object full question data from db. */
04853f27 40 protected $questiondata;
04853f27
TH
41
42 /**
d50b05e6 43 * @var analysis_for_question
04853f27 44 */
d50b05e6 45 public $analysis;
04853f27
TH
46
47 /**
d50b05e6
JP
48 * @var array Two index array first index is unique for each sub question part, the second index is the 'class' that this sub
49 * question part can be classified into. This is the return value from {@link \question_type::get_possible_responses()}
04853f27
TH
50 */
51 public $responseclasses = array();
52
53 /**
54 * Create a new instance of this class for holding/computing the statistics
55 * for a particular question.
d50b05e6
JP
56 *
57 * @param object $questiondata the full question data from the database defining this question.
04853f27
TH
58 */
59 public function __construct($questiondata) {
60 $this->questiondata = $questiondata;
d50b05e6
JP
61 $qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
62 $this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
04853f27 63
04853f27
TH
64 }
65
66 /**
f7970e3c 67 * @return bool whether this analysis has more than one subpart.
04853f27
TH
68 */
69 public function has_subparts() {
70 return count($this->responseclasses) > 1;
71 }
72
73 /**
d50b05e6 74 * @return bool whether this analysis has (a subpart with) more than one response class.
04853f27
TH
75 */
76 public function has_response_classes() {
77 foreach ($this->responseclasses as $partclasses) {
78 if (count($partclasses) > 1) {
79 return true;
80 }
81 }
82 return false;
83 }
84
85 /**
f7970e3c 86 * @return bool whether this analysis has a response class more than one
e5b0920e
TH
87 * different acutal response, or if the actual response is different from
88 * the model response.
04853f27
TH
89 */
90 public function has_actual_responses() {
91 foreach ($this->responseclasses as $subpartid => $partclasses) {
e5b0920e
TH
92 foreach ($partclasses as $responseclassid => $modelresponse) {
93 $numresponses = count($this->responses[$subpartid][$responseclassid]);
94 if ($numresponses > 1) {
95 return true;
96 }
97 $actualresponse = key($this->responses[$subpartid][$responseclassid]);
98 if ($numresponses == 1 && $actualresponse != $modelresponse->responseclass) {
04853f27
TH
99 return true;
100 }
101 }
102 }
103 return false;
104 }
105
106 /**
107 * Analyse all the response data for for all the specified attempts at
108 * this question.
515b3ae6 109 * @param \qubaid_condition $qubaids which attempts to consider.
d50b05e6 110 * @return analysis_for_question
04853f27 111 */
e68e4ccf 112 public function calculate($qubaids) {
04853f27 113 // Load data.
515b3ae6 114 $dm = new \question_engine_data_mapper();
04853f27
TH
115 $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
116
117 // Analyse it.
118 foreach ($questionattempts as $qa) {
d50b05e6
JP
119 $responseparts = $qa->classify_response();
120 $this->analysis->count_response_parts($responseparts);
04853f27 121 }
d50b05e6
JP
122 $this->analysis->cache($qubaids, $this->questiondata->id);
123 return $this->analysis;
04853f27
TH
124 }
125
d50b05e6
JP
126 /** @var integer Time after which responses are automatically reanalysed. */
127 const TIME_TO_CACHE = 900; // 15 minutes.
04853f27 128
04853f27
TH
129
130 /**
d50b05e6
JP
131 * Retrieve the computed response analysis from the question_response_analysis table.
132 *
133 * @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
134 * @return analysis_for_question|boolean analysis or false if no cached analysis found.
04853f27 135 */
e68e4ccf 136 public function load_cached($qubaids) {
04853f27
TH
137 global $DB;
138
d50b05e6 139 $timemodified = time() - self::TIME_TO_CACHE;
e4b17111 140 $rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
d50b05e6 141 array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
04853f27
TH
142 if (!$rows) {
143 return false;
144 }
145
146 foreach ($rows as $row) {
d50b05e6
JP
147 $class = $this->analysis->get_subpart($row->subqid)->get_response_class($row->aid);
148 $class->add_response_and_count($row->response, $row->credit, $row->rcount);
04853f27 149 }
d50b05e6 150 return $this->analysis;
04853f27
TH
151 }
152
d50b05e6 153
04853f27 154 /**
d50b05e6
JP
155 * Find time of non-expired analysis in the database.
156 *
157 * @param $qubaids \qubaid_condition
158 * @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
04853f27 159 */
d50b05e6 160 public function get_last_analysed_time($qubaids) {
04853f27
TH
161 global $DB;
162
d50b05e6 163 $timemodified = time() - self::TIME_TO_CACHE;
e4b17111
JP
164 return $DB->get_field_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
165 array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified), IGNORE_MULTIPLE);
04853f27
TH
166 }
167}