MDL-57791 analytics: Predictions deleted context management
[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
38 /**
39 * @var \core_analytics\predictor[]
40 */
41 protected static $predictionprocessors = null;
42
43 /**
44 * @var \core_analytics\local\indicator\base[]
45 */
46 protected static $allindicators = null;
47
48 /**
49 * @var \core_analytics\local\time_splitting\base[]
50 */
51 protected static $alltimesplittings = null;
52
1611308b
DM
53 /**
54 * Checks that the user can manage models
55 *
56 * @throws \required_capability_exception
57 * @return void
58 */
59 public static function check_can_manage_models() {
60 require_capability('moodle/analytics:managemodels', \context_system::instance());
61 }
62
63 /**
64 * Checks that the user can list that context insights
65 *
66 * @throws \required_capability_exception
67 * @param \context $context
68 * @return void
69 */
70 public static function check_can_list_insights(\context $context) {
71 require_capability('moodle/analytics:listinsights', $context);
72 }
73
584ffa4f
DM
74 /**
75 * Returns all system models that match the provided filters.
76 *
77 * @param bool $enabled
78 * @param bool $trained
08015e18 79 * @param \context|false $predictioncontext
584ffa4f
DM
80 * @return \core_analytics\model[]
81 */
369389c9
DM
82 public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
83 global $DB;
84
1611308b
DM
85 $params = array();
86
466f683a
DM
87 $fields = 'am.id, am.enabled, am.trained, am.target, ' . $DB->sql_compare_text('am.indicators') .
88 ', am.timesplitting, am.version, am.timecreated, am.timemodified, am.usermodified';
89 $sql = "SELECT DISTINCT $fields FROM {analytics_models} am";
1611308b
DM
90 if ($predictioncontext) {
91 $sql .= " JOIN {analytics_predictions} ap ON ap.modelid = am.id AND ap.contextid = :contextid";
92 $params['contextid'] = $predictioncontext->id;
369389c9 93 }
1611308b
DM
94
95 if ($enabled || $trained) {
96 $conditions = [];
97 if ($enabled) {
98 $conditions[] = 'am.enabled = :enabled';
99 $params['enabled'] = 1;
100 }
101 if ($trained) {
102 $conditions[] = 'am.trained = :trained';
103 $params['trained'] = 1;
104 }
e10b29ed 105 $sql .= ' WHERE ' . implode(' AND ', $conditions);
369389c9 106 }
e10b29ed
DM
107 $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
108
1611308b 109 $modelobjs = $DB->get_records_sql($sql, $params);
369389c9
DM
110
111 $models = array();
112 foreach ($modelobjs as $modelobj) {
3a396286
DM
113 $model = new \core_analytics\model($modelobj);
114 if ($model->is_available()) {
115 $models[$modelobj->id] = $model;
116 }
369389c9
DM
117 }
118 return $models;
119 }
120
121 /**
122 * Returns the site selected predictions processor.
123 *
124 * @param string $predictionclass
125 * @param bool $checkisready
126 * @return \core_analytics\predictor
127 */
128 public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
129
130 // We want 0 or 1 so we can use it as an array key for caching.
131 $checkisready = intval($checkisready);
132
133 if ($predictionclass === false) {
134 $predictionclass = get_config('analytics', 'predictionsprocessor');
135 }
136
137 if (empty($predictionclass)) {
138 // Use the default one if nothing set.
139 $predictionclass = '\mlbackend_php\processor';
140 }
141
142 if (!class_exists($predictionclass)) {
143 throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
144 }
145
146 $interfaces = class_implements($predictionclass);
147 if (empty($interfaces['core_analytics\predictor'])) {
148 throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
149 }
150
151 // Return it from the cached list.
152 if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
153
154 $instance = new $predictionclass();
155 if ($checkisready) {
156 $isready = $instance->is_ready();
157 if ($isready !== true) {
158 throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
159 }
160 }
161 self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
162 }
163
164 return self::$predictionprocessors[$checkisready][$predictionclass];
165 }
166
1611308b
DM
167 /**
168 * Return all system predictions processors.
169 *
08015e18 170 * @return \core_analytics\predictor[]
1611308b 171 */
369389c9
DM
172 public static function get_all_prediction_processors() {
173
174 $mlbackends = \core_component::get_plugin_list('mlbackend');
175
176 $predictionprocessors = array();
177 foreach ($mlbackends as $mlbackend => $unused) {
206d7aa9 178 $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
369389c9
DM
179 $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
180 }
181 return $predictionprocessors;
182 }
183
184 /**
185 * Get all available time splitting methods.
186 *
1cc2b4ba 187 * @return \core_analytics\local\time_splitting\base[]
369389c9
DM
188 */
189 public static function get_all_time_splittings() {
190 if (self::$alltimesplittings !== null) {
191 return self::$alltimesplittings;
192 }
193
194 $classes = self::get_analytics_classes('time_splitting');
195
196 self::$alltimesplittings = [];
197 foreach ($classes as $fullclassname => $classpath) {
198 $instance = self::get_time_splitting($fullclassname);
199 // We need to check that it is a valid time splitting method, it may be an abstract class.
200 if ($instance) {
201 self::$alltimesplittings[$instance->get_id()] = $instance;
202 }
203 }
204
205 return self::$alltimesplittings;
206 }
207
208 /**
209 * Returns the enabled time splitting methods.
210 *
211 * @return \core_analytics\local\time_splitting\base[]
212 */
213 public static function get_enabled_time_splitting_methods() {
214
215 if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
216 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
217 }
218
219 $timesplittings = self::get_all_time_splittings();
220 foreach ($timesplittings as $key => $timesplitting) {
221
222 // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
223 if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
224 unset($timesplittings[$key]);
225 }
226 }
227 return $timesplittings;
228 }
229
230 /**
231 * Returns a time splitting method by its classname.
232 *
233 * @param string $fullclassname
234 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
235 */
236 public static function get_time_splitting($fullclassname) {
237 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
238 return false;
239 }
240 return new $fullclassname();
241 }
242
243 /**
244 * Return all system indicators.
245 *
246 * @return \core_analytics\local\indicator\base[]
247 */
248 public static function get_all_indicators() {
249 if (self::$allindicators !== null) {
250 return self::$allindicators;
251 }
252
253 $classes = self::get_analytics_classes('indicator');
254
255 self::$allindicators = [];
256 foreach ($classes as $fullclassname => $classpath) {
257 $instance = self::get_indicator($fullclassname);
258 if ($instance) {
259 // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
b0c24929 260 self::$allindicators[$instance->get_id()] = $instance;
369389c9
DM
261 }
262 }
263
264 return self::$allindicators;
265 }
266
1611308b
DM
267 /**
268 * Returns the specified target
269 *
270 * @param mixed $fullclassname
271 * @return \core_analytics\local\target\base|false False if it is not valid
272 */
369389c9
DM
273 public static function get_target($fullclassname) {
274 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
275 return false;
276 }
277 return new $fullclassname();
278 }
279
280 /**
281 * Returns an instance of the provided indicator.
282 *
283 * @param string $fullclassname
284 * @return \core_analytics\local\indicator\base|false False if it is not valid.
285 */
286 public static function get_indicator($fullclassname) {
287 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
288 return false;
289 }
290 return new $fullclassname();
291 }
292
293 /**
294 * Returns whether a time splitting method is valid or not.
295 *
296 * @param string $fullclassname
1611308b 297 * @param string $baseclass
369389c9
DM
298 * @return bool
299 */
300 public static function is_valid($fullclassname, $baseclass) {
301 if (is_subclass_of($fullclassname, $baseclass)) {
302 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
303 return true;
304 }
305 }
306 return false;
307 }
308
f67f35f3 309 /**
1611308b 310 * Returns the logstore used for analytics.
f67f35f3 311 *
2db6e981 312 * @return \core\log\sql_reader|false False if no log stores are enabled.
f67f35f3
DM
313 */
314 public static function get_analytics_logstore() {
315 $readers = get_log_manager()->get_readers('core\log\sql_reader');
316 $analyticsstore = get_config('analytics', 'logstore');
85e50ba4
DM
317
318 if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
f67f35f3 319 $logstore = $readers[$analyticsstore];
85e50ba4
DM
320 } else if (empty($analyticsstore) && !empty($readers)) {
321 // The first one, it is the same default than in settings.
322 $logstore = reset($readers);
2db6e981 323 } else if (!empty($readers)) {
f67f35f3
DM
324 $logstore = reset($readers);
325 debugging('The selected log store for analytics is not available anymore. Using "' .
326 $logstore->get_name() . '"', DEBUG_DEVELOPER);
327 }
328
2db6e981
DM
329 if (empty($logstore)) {
330 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
331 return false;
332 }
333
f67f35f3
DM
334 if (!$logstore->is_logging()) {
335 debugging('The selected log store for analytics "' . $logstore->get_name() .
336 '" is not logging activity logs', DEBUG_DEVELOPER);
337 }
338
339 return $logstore;
340 }
341
1611308b
DM
342 /**
343 * Returns the models with insights at the provided context.
344 *
345 * @param \context $context
346 * @return \core_analytics\model[]
347 */
348 public static function get_models_with_insights(\context $context) {
349
350 self::check_can_list_insights($context);
351
413f19bc 352 $models = self::get_all_models(true, true, $context);
1611308b
DM
353 foreach ($models as $key => $model) {
354 // Check that it not only have predictions but also generates insights from them.
355 if (!$model->uses_insights()) {
356 unset($models[$key]);
357 }
358 }
359 return $models;
360 }
361
362 /**
363 * Returns a prediction
364 *
365 * @param int $predictionid
366 * @param bool $requirelogin
367 * @return array array($model, $prediction, $context)
368 */
369 public static function get_prediction($predictionid, $requirelogin = false) {
370 global $DB;
371
372 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
4a210b06
DM
373 throw new \moodle_exception('errorpredictionnotfound', 'analytics');
374 }
375
376 $context = \context::instance_by_id($predictionobj->contextid, IGNORE_MISSING);
377 if (!$context) {
378 throw new \moodle_exception('errorpredictioncontextnotavailable', 'analytics');
1611308b
DM
379 }
380
381 if ($requirelogin) {
382 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
383 require_login($course, false, $cm);
1611308b
DM
384 }
385
413f19bc 386 self::check_can_list_insights($context);
1611308b
DM
387
388 $model = new \core_analytics\model($predictionobj->modelid);
389 $sampledata = $model->prediction_sample_data($predictionobj);
390 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
391
392 return array($model, $prediction, $context);
393 }
394
e709e544
DM
395 /**
396 * Adds the models included with moodle core to the system.
397 *
398 * @return void
399 */
400 public static function add_builtin_models() {
401
690ad875 402 $target = self::get_target('\core\analytics\target\course_dropout');
e709e544
DM
403
404 // Community of inquiry indicators.
405 $coiindicators = array(
406 '\mod_assign\analytics\indicator\cognitive_depth',
407 '\mod_assign\analytics\indicator\social_breadth',
408 '\mod_book\analytics\indicator\cognitive_depth',
409 '\mod_book\analytics\indicator\social_breadth',
410 '\mod_chat\analytics\indicator\cognitive_depth',
411 '\mod_chat\analytics\indicator\social_breadth',
412 '\mod_choice\analytics\indicator\cognitive_depth',
413 '\mod_choice\analytics\indicator\social_breadth',
414 '\mod_data\analytics\indicator\cognitive_depth',
415 '\mod_data\analytics\indicator\social_breadth',
416 '\mod_feedback\analytics\indicator\cognitive_depth',
417 '\mod_feedback\analytics\indicator\social_breadth',
418 '\mod_folder\analytics\indicator\cognitive_depth',
419 '\mod_folder\analytics\indicator\social_breadth',
420 '\mod_forum\analytics\indicator\cognitive_depth',
421 '\mod_forum\analytics\indicator\social_breadth',
422 '\mod_glossary\analytics\indicator\cognitive_depth',
423 '\mod_glossary\analytics\indicator\social_breadth',
424 '\mod_imscp\analytics\indicator\cognitive_depth',
425 '\mod_imscp\analytics\indicator\social_breadth',
426 '\mod_label\analytics\indicator\cognitive_depth',
427 '\mod_label\analytics\indicator\social_breadth',
428 '\mod_lesson\analytics\indicator\cognitive_depth',
429 '\mod_lesson\analytics\indicator\social_breadth',
430 '\mod_lti\analytics\indicator\cognitive_depth',
431 '\mod_lti\analytics\indicator\social_breadth',
432 '\mod_page\analytics\indicator\cognitive_depth',
433 '\mod_page\analytics\indicator\social_breadth',
434 '\mod_quiz\analytics\indicator\cognitive_depth',
435 '\mod_quiz\analytics\indicator\social_breadth',
436 '\mod_resource\analytics\indicator\cognitive_depth',
437 '\mod_resource\analytics\indicator\social_breadth',
438 '\mod_scorm\analytics\indicator\cognitive_depth',
439 '\mod_scorm\analytics\indicator\social_breadth',
440 '\mod_survey\analytics\indicator\cognitive_depth',
441 '\mod_survey\analytics\indicator\social_breadth',
442 '\mod_url\analytics\indicator\cognitive_depth',
443 '\mod_url\analytics\indicator\social_breadth',
444 '\mod_wiki\analytics\indicator\cognitive_depth',
445 '\mod_wiki\analytics\indicator\social_breadth',
446 '\mod_workshop\analytics\indicator\cognitive_depth',
447 '\mod_workshop\analytics\indicator\social_breadth',
448 );
449 $indicators = array();
450 foreach ($coiindicators as $coiindicator) {
690ad875 451 $indicator = self::get_indicator($coiindicator);
e709e544
DM
452 $indicators[$indicator->get_id()] = $indicator;
453 }
454 if (!\core_analytics\model::exists($target, $indicators)) {
455 $model = \core_analytics\model::create($target, $indicators);
456 }
457
458 // No teaching model.
690ad875 459 $target = self::get_target('\core\analytics\target\no_teaching');
e709e544 460 $timesplittingmethod = '\core\analytics\time_splitting\single_range';
690ad875 461 $noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
e709e544
DM
462 $indicators = array($noteacher->get_id() => $noteacher);
463 if (!\core_analytics\model::exists($target, $indicators)) {
464 \core_analytics\model::create($target, $indicators, $timesplittingmethod);
465 }
466 }
467
369389c9
DM
468 /**
469 * Returns the provided element classes in the site.
470 *
471 * @param string $element
472 * @return string[] Array keys are the FQCN and the values the class path.
473 */
474 private static function get_analytics_classes($element) {
475
476 // Just in case...
1611308b 477 $element = clean_param($element, PARAM_ALPHANUMEXT);
369389c9 478
206d7aa9
DM
479 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
480 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
481
482 // Plugins.
369389c9
DM
483 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
484 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
485 $frankenstyle = $type . '_' . $pluginname;
486 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
487 }
488 }
1cc2b4ba 489
206d7aa9 490 // Core subsystems.
1cc2b4ba
DM
491 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
492 $componentname = 'core_' . $subsystemname;
493 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
494 }
495
369389c9
DM
496 return $classes;
497 }
498}