MDL-57791 analytics: Always absolute full class names
[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 $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         $sql = "SELECT DISTINCT am.* FROM {analytics_models} am";
88         if ($predictioncontext) {
89             $sql .= " JOIN {analytics_predictions} ap ON ap.modelid = am.id AND ap.contextid = :contextid";
90             $params['contextid'] = $predictioncontext->id;
91         }
93         if ($enabled || $trained) {
94             $conditions = [];
95             if ($enabled) {
96                 $conditions[] = 'am.enabled = :enabled';
97                 $params['enabled'] = 1;
98             }
99             if ($trained) {
100                 $conditions[] = 'am.trained = :trained';
101                 $params['trained'] = 1;
102             }
103             $sql .= ' WHERE ' . implode(' AND ', $conditions);
104         }
105         $modelobjs = $DB->get_records_sql($sql, $params);
107         $models = array();
108         foreach ($modelobjs as $modelobj) {
109             $model = new \core_analytics\model($modelobj);
110             if ($model->is_available()) {
111                 $models[$modelobj->id] = $model;
112             }
113         }
114         return $models;
115     }
117     /**
118      * Returns the site selected predictions processor.
119      *
120      * @param string $predictionclass
121      * @param bool $checkisready
122      * @return \core_analytics\predictor
123      */
124     public static function get_predictions_processor($predictionclass = false, $checkisready = true) {
126         // We want 0 or 1 so we can use it as an array key for caching.
127         $checkisready = intval($checkisready);
129         if ($predictionclass === false) {
130             $predictionclass = get_config('analytics', 'predictionsprocessor');
131         }
133         if (empty($predictionclass)) {
134             // Use the default one if nothing set.
135             $predictionclass = '\mlbackend_php\processor';
136         }
138         if (!class_exists($predictionclass)) {
139             throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
140         }
142         $interfaces = class_implements($predictionclass);
143         if (empty($interfaces['core_analytics\predictor'])) {
144             throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
145         }
147         // Return it from the cached list.
148         if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
150             $instance = new $predictionclass();
151             if ($checkisready) {
152                 $isready = $instance->is_ready();
153                 if ($isready !== true) {
154                     throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
155                 }
156             }
157             self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
158         }
160         return self::$predictionprocessors[$checkisready][$predictionclass];
161     }
163     /**
164      * Return all system predictions processors.
165      *
166      * @return \core_analytics\predictor
167      */
168     public static function get_all_prediction_processors() {
170         $mlbackends = \core_component::get_plugin_list('mlbackend');
172         $predictionprocessors = array();
173         foreach ($mlbackends as $mlbackend => $unused) {
174             $classfullpath = '\\mlbackend_' . $mlbackend . '\\processor';
175             $predictionprocessors[$classfullpath] = self::get_predictions_processor($classfullpath, false);
176         }
177         return $predictionprocessors;
178     }
180     /**
181      * Get all available time splitting methods.
182      *
183      * @return \core_analytics\local\time_splitting\base[]
184      */
185     public static function get_all_time_splittings() {
186         if (self::$alltimesplittings !== null) {
187             return self::$alltimesplittings;
188         }
190         $classes = self::get_analytics_classes('time_splitting');
192         self::$alltimesplittings = [];
193         foreach ($classes as $fullclassname => $classpath) {
194             $instance = self::get_time_splitting($fullclassname);
195             // We need to check that it is a valid time splitting method, it may be an abstract class.
196             if ($instance) {
197                 self::$alltimesplittings[$instance->get_id()] = $instance;
198             }
199         }
201         return self::$alltimesplittings;
202     }
204     /**
205      * Returns the enabled time splitting methods.
206      *
207      * @return \core_analytics\local\time_splitting\base[]
208      */
209     public static function get_enabled_time_splitting_methods() {
211         if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
212             $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
213         }
215         $timesplittings = self::get_all_time_splittings();
216         foreach ($timesplittings as $key => $timesplitting) {
218             // We remove the ones that are not enabled. This also respects the default value (all methods enabled).
219             if (!empty($enabledtimesplittings) && !isset($enabledtimesplittings[$key])) {
220                 unset($timesplittings[$key]);
221             }
222         }
223         return $timesplittings;
224     }
226     /**
227      * Returns a time splitting method by its classname.
228      *
229      * @param string $fullclassname
230      * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
231      */
232     public static function get_time_splitting($fullclassname) {
233         if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
234             return false;
235         }
236         return new $fullclassname();
237     }
239     /**
240      * Return all system indicators.
241      *
242      * @return \core_analytics\local\indicator\base[]
243      */
244     public static function get_all_indicators() {
245         if (self::$allindicators !== null) {
246             return self::$allindicators;
247         }
249         $classes = self::get_analytics_classes('indicator');
251         self::$allindicators = [];
252         foreach ($classes as $fullclassname => $classpath) {
253             $instance = self::get_indicator($fullclassname);
254             if ($instance) {
255                 // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
256                 self::$allindicators[$instance->get_id()] = $instance;
257             }
258         }
260         return self::$allindicators;
261     }
263     /**
264      * Returns the specified target
265      *
266      * @param mixed $fullclassname
267      * @return \core_analytics\local\target\base|false False if it is not valid
268      */
269     public static function get_target($fullclassname) {
270         if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
271             return false;
272         }
273         return new $fullclassname();
274     }
276     /**
277      * Returns an instance of the provided indicator.
278      *
279      * @param string $fullclassname
280      * @return \core_analytics\local\indicator\base|false False if it is not valid.
281      */
282     public static function get_indicator($fullclassname) {
283         if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
284             return false;
285         }
286         return new $fullclassname();
287     }
289     /**
290      * Returns whether a time splitting method is valid or not.
291      *
292      * @param string $fullclassname
293      * @param string $baseclass
294      * @return bool
295      */
296     public static function is_valid($fullclassname, $baseclass) {
297         if (is_subclass_of($fullclassname, $baseclass)) {
298             if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
299                 return true;
300             }
301         }
302         return false;
303     }
305     /**
306      * Returns the logstore used for analytics.
307      *
308      * @return \core\log\sql_reader
309      */
310     public static function get_analytics_logstore() {
311         $readers = get_log_manager()->get_readers('core\log\sql_reader');
312         $analyticsstore = get_config('analytics', 'logstore');
313         if (empty($analyticsstore)) {
314             $logstore = reset($readers);
315         } else if (!empty($readers[$analyticsstore])) {
316             $logstore = $readers[$analyticsstore];
317         } else {
318             $logstore = reset($readers);
319             debugging('The selected log store for analytics is not available anymore. Using "' .
320                 $logstore->get_name() . '"', DEBUG_DEVELOPER);
321         }
323         if (!$logstore->is_logging()) {
324             debugging('The selected log store for analytics "' . $logstore->get_name() .
325                 '" is not logging activity logs', DEBUG_DEVELOPER);
326         }
328         return $logstore;
329     }
331     /**
332      * Returns the models with insights at the provided context.
333      *
334      * @param \context $context
335      * @return \core_analytics\model[]
336      */
337     public static function get_models_with_insights(\context $context) {
339         self::check_can_list_insights($context);
341         $models = self::get_all_models(true, true, $context);
342         foreach ($models as $key => $model) {
343             // Check that it not only have predictions but also generates insights from them.
344             if (!$model->uses_insights()) {
345                 unset($models[$key]);
346             }
347         }
348         return $models;
349     }
351     /**
352      * Returns a prediction
353      *
354      * @param int $predictionid
355      * @param bool $requirelogin
356      * @return array array($model, $prediction, $context)
357      */
358     public static function get_prediction($predictionid, $requirelogin = false) {
359         global $DB;
361         if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
362             throw new \moodle_exception('errorpredictionnotfound', 'report_insights');
363         }
365         if ($requirelogin) {
366             list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
367             require_login($course, false, $cm);
368         } else {
369             $context = \context::instance_by_id($predictionobj->contextid);
370         }
372         self::check_can_list_insights($context);
374         $model = new \core_analytics\model($predictionobj->modelid);
375         $sampledata = $model->prediction_sample_data($predictionobj);
376         $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
378         return array($model, $prediction, $context);
379     }
381     /**
382      * Returns the provided element classes in the site.
383      *
384      * @param string $element
385      * @return string[] Array keys are the FQCN and the values the class path.
386      */
387     private static function get_analytics_classes($element) {
389         // Just in case...
390         $element = clean_param($element, PARAM_ALPHANUMEXT);
392         $classes = \core_component::get_component_classes_in_namespace('core_analytics', 'local\\' . $element);
393         foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
394             foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
395                 $frankenstyle = $type . '_' . $pluginname;
396                 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
397             }
398         }
400         foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
401             $componentname = 'core_' . $subsystemname;
402             $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);
403         }
405         return $classes;
406     }