Differences Between: [Versions 310 and 402] [Versions 39 and 402]
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 /** 18 * Analysers base class. 19 * 20 * @package core_analytics 21 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core_analytics\local\analyser; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Analysers base class. 31 * 32 * @package core_analytics 33 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 abstract class base { 37 38 /** 39 * @var int 40 */ 41 protected $modelid; 42 43 /** 44 * The model target. 45 * 46 * @var \core_analytics\local\target\base 47 */ 48 protected $target; 49 50 /** 51 * The model indicators. 52 * 53 * @var \core_analytics\local\indicator\base[] 54 */ 55 protected $indicators; 56 57 /** 58 * Time splitting methods to use. 59 * 60 * Multiple time splitting methods during evaluation and 1 single 61 * time splitting method once the model is enabled. 62 * 63 * @var \core_analytics\local\time_splitting\base[] 64 */ 65 protected $timesplittings; 66 67 /** 68 * Execution options. 69 * 70 * @var array 71 */ 72 protected $options; 73 74 /** 75 * Simple log array. 76 * 77 * @var string[] 78 */ 79 protected $log; 80 81 /** 82 * Constructor method. 83 * 84 * @param int $modelid 85 * @param \core_analytics\local\target\base $target 86 * @param \core_analytics\local\indicator\base[] $indicators 87 * @param \core_analytics\local\time_splitting\base[] $timesplittings 88 * @param array $options 89 * @return void 90 */ 91 public function __construct($modelid, \core_analytics\local\target\base $target, $indicators, $timesplittings, $options) { 92 $this->modelid = $modelid; 93 $this->target = $target; 94 $this->indicators = $indicators; 95 $this->timesplittings = $timesplittings; 96 97 if (empty($options['evaluation'])) { 98 $options['evaluation'] = false; 99 } 100 $this->options = $options; 101 102 // Checks if the analyser satisfies the indicators requirements. 103 $this->check_indicators_requirements(); 104 105 $this->log = array(); 106 } 107 108 /** 109 * @deprecated since Moodle 3.7 110 */ 111 public function get_analysables() { 112 throw new \coding_exception('get_analysables() method has been removed and cannot be used any more.'); 113 } 114 115 /** 116 * Returns the list of analysable elements available on the site. 117 * 118 * A relatively complex SQL query should be set so that we take into account which analysable elements 119 * have already been processed and the order in which they have been processed. Helper methods are available 120 * to ease to implementation of get_analysables_iterator: get_iterator_sql and order_sql. 121 * 122 * @param string|null $action 'prediction', 'training' or null if no specific action needed. 123 * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables in the system if empty. 124 * @return \Iterator 125 */ 126 abstract public function get_analysables_iterator(?string $action = null, array $contexts = []); 127 128 /** 129 * This function returns this analysable list of samples. 130 * 131 * @param \core_analytics\analysable $analysable 132 * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata) 133 */ 134 abstract public function get_all_samples(\core_analytics\analysable $analysable); 135 136 /** 137 * This function returns the samples data from a list of sample ids. 138 * 139 * @param int[] $sampleids 140 * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata) 141 */ 142 abstract public function get_samples($sampleids); 143 144 /** 145 * Returns the analysable of a sample. 146 * 147 * @param int $sampleid 148 * @return \core_analytics\analysable 149 */ 150 abstract public function get_sample_analysable($sampleid); 151 152 /** 153 * Returns the sample's origin in moodle database. 154 * 155 * @return string 156 */ 157 abstract public function get_samples_origin(); 158 159 /** 160 * Returns the context of a sample. 161 * 162 * moodle/analytics:listinsights will be required at this level to access the sample predictions. 163 * 164 * @param int $sampleid 165 * @return \context 166 */ 167 abstract public function sample_access_context($sampleid); 168 169 /** 170 * Describes a sample with a description summary and a \renderable (an image for example) 171 * 172 * @param int $sampleid 173 * @param int $contextid 174 * @param array $sampledata 175 * @return array array(string, \renderable) 176 */ 177 abstract public function sample_description($sampleid, $contextid, $sampledata); 178 179 /** 180 * Model id getter. 181 * @return int 182 */ 183 public function get_modelid(): int { 184 return $this->modelid; 185 } 186 187 /** 188 * Options getter. 189 * @return array 190 */ 191 public function get_options(): array { 192 return $this->options; 193 } 194 195 /** 196 * Returns the analysed target. 197 * 198 * @return \core_analytics\local\target\base 199 */ 200 public function get_target(): \core_analytics\local\target\base { 201 return $this->target; 202 } 203 204 /** 205 * Getter for time splittings. 206 * 207 * @return \core_analytics\local\time_splitting\base 208 */ 209 public function get_timesplittings(): array { 210 return $this->timesplittings; 211 } 212 213 /** 214 * Getter for indicators. 215 * 216 * @return \core_analytics\local\indicator\base 217 */ 218 public function get_indicators(): array { 219 return $this->indicators; 220 } 221 222 /** 223 * Instantiate the indicators. 224 * 225 * @return \core_analytics\local\indicator\base[] 226 */ 227 public function instantiate_indicators() { 228 foreach ($this->indicators as $key => $indicator) { 229 $this->indicators[$key] = call_user_func(array($indicator, 'instance')); 230 } 231 232 // Free memory ASAP. 233 gc_collect_cycles(); 234 gc_mem_caches(); 235 236 return $this->indicators; 237 } 238 239 /** 240 * Samples data this analyser provides. 241 * 242 * @return string[] 243 */ 244 protected function provided_sample_data() { 245 return array($this->get_samples_origin()); 246 } 247 248 /** 249 * Returns labelled data (training and evaluation). 250 * 251 * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null. 252 * @return \stored_file[] 253 */ 254 public function get_labelled_data(array $contexts = []) { 255 // Delegates all processing to the analysis. 256 $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), true, $this->get_options()); 257 $analysis = new \core_analytics\analysis($this, true, $result); 258 $analysis->run($contexts); 259 return $result->get(); 260 } 261 262 /** 263 * Returns unlabelled data (prediction). 264 * 265 * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null. 266 * @return \stored_file[] 267 */ 268 public function get_unlabelled_data(array $contexts = []) { 269 // Delegates all processing to the analysis. 270 $result = new \core_analytics\local\analysis\result_file($this->get_modelid(), false, $this->get_options()); 271 $analysis = new \core_analytics\analysis($this, false, $result); 272 $analysis->run($contexts); 273 return $result->get(); 274 } 275 276 /** 277 * Returns indicator calculations as an array. 278 * 279 * @param \context[] $contexts Restrict the analysis to these contexts. No context restrictions if null. 280 * @return array 281 */ 282 public function get_static_data(array $contexts = []) { 283 // Delegates all processing to the analysis. 284 $result = new \core_analytics\local\analysis\result_array($this->get_modelid(), false, $this->get_options()); 285 $analysis = new \core_analytics\analysis($this, false, $result); 286 $analysis->run($contexts); 287 return $result->get(); 288 } 289 290 /** 291 * Checks if the analyser satisfies all the model indicators requirements. 292 * 293 * @throws \core_analytics\requirements_exception 294 * @return void 295 */ 296 protected function check_indicators_requirements() { 297 298 foreach ($this->indicators as $indicator) { 299 $missingrequired = $this->check_indicator_requirements($indicator); 300 if ($missingrequired !== true) { 301 throw new \core_analytics\requirements_exception(get_class($indicator) . ' indicator requires ' . 302 json_encode($missingrequired) . ' sample data which is not provided by ' . get_class($this)); 303 } 304 } 305 } 306 307 /** 308 * Checks that this analyser satisfies the provided indicator requirements. 309 * 310 * @param \core_analytics\local\indicator\base $indicator 311 * @return true|string[] True if all good, missing requirements list otherwise 312 */ 313 public function check_indicator_requirements(\core_analytics\local\indicator\base $indicator) { 314 315 $providedsampledata = $this->provided_sample_data(); 316 317 $requiredsampledata = $indicator::required_sample_data(); 318 if (empty($requiredsampledata)) { 319 // The indicator does not need any sample data. 320 return true; 321 } 322 $missingrequired = array_diff($requiredsampledata, $providedsampledata); 323 324 if (empty($missingrequired)) { 325 return true; 326 } 327 328 return $missingrequired; 329 } 330 331 /** 332 * Adds a register to the analysis log. 333 * 334 * @param string $string 335 * @return void 336 */ 337 public function add_log($string) { 338 $this->log[] = $string; 339 } 340 341 /** 342 * Returns the analysis logs. 343 * 344 * @return string[] 345 */ 346 public function get_logs() { 347 return $this->log; 348 } 349 350 /** 351 * Whether the plugin needs user data clearing or not. 352 * 353 * This is related to privacy. Override this method if your analyser samples have any relation 354 * to the 'user' database entity. We need to clean the site from all user-related data if a user 355 * request their data to be deleted from the system. A static::provided_sample_data returning 'user' 356 * is an indicator that you should be returning true. 357 * 358 * @return bool 359 */ 360 public function processes_user_data() { 361 return false; 362 } 363 364 /** 365 * SQL JOIN from a sample to users table. 366 * 367 * This function should be defined if static::processes_user_data returns true and it is related to analytics API 368 * privacy API implementation. It allows the analytics API to identify data associated to users that needs to be 369 * deleted or exported. 370 * 371 * This function receives the alias of a table with a 'sampleid' field and it should return a SQL join 372 * with static::get_samples_origin and with 'user' table. Note that: 373 * - The function caller expects the returned 'user' table to be aliased as 'u' (defacto standard in moodle). 374 * - You can join with other tables if your samples origin table does not contain a 'userid' field (if that would be 375 * a requirement this solution would be automated for you) you can't though use the following 376 * aliases: 'ap', 'apa', 'aic' and 'am'. 377 * 378 * Some examples: 379 * 380 * static::get_samples_origin() === 'user': 381 * JOIN {user} u ON {$sampletablealias}.sampleid = u.id 382 * 383 * static::get_samples_origin() === 'role_assignments': 384 * JOIN {role_assignments} ra ON {$sampletablealias}.sampleid = ra.userid JOIN {user} u ON u.id = ra.userid 385 * 386 * static::get_samples_origin() === 'user_enrolments': 387 * JOIN {user_enrolments} ue ON {$sampletablealias}.sampleid = ue.userid JOIN {user} u ON u.id = ue.userid 388 * 389 * @throws \coding_exception 390 * @param string $sampletablealias The alias of the table with a sampleid field that will join with this SQL string 391 * @return string 392 */ 393 public function join_sample_user($sampletablealias) { 394 throw new \coding_exception('This method should be implemented if static::processes_user_data returns true.'); 395 } 396 397 /** 398 * Do this analyser's analysables have 1 single sample each? 399 * 400 * Overwrite and return true if your analysables only have 401 * one sample. The insights generated by models using this 402 * analyser will then include the suggested actions in the 403 * notification. 404 * 405 * @return bool 406 */ 407 public static function one_sample_per_analysable() { 408 return false; 409 } 410 411 /** 412 * Returns an array of context levels that can be used to restrict the contexts used during analysis. 413 * 414 * The contexts provided to self::get_analysables_iterator will match these contextlevels. 415 * 416 * @return array Array of context levels or an empty array if context restriction is not supported. 417 */ 418 public static function context_restriction_support(): array { 419 return []; 420 } 421 422 /** 423 * Returns the possible contexts used by the analyser. 424 * 425 * This method uses separate logic for each context level because to iterate through 426 * the list of contexts calling get_context_name for each of them would be expensive 427 * in performance terms. 428 * 429 * This generic implementation returns all the contexts in the site for the provided context level. 430 * Overwrite it for specific restrictions in your analyser. 431 * 432 * @param string|null $query Context name filter. 433 * @return int[] 434 */ 435 public static function potential_context_restrictions(string $query = null) { 436 return \core_analytics\manager::get_potential_context_restrictions(static::context_restriction_support(), $query); 437 } 438 439 /** 440 * Get the sql of a default implementation of the iterator. 441 * 442 * This method only works for analysers that return analysable elements which ids map to a context instance ids. 443 * 444 * @param string $tablename The name of the table 445 * @param int $contextlevel The context level of the analysable 446 * @param string|null $action 447 * @param string|null $tablealias The table alias 448 * @param \context[] $contexts Only analysables that depend on the provided contexts. All analysables if empty. 449 * @return array [0] => sql and [1] => params array 450 */ 451 protected function get_iterator_sql(string $tablename, int $contextlevel, ?string $action = null, ?string $tablealias = null, 452 array $contexts = []) { 453 global $DB; 454 455 if (!$tablealias) { 456 $tablealias = 'analysable'; 457 } 458 459 $params = ['contextlevel' => $contextlevel, 'modelid' => $this->get_modelid()]; 460 $select = $tablealias . '.*, ' . \context_helper::get_preload_record_columns_sql('ctx'); 461 462 // We add the action filter on ON instead of on WHERE because otherwise records are not returned if there are existing 463 // records for another action or model. 464 $usedanalysablesjoin = ' LEFT JOIN {analytics_used_analysables} aua ON ' . $tablealias . '.id = aua.analysableid AND ' . 465 '(aua.modelid = :modelid OR aua.modelid IS NULL)'; 466 467 if ($action) { 468 $usedanalysablesjoin .= " AND aua.action = :action"; 469 $params = $params + ['action' => $action]; 470 } 471 472 $sql = 'SELECT ' . $select . ' 473 FROM {' . $tablename . '} ' . $tablealias . ' 474 ' . $usedanalysablesjoin . ' 475 JOIN {context} ctx ON (ctx.contextlevel = :contextlevel AND ctx.instanceid = ' . $tablealias . '.id) '; 476 477 if (!$contexts) { 478 // Adding the 1 = 1 just to have the WHERE part so that all further conditions 479 // added by callers can be appended to $sql with and ' AND'. 480 $sql .= 'WHERE 1 = 1'; 481 } else { 482 483 $contextsqls = []; 484 foreach ($contexts as $context) { 485 $paramkey1 = 'paramctxlike' . $context->id; 486 $paramkey2 = 'paramctxeq' . $context->id; 487 $contextsqls[] = $DB->sql_like('ctx.path', ':' . $paramkey1); 488 $contextsqls[] = 'ctx.path = :' . $paramkey2; 489 490 // This includes the context itself. 491 $params[$paramkey1] = $context->path . '/%'; 492 $params[$paramkey2] = $context->path; 493 } 494 $sql .= 'WHERE (' . implode(' OR ', $contextsqls) . ')'; 495 } 496 497 return [$sql, $params]; 498 } 499 500 /** 501 * Returns the order by clause. 502 * 503 * @param string|null $fieldname The field name 504 * @param string $order 'ASC' or 'DESC' 505 * @param string|null $tablealias The table alias of the field 506 * @return string 507 */ 508 protected function order_sql(?string $fieldname = null, string $order = 'ASC', ?string $tablealias = null) { 509 510 if (!$tablealias) { 511 $tablealias = 'analysable'; 512 } 513 514 if ($order != 'ASC' && $order != 'DESC') { 515 throw new \coding_exception('The order can only be ASC or DESC'); 516 } 517 518 $ordersql = ' ORDER BY (CASE WHEN aua.timeanalysed IS NULL THEN 0 ELSE aua.timeanalysed END) ASC'; 519 if ($fieldname) { 520 $ordersql .= ', ' . $tablealias . '.' . $fieldname .' ' . $order; 521 } 522 523 return $ordersql; 524 } 525 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body