152f5a7658229a7e63d0dac5a7c3885988dd5337
[moodle.git] / analytics / classes / manager.php
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/>.
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  */
25 namespace core_analytics;
27 defined('MOODLE_INTERNAL') || die();
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 {
38     /**
39      * @var \core_analytics\predictor[]
40      */
41     protected static $predictionprocessors = null;
43     /**
44      * @var \core_analytics\local\indicator\base[]
45      */
46     protected static $allindicators = null;
48     /**
49      * @var \core_analytics\local\time_splitting\base[]
50      */
51     protected static $alltimesplittings = null;
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     }
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     }
74     /**
75      * Returns all system models that match the provided filters.
76      *
77      * @param bool $enabled
78      * @param bool $trained
79      * @param \context|false $predictioncontext
80      * @return \core_analytics\model[]
81      */
82     public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
83         global $DB;
85         $params = array();
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";
90         if ($predictioncontext) {
91             $sql .= " JOIN {analytics_predictions} ap ON ap.modelid = am.id AND ap.contextid = :contextid";
92             $params['contextid'] = $predictioncontext->id;
93         }
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             }
105             $sql .= ' WHERE ' . implode(' AND ', $conditions);
106         }
107         $sql .= ' ORDER BY am.enabled DESC, am.timemodified DESC';
109         $modelobjs = $DB->get_records_sql($sql, $params);
111         $models = array();
112         foreach ($modelobjs as $modelobj) {
113             $model = new \core_analytics\model($modelobj);
114             if ($model->is_available()) {
115                 $models[$modelobj->id] = $model;
116             }
117         }
118         return $models;
119     }
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) {
130         // We want 0 or 1 so we can use it as an array key for caching.
131         $checkisready = intval($checkisready);
133         if ($predictionclass === false) {
134             $predictionclass = get_config('analytics', 'predictionsprocessor');
135         }
137         if (empty($predictionclass)) {
138             // Use the default one if nothing set.
139             $predictionclass = '\mlbackend_php\processor';
140         }
142         if (!class_exists($predictionclass)) {
143             throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
144         }
146         $interfaces = class_implements($predictionclass);
147         if (empty($interfaces['core_analytics\predictor'])) {
148             throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
149         }
151         // Return it from the cached list.
152         if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
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         }
164         return self::$predictionprocessors[$checkisready][$predictionclass];
165     }
167     /**
168      * Return all system predictions processors.
169      *
170      * @return \core_analytics\predictor[]
171      */
172     public static function get_all_prediction_processors() {
174         $mlbackends = \core_component::get_plugin_list('mlbackend');
176         $predictionprocessors = array();
177         foreach ($mlbackends as $mlbackend => $unused) {
178             $classfullpath = '\mlbackend_' . $mlbackend . '\processor';
179             $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
180         }
181         return $predictionprocessors;
182     }
184     /**
185      * Get all available time splitting methods.
186      *
187      * @return \core_analytics\local\time_splitting\base[]
188      */
189     public static function get_all_time_splittings() {
190         if (self::$alltimesplittings !== null) {
191             return self::$alltimesplittings;
192         }
194         $classes = self::get_analytics_classes('time_splitting');
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         }
205         return self::$alltimesplittings;
206     }
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() {
215         if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
216             $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
217         }
219         $timesplittings = self::get_all_time_splittings();
220         foreach ($timesplittings as $key => $timesplitting) {
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     }
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     }
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         }
253         $classes = self::get_analytics_classes('indicator');
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.
260                 self::$allindicators[$instance->get_id()] = $instance;
261             }
262         }
264         return self::$allindicators;
265     }
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      */
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     }
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     }
293     /**
294      * Returns whether a time splitting method is valid or not.
295      *
296      * @param string $fullclassname
297      * @param string $baseclass
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     }
309     /**
310      * Returns the logstore used for analytics.
311      *
312      * @return \core\log\sql_reader|false False if no log stores are enabled.
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');
318         if (!empty($analyticsstore) && !empty($readers[$analyticsstore])) {
319             $logstore = $readers[$analyticsstore];
320         } else if (empty($analyticsstore) && !empty($readers)) {
321             // The first one, it is the same default than in settings.
322             $logstore = reset($readers);
323         } else if (!empty($readers)) {
324             $logstore = reset($readers);
325             debugging('The selected log store for analytics is not available anymore. Using "' .
326                 $logstore->get_name() . '"', DEBUG_DEVELOPER);
327         }
329         if (empty($logstore)) {
330             debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
331             return false;
332         }
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         }
339         return $logstore;
340     }
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) {
350         self::check_can_list_insights($context);
352         $models = self::get_all_models(true, true, $context);
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     }
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;
372         if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
373             throw new \moodle_exception('errorpredictionnotfound', 'report_insights');
374         }
376         if ($requirelogin) {
377             list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
378             require_login($course, false, $cm);
379         } else {
380             $context = \context::instance_by_id($predictionobj->contextid);
381         }
383         self::check_can_list_insights($context);
385         $model = new \core_analytics\model($predictionobj->modelid);
386         $sampledata = $model->prediction_sample_data($predictionobj);
387         $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
389         return array($model, $prediction, $context);
390     }
392     /**
393      * Adds the models included with moodle core to the system.
394      *
395      * @return void
396      */
397     public static function add_builtin_models() {
399         $target = self::get_target('\core\analytics\target\course_dropout');
401         // Community of inquiry indicators.
402         $coiindicators = array(
403             '\mod_assign\analytics\indicator\cognitive_depth',
404             '\mod_assign\analytics\indicator\social_breadth',
405             '\mod_book\analytics\indicator\cognitive_depth',
406             '\mod_book\analytics\indicator\social_breadth',
407             '\mod_chat\analytics\indicator\cognitive_depth',
408             '\mod_chat\analytics\indicator\social_breadth',
409             '\mod_choice\analytics\indicator\cognitive_depth',
410             '\mod_choice\analytics\indicator\social_breadth',
411             '\mod_data\analytics\indicator\cognitive_depth',
412             '\mod_data\analytics\indicator\social_breadth',
413             '\mod_feedback\analytics\indicator\cognitive_depth',
414             '\mod_feedback\analytics\indicator\social_breadth',
415             '\mod_folder\analytics\indicator\cognitive_depth',
416             '\mod_folder\analytics\indicator\social_breadth',
417             '\mod_forum\analytics\indicator\cognitive_depth',
418             '\mod_forum\analytics\indicator\social_breadth',
419             '\mod_glossary\analytics\indicator\cognitive_depth',
420             '\mod_glossary\analytics\indicator\social_breadth',
421             '\mod_imscp\analytics\indicator\cognitive_depth',
422             '\mod_imscp\analytics\indicator\social_breadth',
423             '\mod_label\analytics\indicator\cognitive_depth',
424             '\mod_label\analytics\indicator\social_breadth',
425             '\mod_lesson\analytics\indicator\cognitive_depth',
426             '\mod_lesson\analytics\indicator\social_breadth',
427             '\mod_lti\analytics\indicator\cognitive_depth',
428             '\mod_lti\analytics\indicator\social_breadth',
429             '\mod_page\analytics\indicator\cognitive_depth',
430             '\mod_page\analytics\indicator\social_breadth',
431             '\mod_quiz\analytics\indicator\cognitive_depth',
432             '\mod_quiz\analytics\indicator\social_breadth',
433             '\mod_resource\analytics\indicator\cognitive_depth',
434             '\mod_resource\analytics\indicator\social_breadth',
435             '\mod_scorm\analytics\indicator\cognitive_depth',
436             '\mod_scorm\analytics\indicator\social_breadth',
437             '\mod_survey\analytics\indicator\cognitive_depth',
438             '\mod_survey\analytics\indicator\social_breadth',
439             '\mod_url\analytics\indicator\cognitive_depth',
440             '\mod_url\analytics\indicator\social_breadth',
441             '\mod_wiki\analytics\indicator\cognitive_depth',
442             '\mod_wiki\analytics\indicator\social_breadth',
443             '\mod_workshop\analytics\indicator\cognitive_depth',
444             '\mod_workshop\analytics\indicator\social_breadth',
445         );
446         $indicators = array();
447         foreach ($coiindicators as $coiindicator) {
448             $indicator = self::get_indicator($coiindicator);
449             $indicators[$indicator->get_id()] = $indicator;
450         }
451         if (!\core_analytics\model::exists($target, $indicators)) {
452             $model = \core_analytics\model::create($target, $indicators);
453         }
455         // No teaching model.
456         $target = self::get_target('\core\analytics\target\no_teaching');
457         $timesplittingmethod = '\core\analytics\time_splitting\single_range';
458         $noteacher = self::get_indicator('\core_course\analytics\indicator\no_teacher');
459         $indicators = array($noteacher->get_id() => $noteacher);
460         if (!\core_analytics\model::exists($target, $indicators)) {
461             \core_analytics\model::create($target, $indicators, $timesplittingmethod);
462         }
463     }
465     /**
466      * Returns the provided element classes in the site.
467      *
468      * @param string $element
469      * @return string[] Array keys are the FQCN and the values the class path.
470      */
471     private static function get_analytics_classes($element) {
473         // Just in case...
474         $element = clean_param($element, PARAM_ALPHANUMEXT);
476         // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
477         $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
479         // Plugins.
480         foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
481             foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
482                 $frankenstyle = $type . '_' . $pluginname;
483                 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
484             }
485         }
487         // Core subsystems.
488         foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
489             $componentname = 'core_' . $subsystemname;
490             $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
491         }
493         return $classes;
494     }