MDL-70374 qtype_multichoice: improve alignment of choices
[moodle.git] / question / classes / statistics / questions / all_calculated_for_qubaid_condition.php
CommitLineData
c3e2e754
JP
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/>.
16
17/**
18 * A collection of all the question statistics calculated for an activity instance ie. the stats calculated for slots and
19 * sub-questions and variants of those questions.
20 *
21 * @package core_question
fcdd04db 22 * @copyright 2014 The Open University
c3e2e754
JP
23 * @author James Pratt me@jamiep.org
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27namespace core_question\statistics\questions;
28
29/**
30 * A collection of all the question statistics calculated for an activity instance.
31 *
fcdd04db
JP
32 * @package core_question
33 * @copyright 2014 The Open University
34 * @author James Pratt me@jamiep.org
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
c3e2e754
JP
36 */
37class all_calculated_for_qubaid_condition {
38
fcdd04db
JP
39 /** @var int Time after which statistics are automatically recomputed. */
40 const TIME_TO_CACHE = 900; // 15 minutes.
41
c3e2e754
JP
42 /**
43 * @var object[]
44 */
45 public $subquestions;
46
47 /**
48 * Holds slot (position) stats and stats for variants of questions in slots.
49 *
50 * @var calculated[]
51 */
52 public $questionstats = array();
53
54 /**
55 * Holds sub-question stats and stats for variants of subqs.
56 *
57 * @var calculated_for_subquestion[]
58 */
59 public $subquestionstats = array();
60
61 /**
62 * Set up a calculated_for_subquestion instance ready to store a randomly selected question's stats.
63 *
64 * @param object $step
65 * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
66 */
67 public function initialise_for_subq($step, $variant = null) {
fcdd04db 68 $newsubqstat = new calculated_for_subquestion($step, $variant);
c3e2e754
JP
69 if ($variant === null) {
70 $this->subquestionstats[$step->questionid] = $newsubqstat;
71 } else {
72 $this->subquestionstats[$step->questionid]->variantstats[$variant] = $newsubqstat;
73 }
74 }
75
76 /**
77 * Set up a calculated instance ready to store a slot question's stats.
78 *
79 * @param int $slot
80 * @param object $question
81 * @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
82 */
83 public function initialise_for_slot($slot, $question, $variant = null) {
84 $newqstat = new calculated($question, $slot, $variant);
85 if ($variant === null) {
86 $this->questionstats[$slot] = $newqstat;
87 } else {
88 $this->questionstats[$slot]->variantstats[$variant] = $newqstat;
89 }
90 }
91
bccad386
TH
92 /**
93 * Do we have stats for a particular quesitonid (and optionally variant)?
94 *
95 * @param int $questionid The id of the sub question.
96 * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
97 * @return bool whether those stats exist (yet).
98 */
99 public function has_subq($questionid, $variant = null) {
100 if ($variant === null) {
101 return isset($this->subquestionstats[$questionid]);
102 } else {
103 return isset($this->subquestionstats[$questionid]->variantstats[$variant]);
104 }
105 }
106
c3e2e754
JP
107 /**
108 * Reference for a item stats instance for a questionid and optional variant no.
109 *
fcdd04db 110 * @param int $questionid The id of the sub question.
c3e2e754 111 * @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
bccad386
TH
112 * @return calculated|calculated_for_subquestion stats instance for a questionid and optional variant no.
113 * Will be a calculated_for_subquestion if no variant specified.
114 * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
c3e2e754
JP
115 */
116 public function for_subq($questionid, $variant = null) {
117 if ($variant === null) {
118 if (!isset($this->subquestionstats[$questionid])) {
bccad386 119 throw new \coding_exception('Reference to unknown question id ' . $questionid);
c3e2e754
JP
120 } else {
121 return $this->subquestionstats[$questionid];
122 }
123 } else {
124 if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) {
bccad386
TH
125 throw new \coding_exception('Reference to unknown question id ' . $questionid .
126 ' variant ' . $variant);
c3e2e754
JP
127 } else {
128 return $this->subquestionstats[$questionid]->variantstats[$variant];
129 }
130 }
131 }
132
133 /**
134 * ids of all randomly selected question for all slots.
135 *
136 * @return int[] An array of all sub-question ids.
137 */
138 public function get_all_subq_ids() {
139 return array_keys($this->subquestionstats);
140 }
141
142 /**
143 * All slots nos that stats have been calculated for.
144 *
145 * @return int[] An array of all slot nos.
146 */
147 public function get_all_slots() {
148 return array_keys($this->questionstats);
149 }
150
bccad386
TH
151 /**
152 * Do we have stats for a particular slot (and optionally variant)?
153 *
154 * @param int $slot The slot no.
155 * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
156 * @return bool whether those stats exist (yet).
157 */
158 public function has_slot($slot, $variant = null) {
159 if ($variant === null) {
160 return isset($this->questionstats[$slot]);
161 } else {
162 return isset($this->questionstats[$slot]->variantstats[$variant]);
163 }
164 }
165
c3e2e754 166 /**
fcdd04db 167 * Get position stats instance for a slot and optional variant no.
c3e2e754 168 *
fcdd04db 169 * @param int $slot The slot no.
bccad386
TH
170 * @param int|null $variant if provided then we want the object which stores a variant of a position's stats.
171 * @return calculated|calculated_for_subquestion An instance of the class storing the calculated position stats.
172 * @throws \coding_exception if there is an attempt to respond to a non-existant set of stats.
c3e2e754
JP
173 */
174 public function for_slot($slot, $variant = null) {
175 if ($variant === null) {
176 if (!isset($this->questionstats[$slot])) {
bccad386 177 throw new \coding_exception('Reference to unknown slot ' . $slot);
c3e2e754
JP
178 } else {
179 return $this->questionstats[$slot];
180 }
181 } else {
182 if (!isset($this->questionstats[$slot]->variantstats[$variant])) {
bccad386 183 throw new \coding_exception('Reference to unknown slot ' . $slot . ' variant ' . $variant);
c3e2e754
JP
184 } else {
185 return $this->questionstats[$slot]->variantstats[$variant];
186 }
187 }
188 }
189
190 /**
191 * Load cached statistics from the database.
192 *
fcdd04db 193 * @param \qubaid_condition $qubaids Which question usages to load stats for?
c3e2e754
JP
194 */
195 public function get_cached($qubaids) {
196 global $DB;
197
198 $timemodified = time() - self::TIME_TO_CACHE;
199 $questionstatrecs = $DB->get_records_select('question_statistics', 'hashcode = ? AND timemodified > ?',
200 array($qubaids->get_hash_code(), $timemodified));
201
202 $questionids = array();
203 foreach ($questionstatrecs as $fromdb) {
204 if (is_null($fromdb->variant) && !$fromdb->slot) {
205 $questionids[] = $fromdb->questionid;
206 }
207 }
208 $this->subquestions = question_load_questions($questionids);
209 foreach ($questionstatrecs as $fromdb) {
210 if (is_null($fromdb->variant)) {
211 if ($fromdb->slot) {
212 $this->questionstats[$fromdb->slot]->populate_from_record($fromdb);
213 // Array created in constructor and populated from question.
214 } else {
215 $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion();
216 $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb);
217 $this->subquestionstats[$fromdb->questionid]->question = $this->subquestions[$fromdb->questionid];
218 }
219 }
220 }
221 // Add cached variant stats to data structure.
222 foreach ($questionstatrecs as $fromdb) {
223 if (!is_null($fromdb->variant)) {
224 if ($fromdb->slot) {
225 $newcalcinstance = new calculated();
226 $this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance;
227 $newcalcinstance->question = $this->questionstats[$fromdb->slot]->question;
228 } else {
229 $newcalcinstance = new calculated_for_subquestion();
230 $this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance;
231 $newcalcinstance->question = $this->subquestions[$fromdb->questionid];
232 }
233 $newcalcinstance->populate_from_record($fromdb);
234 }
235 }
236 }
237
238 /**
239 * Find time of non-expired statistics in the database.
240 *
fcdd04db
JP
241 * @param \qubaid_condition $qubaids Which question usages to look for stats for?
242 * @return int|bool Time of cached record that matches this qubaid_condition or false if non found.
c3e2e754
JP
243 */
244 public function get_last_calculated_time($qubaids) {
245 global $DB;
246
247 $timemodified = time() - self::TIME_TO_CACHE;
248 return $DB->get_field_select('question_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?',
249 array($qubaids->get_hash_code(), $timemodified), IGNORE_MULTIPLE);
250 }
251
c3e2e754
JP
252 /**
253 * Save stats to db.
254 *
fcdd04db 255 * @param \qubaid_condition $qubaids Which question usages are we caching the stats of?
c3e2e754
JP
256 */
257 public function cache($qubaids) {
258 foreach ($this->get_all_slots() as $slot) {
259 $this->for_slot($slot)->cache($qubaids);
260 }
261
262 foreach ($this->get_all_subq_ids() as $subqid) {
263 $this->for_subq($subqid)->cache($qubaids);
264 }
265 }
266
267 /**
268 * Return all sub-questions used.
269 *
270 * @return \object[] array of questions.
271 */
272 public function get_sub_questions() {
273 return $this->subquestions;
274 }
275
fcdd04db
JP
276 /**
277 * Return all stats for one slot, stats for the slot itself, and either :
278 * - variants of question
279 * - variants of randomly selected questions
280 * - randomly selected questions
281 *
282 * @param int $slot the slot no
283 * @param bool|int $limitvariants limit number of variants and sub-questions displayed?
284 * @return calculated|calculated_for_subquestion[] stats to display
285 */
286 public function structure_analysis_for_one_slot($slot, $limitvariants = false) {
287 return array_merge(array($this->for_slot($slot)), $this->all_subq_and_variant_stats_for_slot($slot, $limitvariants));
288 }
289
290 /**
291 * Call after calculations to output any error messages.
292 *
293 * @return string[] Array of strings describing error messages found during stats calculation.
294 */
295 public function any_error_messages() {
296 $errors = array();
297 foreach ($this->get_all_slots() as $slot) {
298 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
299 if ($this->for_subq($subqid)->differentweights) {
300 $name = $this->for_subq($subqid)->question->name;
5a0221f7 301 $errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'question', $name);
fcdd04db
JP
302 }
303 }
304 }
305 return $errors;
306 }
307
c3e2e754
JP
308 /**
309 * Return all stats for variants of question in slot $slot.
310 *
fcdd04db
JP
311 * @param int $slot The slot no.
312 * @return calculated[] The instances storing the calculated stats.
c3e2e754
JP
313 */
314 protected function all_variant_stats_for_one_slot($slot) {
315 $toreturn = array();
51e3ded8
JP
316 foreach ($this->for_slot($slot)->get_variants() as $variant) {
317 $toreturn[] = $this->for_slot($slot, $variant);
c3e2e754
JP
318 }
319 return $toreturn;
320 }
321
322 /**
323 * Return all stats for variants of randomly selected questions for one slot $slot.
324 *
fcdd04db
JP
325 * @param int $slot The slot no.
326 * @return calculated[] The instances storing the calculated stats.
c3e2e754
JP
327 */
328 protected function all_subq_variants_for_one_slot($slot) {
329 $toreturn = array();
330 $displayorder = 1;
331 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
bec7719c 332 if ($variants = $this->for_subq($subqid)->get_variants()) {
c3e2e754
JP
333 foreach ($variants as $variant) {
334 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant);
335 }
336 }
337 $displayorder++;
338 }
339 return $toreturn;
340 }
341
342 /**
343 * Return all stats for randomly selected questions for one slot $slot.
344 *
fcdd04db
JP
345 * @param int $slot The slot no.
346 * @return calculated[] The instances storing the calculated stats.
c3e2e754
JP
347 */
348 protected function all_subqs_for_one_slot($slot) {
349 $displayorder = 1;
350 $toreturn = array();
351 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
352 $toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid);
353 $displayorder++;
354 }
355 return $toreturn;
356 }
357
358 /**
359 * Return all variant or 'sub-question' stats one slot, either :
360 * - variants of question
361 * - variants of randomly selected questions
362 * - randomly selected questions
363 *
51e3ded8
JP
364 * @param int $slot the slot no
365 * @param bool $limited limit number of variants and sub-questions displayed?
84140b91 366 * @return calculated|calculated_for_subquestion|calculated_question_summary[] stats to display
c3e2e754 367 */
51e3ded8 368 protected function all_subq_and_variant_stats_for_slot($slot, $limited) {
c3e2e754
JP
369 // Random question in this slot?
370 if ($this->for_slot($slot)->get_sub_question_ids()) {
29cc5507
RW
371 $toreturn = array();
372
c3e2e754 373 if ($limited) {
29cc5507 374 $randomquestioncalculated = $this->for_slot($slot);
29cc5507
RW
375
376 if ($subqvariantstats = $this->all_subq_variants_for_one_slot($slot)) {
84140b91 377 // There are some variants from randomly selected questions.
84140b91
SR
378 // If we're showing a limited view of the statistics then add a question summary stat
379 // rather than a stat for each subquestion.
380 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqvariantstats);
381
0ce3fef7 382 $toreturn = array_merge($toreturn, [$summarystat]);
c3e2e754 383 }
29cc5507
RW
384
385 if ($subqstats = $this->all_subqs_for_one_slot($slot)) {
84140b91 386 // There are some randomly selected questions.
84140b91
SR
387 // If we're showing a limited view of the statistics then add a question summary stat
388 // rather than a stat for each subquestion.
389 $summarystat = $this->make_new_calculated_question_summary_stat($randomquestioncalculated, $subqstats);
390
0ce3fef7 391 $toreturn = array_merge($toreturn, [$summarystat]);
84140b91 392 }
d79fef2a 393
29cc5507 394 foreach ($toreturn as $index => $calculated) {
6ee2b972 395 $calculated->subqdisplayorder = $index;
29cc5507
RW
396 }
397 } else {
398 $displaynumber = 1;
399 foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
400 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid);
401 if ($variants = $this->for_subq($subqid)->get_variants()) {
402 foreach ($variants as $variant) {
403 $toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant);
404 }
c3e2e754 405 }
29cc5507 406 $displaynumber++;
c3e2e754 407 }
c3e2e754 408 }
29cc5507 409
c3e2e754
JP
410 return $toreturn;
411 } else {
412 $variantstats = $this->all_variant_stats_for_one_slot($slot);
84140b91
SR
413 if ($limited && $variantstats) {
414 $variantquestioncalculated = $this->for_slot($slot);
84140b91
SR
415
416 // If we're showing a limited view of the statistics then add a question summary stat
417 // rather than a stat for each variation.
418 $summarystat = $this->make_new_calculated_question_summary_stat($variantquestioncalculated, $variantstats);
419
0ce3fef7 420 return [$summarystat];
c3e2e754
JP
421 } else {
422 return $variantstats;
423 }
424 }
51e3ded8
JP
425 }
426
c3e2e754
JP
427 /**
428 * We need a new object for display. Sub-question stats can appear more than once in different slots.
429 * So we create a clone of the object and then we can set properties on the object that are per slot.
430 *
fcdd04db
JP
431 * @param int $displaynumber The display number for this sub question.
432 * @param int $slot The slot number.
433 * @param int $subqid The sub question id.
434 * @param null|int $variant The variant no.
435 * @return calculated_for_subquestion The object for display.
c3e2e754
JP
436 */
437 protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) {
438 $slotstat = fullclone($this->for_subq($subqid, $variant));
439 $slotstat->question->number = $this->for_slot($slot)->question->number;
440 $slotstat->subqdisplayorder = $displaynumber;
441 return $slotstat;
442 }
29cc5507
RW
443
444 /**
84140b91
SR
445 * Create a summary calculated object for a calculated question. This is used as a placeholder
446 * to indicate that a calculated question has sub questions or variations to show rather than listing each
447 * subquestion or variation directly.
29cc5507
RW
448 *
449 * @param calculated $randomquestioncalculated The calculated instance for the random question slot.
d79fef2a 450 * @param calculated[] $subquestionstats The instances of the calculated stats of the questions that are being summarised.
84140b91 451 * @return calculated_question_summary
29cc5507 452 */
84140b91 453 protected function make_new_calculated_question_summary_stat($randomquestioncalculated, $subquestionstats) {
29cc5507
RW
454 $question = $randomquestioncalculated->question;
455 $slot = $randomquestioncalculated->slot;
84140b91 456 $calculatedsummary = new calculated_question_summary($question, $slot, $subquestionstats);
29cc5507
RW
457
458 return $calculatedsummary;
459 }
c3e2e754 460}