Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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