MDL-64783 analytics: Reset PAGE after analytics processes
[moodle.git] / analytics / classes / manager.php
CommitLineData
369389c9
DM
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/**
b94dbb55 18 * Analytics basic actions manager.
369389c9
DM
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
25namespace core_analytics;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
b94dbb55 30 * Analytics basic actions manager.
369389c9
DM
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 */
36class manager {
37
ed12ba6b
DM
38 /**
39 * Default mlbackend
40 */
41 const DEFAULT_MLBACKEND = '\mlbackend_php\processor';
42
f0584045
DM
43 /**
44 * Name of the file where components declare their models.
45 */
46 const ANALYTICS_FILENAME = 'db/analytics.php';
47
369389c9
DM
48 /**
49 * @var \core_analytics\predictor[]
50 */
51 protected static $predictionprocessors = null;
52
e4453adc
DM
53 /**
54 * @var \core_analytics\local\target\base[]
55 */
56 protected static $alltargets = null;
57
369389c9
DM
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
1611308b
DM
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 * @return void
84 */
85 public static function check_can_list_insights(\context $context) {
86 require_capability('moodle/analytics:listinsights', $context);
87 }
88
584ffa4f
DM
89 /**
90 * Returns all system models that match the provided filters.
91 *
92 * @param bool $enabled
93 * @param bool $trained
08015e18 94 * @param \context|false $predictioncontext
584ffa4f
DM
95 * @return \core_analytics\model[]
96 */
369389c9
DM
97 public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
98 global $DB;
99
1611308b
DM
100 $params = array();
101
88777c57 102 $sql = "SELECT am.* FROM {analytics_models} am";
1611308b 103
88777c57 104 if ($enabled || $trained || $predictioncontext) {
1611308b
DM
105 $conditions = [];
106 if ($enabled) {
107 $conditions[] = 'am.enabled = :enabled';
108 $params['enabled'] = 1;
109 }
110 if ($trained) {
111 $conditions[] = 'am.trained = :trained';
112 $params['trained'] = 1;
113 }
88777c57 114 if ($predictioncontext) {
d887b0c7
DM
115 $conditions[] = "EXISTS (SELECT 'x'
116 FROM {analytics_predictions} ap
117 WHERE ap.modelid = am.id AND ap.contextid = :contextid)";
88777c57
DM
118 $params['contextid'] = $predictioncontext->id;
119 }
e10b29ed 120 $sql .= ' WHERE ' . implode(' AND ', $conditions);
369389c9 121 }
e10b29ed
DM
122 $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
123
1611308b 124 $modelobjs = $DB->get_records_sql($sql, $params);
369389c9
DM
125
126 $models = array();
127 foreach ($modelobjs as $modelobj) {
3a396286
DM
128 $model = new \core_analytics\model($modelobj);
129 if ($model->is_available()) {
130 $models[$modelobj->id] = $model;
131 }
369389c9
DM
132 }
133 return $models;
134 }
135
136 /**
ed12ba6b 137 * Returns the provided predictions processor class.
369389c9 138 *
ed12ba6b 139 * @param false|string $predictionclass Returns the system default processor if false
369389c9
DM
140 * @param bool $checkisready
141 * @return \core_analytics\predictor
142 */
143 public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
144
145 // We want 0 or 1 so we can use it as an array key for caching.
146 $checkisready = intval($checkisready);
147
ed12ba6b 148 if (!$predictionclass) {
369389c9
DM
149 $predictionclass = get_config('analytics', 'predictionsprocessor');
150 }
151
152 if (empty($predictionclass)) {
153 // Use the default one if nothing set.
ed12ba6b 154 $predictionclass = self::default_mlbackend();
369389c9
DM
155 }
156
157 if (!class_exists($predictionclass)) {
158 throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
159 }
160
161 $interfaces = class_implements($predictionclass);
162 if (empty($interfaces['core_analytics\predictor'])) {
163 throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
164 }
165
166 // Return it from the cached list.
167 if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
168
169 $instance = new $predictionclass();
170 if ($checkisready) {
171 $isready = $instance->is_ready();
172 if ($isready !== true) {
173 throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
174 }
175 }
176 self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
177 }
178
179 return self::$predictionprocessors[$checkisready][$predictionclass];
180 }
181
1611308b
DM
182 /**
183 * Return all system predictions processors.
184 *
08015e18 185 * @return \core_analytics\predictor[]
1611308b 186 */
369389c9
DM
187 public static function get_all_prediction_processors() {
188
189 $mlbackends = \core_component::get_plugin_list('mlbackend');
190
191 $predictionprocessors = array();
192 foreach ($mlbackends as $mlbackend => $unused) {
206d7aa9 193 $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
369389c9
DM
194 $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
195 }
196 return $predictionprocessors;
197 }
198
ed12ba6b
DM
199 /**
200 * Returns the name of the provided predictions processor.
201 *
202 * @param \core_analytics\predictor $predictionsprocessor
203 * @return string
204 */
205 public static function get_predictions_processor_name(\core_analytics\predictor $predictionsprocessor) {
206 $component = substr(get_class($predictionsprocessor), 0, strpos(get_class($predictionsprocessor), '\\', 1));
207 return get_string('pluginname', $component);
208 }
209
210 /**
211 * Whether the provided plugin is used by any model.
212 *
213 * @param string $plugin
214 * @return bool
215 */
216 public static function is_mlbackend_used($plugin) {
217 $models = self::get_all_models();
218 foreach ($models as $model) {
219 $processor = $model->get_predictions_processor();
220 $noprefixnamespace = ltrim(get_class($processor), '\\');
221 $processorplugin = substr($noprefixnamespace, 0, strpos($noprefixnamespace, '\\'));
222 if ($processorplugin == $plugin) {
223 return true;
224 }
225 }
226
227 // Default predictions processor.
228 $defaultprocessorclass = get_config('analytics', 'predictionsprocessor');
229 $pluginclass = '\\' . $plugin . '\\processor';
230 if ($pluginclass === $defaultprocessorclass) {
231 return true;
232 }
233
234 return false;
235 }
236
369389c9
DM
237 /**
238 * Get all available time splitting methods.
239 *
1cc2b4ba 240 * @return \core_analytics\local\time_splitting\base[]
369389c9
DM
241 */
242 public static function get_all_time_splittings() {
243 if (self::$alltimesplittings !== null) {
244 return self::$alltimesplittings;
245 }
246
247 $classes = self::get_analytics_classes('time_splitting');
248
249 self::$alltimesplittings = [];
250 foreach ($classes as $fullclassname => $classpath) {
251 $instance = self::get_time_splitting($fullclassname);
252 // We need to check that it is a valid time splitting method, it may be an abstract class.
253 if ($instance) {
254 self::$alltimesplittings[$instance->get_id()] = $instance;
255 }
256 }
257
258 return self::$alltimesplittings;
259 }
260
261 /**
262 * Returns the enabled time splitting methods.
263 *
3576b66b
DM
264 * @deprecated since Moodle 3.7
265 * @todo MDL-65086 This will be deleted in Moodle 4.1
266 * @see \core_analytics\manager::get_time_splitting_methods_for_evaluation
369389c9
DM
267 * @return \core_analytics\local\time_splitting\base[]
268 */
269 public static function get_enabled_time_splitting_methods() {
3576b66b
DM
270 debugging('This function has been deprecated. You can use self::get_time_splitting_methods_for_evaluation if ' .
271 'you want to get the default time splitting methods for evaluation, or you can use self::get_all_time_splittings if ' .
272 'you want to get all the time splitting methods available on this site.');
273 return self::get_time_splitting_methods_for_evaluation();
274 }
275
276 /**
277 * Returns the default time splitting methods for model evaluation.
278 *
279 * @return \core_analytics\local\time_splitting\base[]
280 */
281 public static function get_time_splitting_methods_for_evaluation() {
369389c9 282
3576b66b 283 if ($enabledtimesplittings = get_config('analytics', 'defaulttimesplittingsevaluation')) {
369389c9
DM
284 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
285 }
286
287 $timesplittings = self::get_all_time_splittings();
288 foreach ($timesplittings as $key => $timesplitting) {
289
290 // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
291 if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
292 unset($timesplittings[$key]);
293 }
294 }
295 return $timesplittings;
296 }
297
298 /**
299 * Returns a time splitting method by its classname.
300 *
301 * @param string $fullclassname
302 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
303 */
304 public static function get_time_splitting($fullclassname) {
305 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
306 return false;
307 }
308 return new $fullclassname();
309 }
310
e4453adc
DM
311 /**
312 * Return all targets in the system.
313 *
314 * @return \core_analytics\local\target\base[]
315 */
316 public static function get_all_targets() : array {
317 if (self::$alltargets !== null) {
318 return self::$alltargets;
319 }
320
321 $classes = self::get_analytics_classes('target');
322
323 self::$alltargets = [];
324 foreach ($classes as $fullclassname => $classpath) {
325 $instance = self::get_target($fullclassname);
326 if ($instance) {
327 self::$alltargets[$instance->get_id()] = $instance;
328 }
329 }
330
331 return self::$alltargets;
332 }
369389c9
DM
333 /**
334 * Return all system indicators.
335 *
336 * @return \core_analytics\local\indicator\base[]
337 */
338 public static function get_all_indicators() {
339 if (self::$allindicators !== null) {
340 return self::$allindicators;
341 }
342
343 $classes = self::get_analytics_classes('indicator');
344
345 self::$allindicators = [];
346 foreach ($classes as $fullclassname => $classpath) {
347 $instance = self::get_indicator($fullclassname);
348 if ($instance) {
b0c24929 349 self::$allindicators[$instance->get_id()] = $instance;
369389c9
DM
350 }
351 }
352
353 return self::$allindicators;
354 }
355
1611308b
DM
356 /**
357 * Returns the specified target
358 *
359 * @param mixed $fullclassname
360 * @return \core_analytics\local\target\base|false False if it is not valid
361 */
369389c9
DM
362 public static function get_target($fullclassname) {
363 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
364 return false;
365 }
366 return new $fullclassname();
367 }
368
369 /**
370 * Returns an instance of the provided indicator.
371 *
372 * @param string $fullclassname
373 * @return \core_analytics\local\indicator\base|false False if it is not valid.
374 */
375 public static function get_indicator($fullclassname) {
376 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
377 return false;
378 }
379 return new $fullclassname();
380 }
381
382 /**
383 * Returns whether a time splitting method is valid or not.
384 *
385 * @param string $fullclassname
1611308b 386 * @param string $baseclass
369389c9
DM
387 * @return bool
388 */
389 public static function is_valid($fullclassname, $baseclass) {
390 if (is_subclass_of($fullclassname, $baseclass)) {
391 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
392 return true;
393 }
394 }
395 return false;
396 }
397
f67f35f3 398 /**
1611308b 399 * Returns the logstore used for analytics.
f67f35f3 400 *
2db6e981 401 * @return \core\log\sql_reader|false False if no log stores are enabled.
f67f35f3
DM
402 */
403 public static function get_analytics_logstore() {
404 $readers = get_log_manager()->get_readers('core\log\sql_reader');
405 $analyticsstore = get_config('analytics', 'logstore');
85e50ba4
DM
406
407 if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
f67f35f3 408 $logstore = $readers[$analyticsstore];
85e50ba4
DM
409 } else if (empty($analyticsstore) && !empty($readers)) {
410 // The first one, it is the same default than in settings.
411 $logstore = reset($readers);
2db6e981 412 } else if (!empty($readers)) {
f67f35f3
DM
413 $logstore = reset($readers);
414 debugging('The selected log store for analytics is not available anymore. Using "' .
415 $logstore->get_name() . '"', DEBUG_DEVELOPER);
416 }
417
2db6e981
DM
418 if (empty($logstore)) {
419 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
420 return false;
421 }
422
f67f35f3
DM
423 if (!$logstore->is_logging()) {
424 debugging('The selected log store for analytics "' . $logstore->get_name() .
425 '" is not logging activity logs', DEBUG_DEVELOPER);
426 }
427
428 return $logstore;
429 }
430
0690a271
DM
431 /**
432 * Returns this analysable calculations during the provided period.
433 *
434 * @param \core_analytics\analysable $analysable
435 * @param int $starttime
436 * @param int $endtime
437 * @param string $samplesorigin The samples origin as sampleid is not unique across models.
438 * @return array
439 */
440 public static function get_indicator_calculations($analysable, $starttime, $endtime, $samplesorigin) {
441 global $DB;
442
443 $params = array('starttime' => $starttime, 'endtime' => $endtime, 'contextid' => $analysable->get_context()->id,
444 'sampleorigin' => $samplesorigin);
445 $calculations = $DB->get_recordset('analytics_indicator_calc', $params, '', 'indicator, sampleid, value');
446
447 $existingcalculations = array();
448 foreach ($calculations as $calculation) {
449 if (empty($existingcalculations[$calculation->indicator])) {
450 $existingcalculations[$calculation->indicator] = array();
451 }
452 $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
453 }
a938e409 454 $calculations->close();
0690a271
DM
455 return $existingcalculations;
456 }
457
1611308b
DM
458 /**
459 * Returns the models with insights at the provided context.
460 *
461 * @param \context $context
462 * @return \core_analytics\model[]
463 */
464 public static function get_models_with_insights(\context $context) {
465
466 self::check_can_list_insights($context);
467
413f19bc 468 $models = self::get_all_models(true, true, $context);
1611308b
DM
469 foreach ($models as $key => $model) {
470 // Check that it not only have predictions but also generates insights from them.
471 if (!$model->uses_insights()) {
472 unset($models[$key]);
473 }
474 }
475 return $models;
476 }
477
478 /**
479 * Returns a prediction
480 *
481 * @param int $predictionid
482 * @param bool $requirelogin
483 * @return array array($model, $prediction, $context)
484 */
485 public static function get_prediction($predictionid, $requirelogin = false) {
486 global $DB;
487
488 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
4a210b06
DM
489 throw new \moodle_exception('errorpredictionnotfound', 'analytics');
490 }
491
492 $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
493 if (!$context) {
494 throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
1611308b
DM
495 }
496
497 if ($requirelogin) {
498 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
499 require_login($course, false, $cm);
1611308b
DM
500 }
501
413f19bc 502 self::check_can_list_insights($context);
1611308b
DM
503
504 $model = new \core_analytics\model($predictionobj->modelid);
505 $sampledata = $model->prediction_sample_data($predictionobj);
506 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
507
508 return array($model, $prediction, $context);
509 }
510
e709e544 511 /**
3b7c7918 512 * Used to be used to add models included with the Moodle core.
e709e544 513 *
3b7c7918
DM
514 * @deprecated Deprecated since Moodle 3.7 (MDL-61667) - Use lib/db/analytics.php instead.
515 * @todo Remove this method in Moodle 4.1 (MDL-65186).
e709e544
DM
516 * @return void
517 */
518 public static function add_builtin_models() {
519
3b7c7918
DM
520 debugging('core_analytics\manager::add_builtin_models() has been deprecated. Core models are now automatically '.
521 'updated according to their declaration in the lib/db/analytics.php file.', DEBUG_DEVELOPER);
e709e544
DM
522 }
523
f9222c49
DM
524 /**
525 * Cleans up analytics db tables that do not directly depend on analysables that may have been deleted.
526 */
527 public static function cleanup() {
528 global $DB;
529
530 // Clean up stuff that depends on contexts that do not exist anymore.
531 $sql = "SELECT DISTINCT ap.contextid FROM {analytics_predictions} ap
532 LEFT JOIN {context} ctx ON ap.contextid = ctx.id
533 WHERE ctx.id IS NULL";
534 $apcontexts = $DB->get_records_sql($sql);
535
536 $sql = "SELECT DISTINCT aic.contextid FROM {analytics_indicator_calc} aic
537 LEFT JOIN {context} ctx ON aic.contextid = ctx.id
538 WHERE ctx.id IS NULL";
539 $indcalccontexts = $DB->get_records_sql($sql);
540
541 $contexts = $apcontexts + $indcalccontexts;
542 if ($contexts) {
543 list($sql, $params) = $DB->get_in_or_equal(array_keys($contexts));
e08c74f5 544 $DB->execute("DELETE FROM {analytics_prediction_actions} WHERE predictionid IN
f9222c49
DM
545 (SELECT ap.id FROM {analytics_predictions} ap WHERE ap.contextid $sql)", $params);
546
547 $DB->delete_records_select('analytics_predictions', "contextid $sql", $params);
548 $DB->delete_records_select('analytics_indicator_calc', "contextid $sql", $params);
549 }
550
551 // Clean up stuff that depends on analysable ids that do not exist anymore.
552 $models = self::get_all_models();
553 foreach ($models as $model) {
554 $analyser = $model->get_analyser(array('notimesplitting' => true));
555 $analysables = $analyser->get_analysables();
556 if (!$analysables) {
557 continue;
558 }
559
560 $analysableids = array_map(function($analysable) {
561 return $analysable->get_id();
562 }, $analysables);
563
564 list($notinsql, $params) = $DB->get_in_or_equal($analysableids, SQL_PARAMS_NAMED, 'param', false);
565 $params['modelid'] = $model->get_id();
566
567 $DB->delete_records_select('analytics_predict_samples', "modelid = :modelid AND analysableid $notinsql", $params);
568 $DB->delete_records_select('analytics_train_samples', "modelid = :modelid AND analysableid $notinsql", $params);
569 }
570 }
571
ed12ba6b
DM
572 /**
573 * Default system backend.
574 *
575 * @return string
576 */
577 public static function default_mlbackend() {
578 return self::DEFAULT_MLBACKEND;
579 }
580
369389c9
DM
581 /**
582 * Returns the provided element classes in the site.
583 *
584 * @param string $element
585 * @return string[] Array keys are the FQCN and the values the class path.
586 */
587 private static function get_analytics_classes($element) {
588
589 // Just in case...
1611308b 590 $element = clean_param($element, PARAM_ALPHANUMEXT);
369389c9 591
206d7aa9
DM
592 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
593 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
594
595 // Plugins.
369389c9
DM
596 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
597 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
598 $frankenstyle = $type . '_' . $pluginname;
599 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
600 }
601 }
1cc2b4ba 602
206d7aa9 603 // Core subsystems.
1cc2b4ba
DM
604 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
605 $componentname = 'core_' . $subsystemname;
606 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
607 }
608
369389c9
DM
609 return $classes;
610 }
f0584045 611
606c3be0
DM
612 /**
613 * Check that all the models declared by the component are up to date.
614 *
615 * This is intended to be called during the installation / upgrade to automatically create missing models.
616 *
617 * @param string $componentname The name of the component to load models for.
618 * @return array \core_analytics\model[] List of actually created models.
619 */
620 public static function update_default_models_for_component(string $componentname): array {
621
622 $result = [];
623
624 foreach (static::load_default_models_for_component($componentname) as $definition) {
625 if (!\core_analytics\model::exists(static::get_target($definition['target']))) {
626 $result[] = static::create_declared_model($definition);
627 }
628 }
629
630 return $result;
631 }
632
f0584045
DM
633 /**
634 * Return the list of models declared by the given component.
635 *
636 * @param string $componentname The name of the component to load models for.
637 * @throws \coding_exception Exception thrown in case of invalid syntax.
638 * @return array The $models description array.
639 */
640 public static function load_default_models_for_component(string $componentname): array {
641
642 $dir = \core_component::get_component_directory($componentname);
643
644 if (!$dir) {
645 // This is either an invalid component, or a core subsystem without its own root directory.
646 return [];
647 }
648
649 $file = $dir . '/' . self::ANALYTICS_FILENAME;
650
651 if (!is_readable($file)) {
652 return [];
653 }
654
655 $models = null;
656 include($file);
657
658 if (!isset($models) || !is_array($models) || empty($models)) {
659 return [];
660 }
661
662 foreach ($models as &$model) {
663 if (!isset($model['enabled'])) {
664 $model['enabled'] = false;
665 } else {
666 $model['enabled'] = clean_param($model['enabled'], PARAM_BOOL);
667 }
668 }
669
670 static::validate_models_declaration($models);
671
672 return $models;
673 }
674
675 /**
676 * Validate the declaration of prediction models according the syntax expected in the component's db folder.
677 *
678 * The expected structure looks like this:
679 *
680 * [
681 * [
682 * 'target' => '\fully\qualified\name\of\the\target\class',
683 * 'indicators' => [
684 * '\fully\qualified\name\of\the\first\indicator',
685 * '\fully\qualified\name\of\the\second\indicator',
686 * ],
687 * 'timesplitting' => '\optional\name\of\the\time_splitting\class',
688 * 'enabled' => true,
689 * ],
690 * ];
691 *
692 * @param array $models List of declared models.
693 * @throws \coding_exception Exception thrown in case of invalid syntax.
694 */
695 public static function validate_models_declaration(array $models) {
696
697 foreach ($models as $model) {
698 if (!isset($model['target'])) {
699 throw new \coding_exception('Missing target declaration');
700 }
701
702 if (!static::is_valid($model['target'], '\core_analytics\local\target\base')) {
703 throw new \coding_exception('Invalid target classname', $model['target']);
704 }
705
706 if (empty($model['indicators']) || !is_array($model['indicators'])) {
707 throw new \coding_exception('Missing indicators declaration');
708 }
709
710 foreach ($model['indicators'] as $indicator) {
711 if (!static::is_valid($indicator, '\core_analytics\local\indicator\base')) {
712 throw new \coding_exception('Invalid indicator classname', $indicator);
713 }
714 }
715
716 if (isset($model['timesplitting'])) {
717 if (substr($model['timesplitting'], 0, 1) !== '\\') {
718 throw new \coding_exception('Expecting fully qualified time splitting classname', $model['timesplitting']);
719 }
720 if (!static::is_valid($model['timesplitting'], '\core_analytics\local\time_splitting\base')) {
721 throw new \coding_exception('Invalid time splitting classname', $model['timesplitting']);
722 }
723 }
724
725 if (!empty($model['enabled']) && !isset($model['timesplitting'])) {
726 throw new \coding_exception('Cannot enable a model without time splitting method specified');
727 }
728 }
729 }
6187213f
DM
730
731 /**
732 * Create the defined model.
733 *
734 * @param array $definition See {@link self::validate_models_declaration()} for the syntax.
735 * @return \core_analytics\model
736 */
737 public static function create_declared_model(array $definition): \core_analytics\model {
738
739 $target = static::get_target($definition['target']);
740
741 $indicators = [];
742
743 foreach ($definition['indicators'] as $indicatorname) {
744 $indicator = static::get_indicator($indicatorname);
745 $indicators[$indicator->get_id()] = $indicator;
746 }
747
748 if (isset($definition['timesplitting'])) {
749 $timesplitting = $definition['timesplitting'];
750 } else {
751 $timesplitting = false;
752 }
753
754 $created = \core_analytics\model::create($target, $indicators, $timesplitting);
755
756 if (!empty($definition['enabled'])) {
757 $created->enable();
758 }
759
760 return $created;
761 }
369389c9 762}