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
 * Analytics basic actions manager.
19
 *
20
 * @package   core_analytics
21
 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_analytics;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Analytics basic actions manager.
31
 *
32
 * @package   core_analytics
33
 * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
class manager {
37
 
38
    /**
39
     * Default mlbackend
40
     */
41
    const DEFAULT_MLBACKEND = '\mlbackend_php\processor';
42
 
43
    /**
44
     * Name of the file where components declare their models.
45
     */
46
    const ANALYTICS_FILENAME = 'db/analytics.php';
47
 
48
    /**
49
     * @var \core_analytics\predictor[]
50
     */
51
    protected static $predictionprocessors = [];
52
 
53
    /**
54
     * @var \core_analytics\local\target\base[]
55
     */
56
    protected static $alltargets = null;
57
 
58
    /**
59
     * @var \core_analytics\local\indicator\base[]
60
     */
61
    protected static $allindicators = null;
62
 
63
    /**
64
     * @var \core_analytics\local\time_splitting\base[]
65
     */
66
    protected static $alltimesplittings = null;
67
 
68
    /**
69
     * Checks that the user can manage models
70
     *
71
     * @throws \required_capability_exception
72
     * @return void
73
     */
74
    public static function check_can_manage_models() {
75
        require_capability('moodle/analytics:managemodels', \context_system::instance());
76
    }
77
 
78
    /**
79
     * Checks that the user can list that context insights
80
     *
81
     * @throws \required_capability_exception
82
     * @param \context $context
83
     * @param  bool $return The method returns a bool if true.
84
     * @return void
85
     */
86
    public static function check_can_list_insights(\context $context, bool $return = false) {
87
        global $USER;
88
 
89
        if ($context->contextlevel === CONTEXT_USER && $context->instanceid == $USER->id) {
90
            $capability = 'moodle/analytics:listowninsights';
91
        } else {
92
            $capability = 'moodle/analytics:listinsights';
93
        }
94
 
95
        if ($return) {
96
            return has_capability($capability, $context);
97
        } else {
98
            require_capability($capability, $context);
99
        }
100
    }
101
 
102
    /**
103
     * Is analytics enabled globally?
104
     *
105
     * return bool
106
     */
107
    public static function is_analytics_enabled(): bool {
108
        global $CFG;
109
 
110
        if (isset($CFG->enableanalytics)) {
111
            return $CFG->enableanalytics;
112
        }
113
 
114
        // Enabled by default.
115
        return true;
116
    }
117
 
118
    /**
119
     * Returns all system models that match the provided filters.
120
     *
121
     * @param bool $enabled
122
     * @param bool $trained
123
     * @param \context|false $predictioncontext
124
     * @return \core_analytics\model[]
125
     */
126
    public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
127
        global $DB;
128
 
129
        $params = array();
130
 
131
        $sql = "SELECT am.* FROM {analytics_models} am";
132
 
133
        if ($enabled || $trained || $predictioncontext) {
134
            $conditions = [];
135
            if ($enabled) {
136
                $conditions[] = 'am.enabled = :enabled';
137
                $params['enabled'] = 1;
138
            }
139
            if ($trained) {
140
                $conditions[] = 'am.trained = :trained';
141
                $params['trained'] = 1;
142
            }
143
            if ($predictioncontext) {
144
                $conditions[] = "EXISTS (SELECT 'x'
145
                                           FROM {analytics_predictions} ap
146
                                          WHERE ap.modelid = am.id AND ap.contextid = :contextid)";
147
                $params['contextid'] = $predictioncontext->id;
148
            }
149
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
150
        }
151
        $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
152
 
153
        $modelobjs = $DB->get_records_sql($sql, $params);
154
 
155
        $models = array();
156
        foreach ($modelobjs as $modelobj) {
157
            $model = new \core_analytics\model($modelobj);
158
            if ($model->is_available()) {
159
                $models[$modelobj->id] = $model;
160
            }
161
        }
162
 
163
        // Sort the models by the model name using the current session language.
164
        \core_collator::asort_objects_by_method($models, 'get_name');
165
 
166
        return $models;
167
    }
168
 
169
    /**
170
     * Returns the provided predictions processor class.
171
     *
172
     * @param false|string $predictionclass Returns the system default processor if false
173
     * @param bool $checkisready
174
     * @return \core_analytics\predictor
175
     */
176
    public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
177
 
178
        // We want 0 or 1 so we can use it as an array key for caching.
179
        $checkisready = intval($checkisready);
180
 
181
        if (!$predictionclass) {
182
            $predictionclass = get_config('analytics', 'predictionsprocessor');
183
        }
184
 
185
        if (empty($predictionclass)) {
186
            // Use the default one if nothing set.
187
            $predictionclass = self::default_mlbackend();
188
        }
189
 
190
        if (!class_exists($predictionclass)) {
191
            throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
192
        }
193
 
194
        $interfaces = class_implements($predictionclass);
195
        if (empty($interfaces['core_analytics\predictor'])) {
196
            throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
197
        }
198
 
199
        // Return it from the cached list.
200
        if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
201
 
202
            $instance = new $predictionclass();
203
            if ($checkisready) {
204
                $isready = $instance->is_ready();
205
                if ($isready !== true) {
206
                    throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
207
                }
208
            }
209
            self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
210
        }
211
 
212
        return self::$predictionprocessors[$checkisready][$predictionclass];
213
    }
214
 
215
    /**
216
     * Return all system predictions processors.
217
     *
218
     * @return \core_analytics\predictor[]
219
     */
220
    public static function get_all_prediction_processors() {
221
 
222
        $mlbackends = \core_component::get_plugin_list('mlbackend');
223
 
224
        $predictionprocessors = array();
225
        foreach ($mlbackends as $mlbackend => $unused) {
226
            $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
227
            $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
228
        }
229
        return $predictionprocessors;
230
    }
231
 
232
    /**
233
     * Resets the cached prediction processors.
234
     * @return null
235
     */
236
    public static function reset_prediction_processors() {
237
        self::$predictionprocessors = [];
238
    }
239
 
240
    /**
241
     * Returns the name of the provided predictions processor.
242
     *
243
     * @param \core_analytics\predictor $predictionsprocessor
244
     * @return string
245
     */
246
    public static function get_predictions_processor_name(\core_analytics\predictor $predictionsprocessor) {
247
            $component = substr(get_class($predictionsprocessor), 0, strpos(get_class($predictionsprocessor), '\\', 1));
248
        return get_string('pluginname', $component);
249
    }
250
 
251
    /**
252
     * Whether the provided plugin is used by any model.
253
     *
254
     * @param string $plugin
255
     * @return bool
256
     */
257
    public static function is_mlbackend_used($plugin) {
258
        $models = self::get_all_models();
259
        foreach ($models as $model) {
260
            $processor = $model->get_predictions_processor();
261
            $noprefixnamespace = ltrim(get_class($processor), '\\');
262
            $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\'));
263
            if ($processorplugin == $plugin) {
264
                return true;
265
            }
266
        }
267
 
268
        // Default predictions processor.
269
        $defaultprocessorclass = get_config('analytics', 'predictionsprocessor');
270
        $pluginclass = '\\' . $plugin . '\\processor';
271
        if ($pluginclass === $defaultprocessorclass) {
272
            return true;
273
        }
274
 
275
        return false;
276
    }
277
 
278
    /**
279
     * Get all available time splitting methods.
280
     *
281
     * @return \core_analytics\local\time_splitting\base[]
282
     */
283
    public static function get_all_time_splittings() {
284
        if (self::$alltimesplittings !== null) {
285
            return self::$alltimesplittings;
286
        }
287
 
288
        $classes = self::get_analytics_classes('time_splitting');
289
 
290
        self::$alltimesplittings = [];
291
        foreach ($classes as $fullclassname => $classpath) {
292
            $instance = self::get_time_splitting($fullclassname);
293
            // We need to check that it is a valid time splitting method, it may be an abstract class.
294
            if ($instance) {
295
                self::$alltimesplittings[$instance->get_id()] = $instance;
296
            }
297
        }
298
 
299
        return self::$alltimesplittings;
300
    }
301
 
302
    /**
303
     * @deprecated since Moodle 3.7 use get_time_splitting_methods_for_evaluation instead
304
     */
305
    public static function get_enabled_time_splitting_methods() {
306
        throw new coding_exception(__FUNCTION__ . '() has been removed. You can use self::get_time_splitting_methods_for_evaluation if ' .
307
            'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
308
            'you want to get all the time splitting methods available on this site.');
309
    }
310
 
311
    /**
312
     * Returns the time-splitting methods for model evaluation.
313
     *
314
     * @param  bool $all Return all the time-splitting methods that can potentially be used for evaluation or the default ones.
315
     * @return \core_analytics\local\time_splitting\base[]
316
     */
317
    public static function get_time_splitting_methods_for_evaluation(bool $all = false) {
318
 
319
        if ($all === false) {
320
            if ($enabledtimesplittings = get_config('analytics', 'defaulttimesplittingsevaluation')) {
321
                $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
322
            }
323
        }
324
 
325
        $timesplittings = self::get_all_time_splittings();
326
        foreach ($timesplittings as $key => $timesplitting) {
327
 
328
            if (!$timesplitting->valid_for_evaluation()) {
329
                unset($timesplittings[$key]);
330
            }
331
 
332
            if ($all === false) {
333
                // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
334
                if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
335
                    unset($timesplittings[$key]);
336
                }
337
            }
338
        }
339
        return $timesplittings;
340
    }
341
 
342
    /**
343
     * Returns a time splitting method by its classname.
344
     *
345
     * @param string $fullclassname
346
     * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
347
     */
348
    public static function get_time_splitting($fullclassname) {
349
        if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
350
            return false;
351
        }
352
        return new $fullclassname();
353
    }
354
 
355
    /**
356
     * Return all targets in the system.
357
     *
358
     * @return \core_analytics\local\target\base[]
359
     */
360
    public static function get_all_targets(): array {
361
        if (self::$alltargets !== null) {
362
            return self::$alltargets;
363
        }
364
 
365
        $classes = self::get_analytics_classes('target');
366
 
367
        self::$alltargets = [];
368
        foreach ($classes as $fullclassname => $classpath) {
369
            $instance = self::get_target($fullclassname);
370
            if ($instance) {
371
                self::$alltargets[$instance->get_id()] = $instance;
372
            }
373
        }
374
 
375
        return self::$alltargets;
376
    }
377
    /**
378
     * Return all system indicators.
379
     *
380
     * @return \core_analytics\local\indicator\base[]
381
     */
382
    public static function get_all_indicators() {
383
        if (self::$allindicators !== null) {
384
            return self::$allindicators;
385
        }
386
 
387
        $classes = self::get_analytics_classes('indicator');
388
 
389
        self::$allindicators = [];
390
        foreach ($classes as $fullclassname => $classpath) {
391
            $instance = self::get_indicator($fullclassname);
392
            if ($instance) {
393
                self::$allindicators[$instance->get_id()] = $instance;
394
            }
395
        }
396
 
397
        return self::$allindicators;
398
    }
399
 
400
    /**
401
     * Returns the specified target
402
     *
403
     * @param mixed $fullclassname
404
     * @return \core_analytics\local\target\base|false False if it is not valid
405
     */
406
    public static function get_target($fullclassname) {
407
        if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
408
            return false;
409
        }
410
        return new $fullclassname();
411
    }
412
 
413
    /**
414
     * Returns an instance of the provided indicator.
415
     *
416
     * @param string $fullclassname
417
     * @return \core_analytics\local\indicator\base|false False if it is not valid.
418
     */
419
    public static function get_indicator($fullclassname) {
420
        if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
421
            return false;
422
        }
423
        return new $fullclassname();
424
    }
425
 
426
    /**
427
     * Returns whether a time splitting method is valid or not.
428
     *
429
     * @param string $fullclassname
430
     * @param string $baseclass
431
     * @return bool
432
     */
433
    public static function is_valid($fullclassname, $baseclass) {
434
        if (is_subclass_of($fullclassname, $baseclass)) {
435
            if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
436
                return true;
437
            }
438
        }
439
        return false;
440
    }
441
 
442
    /**
443
     * Returns the logstore used for analytics.
444
     *
445
     * @return \core\log\sql_reader|false False if no log stores are enabled.
446
     */
447
    public static function get_analytics_logstore() {
448
        $readers = get_log_manager()->get_readers('core\log\sql_reader');
449
        $analyticsstore = get_config('analytics', 'logstore');
450
 
451
        if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
452
            $logstore = $readers[$analyticsstore];
453
        } else if (empty($analyticsstore) && !empty($readers)) {
454
            // The first one, it is the same default than in settings.
455
            $logstore = reset($readers);
456
        } else if (!empty($readers)) {
457
            $logstore = reset($readers);
458
            debugging('The selected log store for analytics is not available anymore. Using "' .
459
                $logstore->get_name() . '"', DEBUG_DEVELOPER);
460
        }
461
 
462
        if (empty($logstore)) {
463
            debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
464
            return false;
465
        }
466
 
467
        if (!$logstore->is_logging()) {
468
            debugging('The selected log store for analytics "' . $logstore->get_name() .
469
                '" is not logging activity logs', DEBUG_DEVELOPER);
470
        }
471
 
472
        return $logstore;
473
    }
474
 
475
    /**
476
     * Returns this analysable calculations during the provided period.
477
     *
478
     * @param \core_analytics\analysable $analysable
479
     * @param int $starttime
480
     * @param int $endtime
481
     * @param string $samplesorigin The samples origin as sampleid is not unique across models.
482
     * @return array
483
     */
484
    public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
485
        global $DB;
486
 
487
        $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
488
            'sampleorigin' => $samplesorigin);
489
        $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
490
 
491
        $existingcalculations = array();
492
        foreach ($calculations as $calculation) {
493
            if (empty($existingcalculations[$calculation->indicator])) {
494
                $existingcalculations[$calculation->indicator] = array();
495
            }
496
            $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
497
        }
498
        $calculations->close();
499
        return $existingcalculations;
500
    }
501
 
502
    /**
503
     * Returns the models with insights at the provided context.
504
     *
505
     * Note that this method is used for display purposes. It filters out models whose insights
506
     * are not linked from the reports page.
507
     *
508
     * @param \context $context
509
     * @return \core_analytics\model[]
510
     */
511
    public static function get_models_with_insights(\context $context) {
512
 
513
        self::check_can_list_insights($context);
514
 
515
        $models = self::get_all_models(true, true, $context);
516
        foreach ($models as $key => $model) {
517
            // Check that it not only have predictions but also generates insights from them.
518
            if (!$model->uses_insights() || !$model->get_target()->link_insights_report()) {
519
                unset($models[$key]);
520
            }
521
        }
522
        return $models;
523
    }
524
 
525
    /**
526
     * Returns the models that generated insights in the provided context. It can also be used to add new models to the context.
527
     *
528
     * Note that if you use this function with $newmodelid is the caller responsibility to ensure that the
529
     * provided model id generated insights for the provided context.
530
     *
531
     * @throws \coding_exception
532
     * @param  \context $context
533
     * @param  int|null $newmodelid A new model to add to the list of models with insights in the provided context.
534
     * @return int[]
535
     */
536
    public static function cached_models_with_insights(\context $context, int $newmodelid = null) {
537
 
538
        $cache = \cache::make('core', 'contextwithinsights');
539
        $modelids = $cache->get($context->id);
540
        if ($modelids === false) {
541
            // The cache is empty, but we don't know if it is empty because there are no insights
542
            // in this context or because cache/s have been purged, we need to be conservative and
543
            // "pay" 1 db read to fill up the cache.
544
 
545
            $models = \core_analytics\manager::get_models_with_insights($context);
546
 
547
            if ($newmodelid && empty($models[$newmodelid])) {
548
                throw new \coding_exception('The provided modelid ' . $newmodelid . ' did not generate any insights');
549
            }
550
 
551
            $modelids = array_keys($models);
552
            $cache->set($context->id, $modelids);
553
 
554
        } else if ($newmodelid && !in_array($newmodelid, $modelids)) {
555
            // We add the context we got as an argument to the cache.
556
 
557
            array_push($modelids, $newmodelid);
558
            $cache->set($context->id, $modelids);
559
        }
560
 
561
        return $modelids;
562
    }
563
 
564
    /**
565
     * Returns a prediction
566
     *
567
     * @param int $predictionid
568
     * @param bool $requirelogin
569
     * @return array array($model, $prediction, $context)
570
     */
571
    public static function get_prediction($predictionid, $requirelogin = false) {
572
        global $DB;
573
 
574
        if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
575
            throw new \moodle_exception('errorpredictionnotfound', 'analytics');
576
        }
577
 
578
        $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
579
        if (!$context) {
580
            throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
581
        }
582
 
583
        if ($requirelogin) {
584
            list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
585
            require_login($course, false, $cm);
586
        }
587
 
588
        self::check_can_list_insights($context);
589
 
590
        $model = new \core_analytics\model($predictionobj->modelid);
591
        $sampledata = $model->prediction_sample_data($predictionobj);
592
        $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
593
 
594
        return array($model, $prediction, $context);
595
    }
596
 
597
    /**
598
     * Used to be used to add models included with the Moodle core.
599
     *
600
     * @deprecated Deprecated since Moodle 3.7 (MDL-61667) - Use lib/db/analytics.php instead.
601
     * @todo Remove this method in Moodle 3.11 (MDL-65186).
602
     * @return void
603
     */
604
    public static function add_builtin_models() {
605
 
606
        throw new \coding_exception('core_analytics\manager::add_builtin_models() has been removed. Core models ' .
607
                        'are now automatically updated according to their declaration in the lib/db/analytics.php file.');
608
    }
609
 
610
    /**
611
     * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
612
     */
613
    public static function cleanup() {
614
        global $DB;
615
 
616
        $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
617
                          (SELECT ap.id FROM {analytics_predictions} ap
618
                        LEFT JOIN {context} ctx ON ap.contextid = ctx.id
619
                            WHERE ctx.id IS NULL)");
620
 
621
        // Cleanup analaytics predictions/calcs with MySQL friendly sub-select.
622
        $DB->execute("DELETE FROM {analytics_predictions} WHERE id IN (
623
                        SELECT oldpredictions.id
624
                        FROM (
625
                            SELECT p.id
626
                            FROM {analytics_predictions} p
627
                            LEFT JOIN {context} ctx ON p.contextid = ctx.id
628
                            WHERE ctx.id IS NULL
629
                        ) oldpredictions
630
                    )");
631
 
632
        $DB->execute("DELETE FROM {analytics_indicator_calc} WHERE id IN (
633
                        SELECT oldcalcs.id FROM (
634
                            SELECT c.id
635
                            FROM {analytics_indicator_calc} c
636
                            LEFT JOIN {context} ctx ON c.contextid = ctx.id
637
                            WHERE ctx.id IS NULL
638
                        ) oldcalcs
639
                    )");
640
 
641
        // Clean up stuff that depends on analysable ids that do not exist anymore.
642
 
643
        $models = self::get_all_models();
644
        foreach ($models as $model) {
645
 
646
            // We first dump into memory the list of analysables we have in the database (we could probably do this with 1 single
647
            // query for the 3 tables, but it may be safer to do it separately).
648
            $predictsamplesanalysableids = $DB->get_fieldset_select('analytics_predict_samples', 'DISTINCT analysableid',
649
                'modelid = :modelid', ['modelid' => $model->get_id()]);
650
            $predictsamplesanalysableids = array_flip($predictsamplesanalysableids);
651
            $trainsamplesanalysableids = $DB->get_fieldset_select('analytics_train_samples', 'DISTINCT analysableid',
652
                'modelid = :modelid', ['modelid' => $model->get_id()]);
653
            $trainsamplesanalysableids = array_flip($trainsamplesanalysableids);
654
            $usedanalysablesanalysableids = $DB->get_fieldset_select('analytics_used_analysables', 'DISTINCT analysableid',
655
                'modelid = :modelid', ['modelid' => $model->get_id()]);
656
            $usedanalysablesanalysableids = array_flip($usedanalysablesanalysableids);
657
 
658
            $analyser = $model->get_analyser(array('notimesplitting' => true));
659
 
660
            // We do not honour the list of contexts in this model as it can contain stale records.
661
            $analysables = $analyser->get_analysables_iterator();
662
 
663
            $analysableids = [];
664
            foreach ($analysables as $analysable) {
665
                if (!$analysable) {
666
                    continue;
667
                }
668
                unset($predictsamplesanalysableids[$analysable->get_id()]);
669
                unset($trainsamplesanalysableids[$analysable->get_id()]);
670
                unset($usedanalysablesanalysableids[$analysable->get_id()]);
671
            }
672
 
673
            $param = ['modelid' => $model->get_id()];
674
 
675
            if ($predictsamplesanalysableids) {
676
                list($idssql, $idsparams) = $DB->get_in_or_equal(array_flip($predictsamplesanalysableids), SQL_PARAMS_NAMED);
677
                $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $idssql",
678
                    $param + $idsparams);
679
            }
680
            if ($trainsamplesanalysableids) {
681
                list($idssql, $idsparams) = $DB->get_in_or_equal(array_flip($trainsamplesanalysableids), SQL_PARAMS_NAMED);
682
                $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $idssql",
683
                    $param + $idsparams);
684
            }
685
            if ($usedanalysablesanalysableids) {
686
                list($idssql, $idsparams) = $DB->get_in_or_equal(array_flip($usedanalysablesanalysableids), SQL_PARAMS_NAMED);
687
                $DB->delete_records_select('analytics_used_analysables', "modelid = :modelid AND analysableid $idssql",
688
                    $param + $idsparams);
689
            }
690
        }
691
 
692
        // Clean up calculations table.
693
        $calclifetime = get_config('analytics', 'calclifetime');
694
        if (!empty($calclifetime)) {
695
            $lifetime = time() - ($calclifetime * DAYSECS); // Value in days.
696
            $DB->delete_records_select('analytics_indicator_calc', 'timecreated < ?', [$lifetime]);
697
        }
698
    }
699
 
700
    /**
701
     * Default system backend.
702
     *
703
     * @return string
704
     */
705
    public static function default_mlbackend() {
706
        return self::DEFAULT_MLBACKEND;
707
    }
708
 
709
    /**
710
     * Returns the provided element classes in the site.
711
     *
712
     * @param string $element
713
     * @return string[] Array keys are the FQCN and the values the class path.
714
     */
715
    private static function get_analytics_classes($element) {
716
 
717
        // Just in case...
718
        $element = clean_param($element, PARAM_ALPHANUMEXT);
719
 
720
        $classes = \core_component::get_component_classes_in_namespace(null, 'analytics\\' . $element);
721
 
722
        return $classes;
723
    }
724
 
725
    /**
726
     * Check that all the models declared by the component are up to date.
727
     *
728
     * This is intended to be called during the installation / upgrade to automatically create missing models.
729
     *
730
     * @param string $componentname The name of the component to load models for.
731
     * @return array \core_analytics\model[] List of actually created models.
732
     */
733
    public static function update_default_models_for_component(string $componentname): array {
734
 
735
        $result = [];
736
 
737
        foreach (static::load_default_models_for_component($componentname) as $definition) {
738
            if (!\core_analytics\model::exists(static::get_target($definition['target']))) {
739
                $result[] = static::create_declared_model($definition);
740
            }
741
        }
742
 
743
        return $result;
744
    }
745
 
746
    /**
747
     * Return the list of models declared by the given component.
748
     *
749
     * @param string $componentname The name of the component to load models for.
750
     * @throws \coding_exception Exception thrown in case of invalid syntax.
751
     * @return array The $models description array.
752
     */
753
    public static function load_default_models_for_component(string $componentname): array {
754
 
755
        $dir = \core_component::get_component_directory($componentname);
756
 
757
        if (!$dir) {
758
            // This is either an invalid component, or a core subsystem without its own root directory.
759
            return [];
760
        }
761
 
762
        $file = $dir . '/' . self::ANALYTICS_FILENAME;
763
 
764
        if (!is_readable($file)) {
765
            return [];
766
        }
767
 
768
        $models = null;
769
        include($file);
770
 
771
        if (!isset($models) || !is_array($models) || empty($models)) {
772
            return [];
773
        }
774
 
775
        foreach ($models as &$model) {
776
            if (!isset($model['enabled'])) {
777
                $model['enabled'] = false;
778
            } else {
779
                $model['enabled'] = clean_param($model['enabled'], PARAM_BOOL);
780
            }
781
        }
782
 
783
        static::validate_models_declaration($models);
784
 
785
        return $models;
786
    }
787
 
788
    /**
789
     * Return the list of all the models declared anywhere in this Moodle installation.
790
     *
791
     * Models defined by the core and core subsystems come first, followed by those provided by plugins.
792
     *
793
     * @return array indexed by the frankenstyle component
794
     */
795
    public static function load_default_models_for_all_components(): array {
796
 
797
        $tmp = [];
798
 
799
        foreach (\core_component::get_component_list() as $type => $components) {
800
            foreach (array_keys($components) as $component) {
801
                if ($loaded = static::load_default_models_for_component($component)) {
802
                    $tmp[$type][$component] = $loaded;
803
                }
804
            }
805
        }
806
 
807
        $result = [];
808
 
809
        if ($loaded = static::load_default_models_for_component('core')) {
810
            $result['core'] = $loaded;
811
        }
812
 
813
        if (!empty($tmp['core'])) {
814
            $result += $tmp['core'];
815
            unset($tmp['core']);
816
        }
817
 
818
        foreach ($tmp as $components) {
819
            $result += $components;
820
        }
821
 
822
        return $result;
823
    }
824
 
825
    /**
826
     * Validate the declaration of prediction models according the syntax expected in the component's db folder.
827
     *
828
     * The expected structure looks like this:
829
     *
830
     *  [
831
     *      [
832
     *          'target' => '\fully\qualified\name\of\the\target\class',
833
     *          'indicators' => [
834
     *              '\fully\qualified\name\of\the\first\indicator',
835
     *              '\fully\qualified\name\of\the\second\indicator',
836
     *          ],
837
     *          'timesplitting' => '\optional\name\of\the\time_splitting\class',
838
     *          'enabled' => true,
839
     *      ],
840
     *  ];
841
     *
842
     * @param array $models List of declared models.
843
     * @throws \coding_exception Exception thrown in case of invalid syntax.
844
     */
845
    public static function validate_models_declaration(array $models) {
846
 
847
        foreach ($models as $model) {
848
            if (!isset($model['target'])) {
849
                throw new \coding_exception('Missing target declaration');
850
            }
851
 
852
            if (!static::is_valid($model['target'], '\core_analytics\local\target\base')) {
853
                throw new \coding_exception('Invalid target classname', $model['target']);
854
            }
855
 
856
            if (empty($model['indicators']) || !is_array($model['indicators'])) {
857
                throw new \coding_exception('Missing indicators declaration');
858
            }
859
 
860
            foreach ($model['indicators'] as $indicator) {
861
                if (!static::is_valid($indicator, '\core_analytics\local\indicator\base')) {
862
                    throw new \coding_exception('Invalid indicator classname', $indicator);
863
                }
864
            }
865
 
866
            if (isset($model['timesplitting'])) {
867
                if (substr($model['timesplitting'], 0, 1) !== '\\') {
868
                    throw new \coding_exception('Expecting fully qualified time splitting classname', $model['timesplitting']);
869
                }
870
                if (!static::is_valid($model['timesplitting'], '\core_analytics\local\time_splitting\base')) {
871
                    throw new \coding_exception('Invalid time splitting classname', $model['timesplitting']);
872
                }
873
            }
874
 
875
            if (!empty($model['enabled']) && !isset($model['timesplitting'])) {
876
                throw new \coding_exception('Cannot enable a model without time splitting method specified');
877
            }
878
        }
879
    }
880
 
881
    /**
882
     * Create the defined model.
883
     *
884
     * @param array $definition See {@link self::validate_models_declaration()} for the syntax.
885
     * @return \core_analytics\model
886
     */
887
    public static function create_declared_model(array $definition): \core_analytics\model {
888
 
889
        list($target, $indicators) = static::get_declared_target_and_indicators_instances($definition);
890
 
891
        if (isset($definition['timesplitting'])) {
892
            $timesplitting = $definition['timesplitting'];
893
        } else {
894
            $timesplitting = false;
895
        }
896
 
897
        $created = \core_analytics\model::create($target, $indicators, $timesplitting);
898
 
899
        if (!empty($definition['enabled'])) {
900
            $created->enable();
901
        }
902
 
903
        return $created;
904
    }
905
 
906
    /**
907
     * Returns a string uniquely representing the given model declaration.
908
     *
909
     * @param array $model Model declaration
910
     * @return string complying with PARAM_ALPHANUM rules and starting with an 'id' prefix
911
     */
912
    public static function model_declaration_identifier(array $model): string {
913
        return 'id'.sha1(serialize($model));
914
    }
915
 
916
    /**
917
     * Given a model definition, return actual target and indicators instances.
918
     *
919
     * @param array $definition See {@link self::validate_models_declaration()} for the syntax.
920
     * @return array [0] => target instance, [1] => array of indicators instances
921
     */
922
    public static function get_declared_target_and_indicators_instances(array $definition): array {
923
 
924
        $target = static::get_target($definition['target']);
925
 
926
        $indicators = [];
927
 
928
        foreach ($definition['indicators'] as $indicatorname) {
929
            $indicator = static::get_indicator($indicatorname);
930
            $indicators[$indicator->get_id()] = $indicator;
931
        }
932
 
933
        return [$target, $indicators];
934
    }
935
 
936
    /**
937
     * Return the context restrictions that can be applied to the provided context levels.
938
     *
939
     * @throws \coding_exception
940
     * @param  array|null $contextlevels The list of context levels provided by the analyser. Null if all of them.
941
     * @param  string|null $query
942
     * @return array Associative array with contextid as key and the short version of the context name as value.
943
     */
944
    public static function get_potential_context_restrictions(?array $contextlevels = null, string $query = null) {
945
        global $DB;
946
 
947
        if (empty($contextlevels) && !is_null($contextlevels)) {
948
            return false;
949
        }
950
 
951
        if (!is_null($contextlevels)) {
952
            foreach ($contextlevels as $contextlevel) {
953
                if ($contextlevel !== CONTEXT_COURSE && $contextlevel !== CONTEXT_COURSECAT) {
954
                    throw new \coding_exception('Only CONTEXT_COURSE and CONTEXT_COURSECAT are supported at the moment.');
955
                }
956
            }
957
        }
958
 
959
        $contexts = [];
960
 
961
        // We have a separate process for each context level for performance reasons (to iterate through mdl_context calling
962
        // get_context_name() would be too slow).
963
        $contextsystem = \context_system::instance();
964
        if (is_null($contextlevels) || in_array(CONTEXT_COURSECAT, $contextlevels)) {
965
 
966
            $sql = "SELECT cc.id, cc.name, ctx.id AS contextid
967
                      FROM {course_categories} cc
968
                      JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = cc.id";
969
            $params = ['ctxlevel' => CONTEXT_COURSECAT];
970
 
971
            if ($query) {
972
                $sql .= " WHERE " . $DB->sql_like('cc.name', ':query', false, false);
973
                $params['query'] = '%' . $query . '%';
974
            }
975
 
976
            $coursecats = $DB->get_recordset_sql($sql, $params);
977
            foreach ($coursecats as $record) {
978
                $contexts[$record->contextid] = get_string('category') . ': ' .
979
                    format_string($record->name, true, array('context' => $contextsystem));
980
            }
981
            $coursecats->close();
982
        }
983
 
984
        if (is_null($contextlevels) || in_array(CONTEXT_COURSE, $contextlevels)) {
985
 
986
            $sql = "SELECT c.id, c.shortname, ctx.id AS contextid
987
                      FROM {course} c
988
                      JOIN {context} ctx ON ctx.contextlevel = :ctxlevel AND ctx.instanceid = c.id
989
                      WHERE c.id != :siteid";
990
            $params = ['ctxlevel' => CONTEXT_COURSE, 'siteid' => SITEID];
991
 
992
            if ($query) {
993
                $sql .= ' AND (' . $DB->sql_like('c.fullname', ':query1', false, false) . ' OR ' .
994
                    $DB->sql_like('c.shortname', ':query2', false, false) . ')';
995
                $params['query1'] = '%' . $query . '%';
996
                $params['query2'] = '%' . $query . '%';
997
            }
998
 
999
            $courses = $DB->get_recordset_sql($sql, $params);
1000
            foreach ($courses as $record) {
1001
                $contexts[$record->contextid] = get_string('course') . ': ' .
1002
                    format_string($record->shortname, true, array('context' => $contextsystem));
1003
            }
1004
            $courses->close();
1005
        }
1006
 
1007
        return $contexts;
1008
    }
1009
 
1010
}