2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Analytics basic actions manager.
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
25 namespace core_analytics;
27 defined('MOODLE_INTERNAL') || die();
30 * Analytics basic actions manager.
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
39 * @var \core_analytics\predictor[]
41 protected static $predictionprocessors = null;
44 * @var \core_analytics\local\indicator\base[]
46 protected static $allindicators = null;
49 * @var \core_analytics\local\time_splitting\base[]
51 protected static $alltimesplittings = null;
54 * Checks that the user can manage models
56 * @throws \required_capability_exception
59 public static function check_can_manage_models() {
60 require_capability('moodle/analytics:managemodels', \context_system::instance());
64 * Checks that the user can list that context insights
66 * @throws \required_capability_exception
67 * @param \context $context
70 public static function check_can_list_insights(\context $context) {
71 require_capability('moodle/analytics:listinsights', $context);
75 * Returns all system models that match the provided filters.
77 * @param bool $enabled
78 * @param bool $trained
79 * @param \context|false $predictioncontext
80 * @return \core_analytics\model[]
82 public static function get_all_models($enabled = false, $trained = false, $predictioncontext = false) {
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;
93 if ($enabled || $trained) {
96 $conditions[] = 'am.enabled = :enabled';
97 $params['enabled'] = 1;
100 $conditions[] = 'am.trained = :trained';
101 $params['trained'] = 1;
103 $sql .= ' WHERE ' . implode(' AND ', $conditions);
105 $modelobjs = $DB->get_records_sql($sql, $params);
108 foreach ($modelobjs as $modelobj) {
109 $model = new \core_analytics\model($modelobj);
110 if ($model->is_available()) {
111 $models[$modelobj->id] = $model;
118 * Returns the site selected predictions processor.
120 * @param string $predictionclass
121 * @param bool $checkisready
122 * @return \core_analytics\predictor
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');
133 if (empty($predictionclass)) {
134 // Use the default one if nothing set.
135 $predictionclass = '\mlbackend_php\processor';
138 if (!class_exists($predictionclass)) {
139 throw new \coding_exception('Invalid predictions processor ' . $predictionclass . '.');
142 $interfaces = class_implements($predictionclass);
143 if (empty($interfaces['core_analytics\predictor'])) {
144 throw new \coding_exception($predictionclass . ' should implement \core_analytics\predictor.');
147 // Return it from the cached list.
148 if (!isset(self::$predictionprocessors[$checkisready][$predictionclass])) {
150 $instance = new $predictionclass();
152 $isready = $instance->is_ready();
153 if ($isready !== true) {
154 throw new \moodle_exception('errorprocessornotready', 'analytics', '', $isready);
157 self::$predictionprocessors[$checkisready][$predictionclass] = $instance;
160 return self::$predictionprocessors[$checkisready][$predictionclass];
164 * Return all system predictions processors.
166 * @return \core_analytics\predictor[]
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);
177 return $predictionprocessors;
181 * Get all available time splitting methods.
183 * @return \core_analytics\local\time_splitting\base[]
185 public static function get_all_time_splittings() {
186 if (self::$alltimesplittings !== null) {
187 return self::$alltimesplittings;
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.
197 self::$alltimesplittings[$instance->get_id()] = $instance;
201 return self::$alltimesplittings;
205 * Returns the enabled time splitting methods.
207 * @return \core_analytics\local\time_splitting\base[]
209 public static function get_enabled_time_splitting_methods() {
211 if ($enabledtimesplittings = get_config('analytics', 'timesplittings')) {
212 $enabledtimesplittings = array_flip(explode(',', $enabledtimesplittings));
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]);
223 return $timesplittings;
227 * Returns a time splitting method by its classname.
229 * @param string $fullclassname
230 * @return \core_analytics\local\time_splitting\base|false False if it is not valid.
232 public static function get_time_splitting($fullclassname) {
233 if (!self::is_valid($fullclassname, '\core_analytics\local\time_splitting\base')) {
236 return new $fullclassname();
240 * Return all system indicators.
242 * @return \core_analytics\local\indicator\base[]
244 public static function get_all_indicators() {
245 if (self::$allindicators !== null) {
246 return self::$allindicators;
249 $classes = self::get_analytics_classes('indicator');
251 self::$allindicators = [];
252 foreach ($classes as $fullclassname => $classpath) {
253 $instance = self::get_indicator($fullclassname);
255 // Using get_class as get_component_classes_in_namespace returns double escaped fully qualified class names.
256 self::$allindicators[$instance->get_id()] = $instance;
260 return self::$allindicators;
264 * Returns the specified target
266 * @param mixed $fullclassname
267 * @return \core_analytics\local\target\base|false False if it is not valid
269 public static function get_target($fullclassname) {
270 if (!self::is_valid($fullclassname, 'core_analytics\local\target\base')) {
273 return new $fullclassname();
277 * Returns an instance of the provided indicator.
279 * @param string $fullclassname
280 * @return \core_analytics\local\indicator\base|false False if it is not valid.
282 public static function get_indicator($fullclassname) {
283 if (!self::is_valid($fullclassname, 'core_analytics\local\indicator\base')) {
286 return new $fullclassname();
290 * Returns whether a time splitting method is valid or not.
292 * @param string $fullclassname
293 * @param string $baseclass
296 public static function is_valid($fullclassname, $baseclass) {
297 if (is_subclass_of($fullclassname, $baseclass)) {
298 if ((new \ReflectionClass($fullclassname))->isInstantiable()) {
306 * Returns the logstore used for analytics.
308 * @return \core\log\sql_reader|false False if no log stores are enabled.
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 if (!empty($readers)) {
318 $logstore = reset($readers);
319 debugging('The selected log store for analytics is not available anymore. Using "' .
320 $logstore->get_name() . '"', DEBUG_DEVELOPER);
323 if (empty($logstore)) {
324 debugging('No system log stores available to use for analytics', DEBUG_DEVELOPER);
328 if (!$logstore->is_logging()) {
329 debugging('The selected log store for analytics "' . $logstore->get_name() .
330 '" is not logging activity logs', DEBUG_DEVELOPER);
337 * Returns the models with insights at the provided context.
339 * @param \context $context
340 * @return \core_analytics\model[]
342 public static function get_models_with_insights(\context $context) {
344 self::check_can_list_insights($context);
346 $models = self::get_all_models(true, true, $context);
347 foreach ($models as $key => $model) {
348 // Check that it not only have predictions but also generates insights from them.
349 if (!$model->uses_insights()) {
350 unset($models[$key]);
357 * Returns a prediction
359 * @param int $predictionid
360 * @param bool $requirelogin
361 * @return array array($model, $prediction, $context)
363 public static function get_prediction($predictionid, $requirelogin = false) {
366 if (!$predictionobj = $DB->get_record('analytics_predictions', array('id' => $predictionid))) {
367 throw new \moodle_exception('errorpredictionnotfound', 'report_insights');
371 list($context, $course, $cm) = get_context_info_array($predictionobj->contextid);
372 require_login($course, false, $cm);
374 $context = \context::instance_by_id($predictionobj->contextid);
377 self::check_can_list_insights($context);
379 $model = new \core_analytics\model($predictionobj->modelid);
380 $sampledata = $model->prediction_sample_data($predictionobj);
381 $prediction = new \core_analytics\prediction($predictionobj, $sampledata);
383 return array($model, $prediction, $context);
387 * Adds the models included with moodle core to the system.
391 public static function add_builtin_models() {
393 $target = \core_analytics\manager::get_target('\core\analytics\target\course_dropout');
395 // Community of inquiry indicators.
396 $coiindicators = array(
397 '\mod_assign\analytics\indicator\cognitive_depth',
398 '\mod_assign\analytics\indicator\social_breadth',
399 '\mod_book\analytics\indicator\cognitive_depth',
400 '\mod_book\analytics\indicator\social_breadth',
401 '\mod_chat\analytics\indicator\cognitive_depth',
402 '\mod_chat\analytics\indicator\social_breadth',
403 '\mod_choice\analytics\indicator\cognitive_depth',
404 '\mod_choice\analytics\indicator\social_breadth',
405 '\mod_data\analytics\indicator\cognitive_depth',
406 '\mod_data\analytics\indicator\social_breadth',
407 '\mod_feedback\analytics\indicator\cognitive_depth',
408 '\mod_feedback\analytics\indicator\social_breadth',
409 '\mod_folder\analytics\indicator\cognitive_depth',
410 '\mod_folder\analytics\indicator\social_breadth',
411 '\mod_forum\analytics\indicator\cognitive_depth',
412 '\mod_forum\analytics\indicator\social_breadth',
413 '\mod_glossary\analytics\indicator\cognitive_depth',
414 '\mod_glossary\analytics\indicator\social_breadth',
415 '\mod_imscp\analytics\indicator\cognitive_depth',
416 '\mod_imscp\analytics\indicator\social_breadth',
417 '\mod_label\analytics\indicator\cognitive_depth',
418 '\mod_label\analytics\indicator\social_breadth',
419 '\mod_lesson\analytics\indicator\cognitive_depth',
420 '\mod_lesson\analytics\indicator\social_breadth',
421 '\mod_lti\analytics\indicator\cognitive_depth',
422 '\mod_lti\analytics\indicator\social_breadth',
423 '\mod_page\analytics\indicator\cognitive_depth',
424 '\mod_page\analytics\indicator\social_breadth',
425 '\mod_quiz\analytics\indicator\cognitive_depth',
426 '\mod_quiz\analytics\indicator\social_breadth',
427 '\mod_resource\analytics\indicator\cognitive_depth',
428 '\mod_resource\analytics\indicator\social_breadth',
429 '\mod_scorm\analytics\indicator\cognitive_depth',
430 '\mod_scorm\analytics\indicator\social_breadth',
431 '\mod_survey\analytics\indicator\cognitive_depth',
432 '\mod_survey\analytics\indicator\social_breadth',
433 '\mod_url\analytics\indicator\cognitive_depth',
434 '\mod_url\analytics\indicator\social_breadth',
435 '\mod_wiki\analytics\indicator\cognitive_depth',
436 '\mod_wiki\analytics\indicator\social_breadth',
437 '\mod_workshop\analytics\indicator\cognitive_depth',
438 '\mod_workshop\analytics\indicator\social_breadth',
440 $indicators = array();
441 foreach ($coiindicators as $coiindicator) {
442 $indicator = \core_analytics\manager::get_indicator($coiindicator);
443 $indicators[$indicator->get_id()] = $indicator;
445 if (!\core_analytics\model::exists($target, $indicators)) {
446 $model = \core_analytics\model::create($target, $indicators);
449 // No teaching model.
450 $target = \core_analytics\manager::get_target('\core\analytics\target\no_teaching');
451 $timesplittingmethod = '\core\analytics\time_splitting\single_range';
452 $noteacher = \core_analytics\manager::get_indicator('\core_course\analytics\indicator\no_teacher');
453 $indicators = array($noteacher->get_id() => $noteacher);
454 if (!\core_analytics\model::exists($target, $indicators)) {
455 \core_analytics\model::create($target, $indicators, $timesplittingmethod);
460 * Returns the provided element classes in the site.
462 * @param string $element
463 * @return string[] Array keys are the FQCN and the values the class path.
465 private static function get_analytics_classes($element) {
468 $element = clean_param($element, PARAM_ALPHANUMEXT);
470 // Core analytics classes (analytics subsystem should not contain uses of the analytics API).
471 $classes = \core_component::get_component_classes_in_namespace('core', 'analytics\\' . $element);
474 foreach (\core_component::get_plugin_types() as $type => $unusedplugintypepath) {
475 foreach (\core_component::get_plugin_list($type) as $pluginname => $unusedpluginpath) {
476 $frankenstyle = $type . '_' . $pluginname;
477 $classes += \core_component::get_component_classes_in_namespace($frankenstyle, 'analytics\\' . $element);
482 foreach (\core_component::get_core_subsystems() as $subsystemname => $unusedsubsystempath) {
483 $componentname = 'core_' . $subsystemname;
484 $classes += \core_component::get_component_classes_in_namespace($componentname, 'analytics\\' . $element);