See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 401]
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 * Abstract base target. 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\target; 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 /** 30 * Abstract base target. 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 extends \core_analytics\calculable { 37 38 /** 39 * This target have linear or discrete values. 40 * 41 * @return bool 42 */ 43 abstract public function is_linear(); 44 45 /** 46 * Returns the analyser class that should be used along with this target. 47 * 48 * @return string The full class name as a string 49 */ 50 abstract public function get_analyser_class(); 51 52 /** 53 * Allows the target to verify that the analysable is a good candidate. 54 * 55 * This method can be used as a quick way to discard invalid analysables. 56 * e.g. Imagine that your analysable don't have students and you need them. 57 * 58 * @param \core_analytics\analysable $analysable 59 * @param bool $fortraining 60 * @return true|string 61 */ 62 abstract public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true); 63 64 /** 65 * Is this sample from the $analysable valid? 66 * 67 * @param int $sampleid 68 * @param \core_analytics\analysable $analysable 69 * @param bool $fortraining 70 * @return bool 71 */ 72 abstract public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true); 73 74 /** 75 * Calculates this target for the provided samples. 76 * 77 * In case there are no values to return or the provided sample is not applicable just return null. 78 * 79 * @param int $sampleid 80 * @param \core_analytics\analysable $analysable 81 * @param int|false $starttime Limit calculations to start time 82 * @param int|false $endtime Limit calculations to end time 83 * @return float|null 84 */ 85 abstract protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false); 86 87 /** 88 * Can the provided time-splitting method be used on this target?. 89 * 90 * Time-splitting methods not matching the target requirements will not be selectable by models based on this target. 91 * 92 * @param \core_analytics\local\time_splitting\base $timesplitting 93 * @return bool 94 */ 95 abstract public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool; 96 97 /** 98 * Is this target generating insights? 99 * 100 * Defaults to true. 101 * 102 * @return bool 103 */ 104 public static function uses_insights() { 105 return true; 106 } 107 108 /** 109 * Should the insights of this model be linked from reports? 110 * 111 * @return bool 112 */ 113 public function link_insights_report(): bool { 114 return true; 115 } 116 117 /** 118 * Based on facts (processed by machine learning backends) by default. 119 * 120 * @return bool 121 */ 122 public static function based_on_assumptions() { 123 return false; 124 } 125 126 /** 127 * Update the last analysis time on analysable processed or always. 128 * 129 * If you overwrite this method to return false the last analysis time 130 * will only be recorded in DB when the element successfully analysed. You can 131 * safely return false for lightweight targets. 132 * 133 * @return bool 134 */ 135 public function always_update_analysis_time(): bool { 136 return true; 137 } 138 139 /** 140 * Suggested actions for a user. 141 * 142 * @param \core_analytics\prediction $prediction 143 * @param bool $includedetailsaction 144 * @param bool $isinsightuser Force all the available actions to be returned as it the user who 145 * receives the insight is the one logged in. 146 * @return \core_analytics\prediction_action[] 147 */ 148 public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false, 149 $isinsightuser = false) { 150 global $PAGE; 151 152 $predictionid = $prediction->get_prediction_data()->id; 153 $contextid = $prediction->get_prediction_data()->contextid; 154 $modelid = $prediction->get_prediction_data()->modelid; 155 156 $actions = array(); 157 158 if ($this->link_insights_report() && $includedetailsaction) { 159 160 $predictionurl = new \moodle_url('/report/insights/prediction.php', array('id' => $predictionid)); 161 $detailstext = $this->get_view_details_text(); 162 163 $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction, 164 $predictionurl, new \pix_icon('t/preview', $detailstext), 165 $detailstext, false, [], \core_analytics\action::TYPE_NEUTRAL); 166 } 167 168 return $actions; 169 } 170 171 /** 172 * Suggested bulk actions for a user. 173 * 174 * @param \core_analytics\prediction[] $predictions List of predictions suitable for the bulk actions to use. 175 * @return \core_analytics\bulk_action[] The list of bulk actions. 176 */ 177 public function bulk_actions(array $predictions) { 178 179 $analyserclass = $this->get_analyser_class(); 180 if ($analyserclass::one_sample_per_analysable()) { 181 // Default actions are useful / not useful. 182 $actions = [ 183 \core_analytics\default_bulk_actions::useful(), 184 \core_analytics\default_bulk_actions::not_useful() 185 ]; 186 187 } else { 188 // Accept and not applicable. 189 190 $actions = [ 191 \core_analytics\default_bulk_actions::accept(), 192 \core_analytics\default_bulk_actions::not_applicable() 193 ]; 194 195 if (!self::based_on_assumptions()) { 196 // We include incorrectly flagged. 197 $actions[] = \core_analytics\default_bulk_actions::incorrectly_flagged(); 198 } 199 } 200 201 return $actions; 202 } 203 204 /** 205 * Adds the JS required to run the bulk actions. 206 */ 207 public function add_bulk_actions_js() { 208 global $PAGE; 209 $PAGE->requires->js_call_amd('report_insights/actions', 'initBulk', ['.insights-bulk-actions']); 210 } 211 212 /** 213 * Returns the view details link text. 214 * @return string 215 */ 216 private function get_view_details_text() { 217 if ($this->based_on_assumptions()) { 218 $analyserclass = $this->get_analyser_class(); 219 if ($analyserclass::one_sample_per_analysable()) { 220 $detailstext = get_string('viewinsightdetails', 'analytics'); 221 } else { 222 $detailstext = get_string('viewdetails', 'analytics'); 223 } 224 } else { 225 $detailstext = get_string('viewprediction', 'analytics'); 226 } 227 228 return $detailstext; 229 } 230 231 /** 232 * Callback to execute once a prediction has been returned from the predictions processor. 233 * 234 * Note that the analytics_predictions db record is not yet inserted. 235 * 236 * @param int $modelid 237 * @param int $sampleid 238 * @param int $rangeindex 239 * @param \context $samplecontext 240 * @param float|int $prediction 241 * @param float $predictionscore 242 * @return void 243 */ 244 public function prediction_callback($modelid, $sampleid, $rangeindex, \context $samplecontext, $prediction, $predictionscore) { 245 return; 246 } 247 248 /** 249 * Generates insights notifications 250 * 251 * @param int $modelid 252 * @param \context[] $samplecontexts 253 * @param \core_analytics\prediction[] $predictions 254 * @return void 255 */ 256 public function generate_insight_notifications($modelid, $samplecontexts, array $predictions = []) { 257 // Delegate the processing of insights to the insights_generator. 258 $insightsgenerator = new \core_analytics\insights_generator($modelid, $this); 259 $insightsgenerator->generate($samplecontexts, $predictions); 260 } 261 262 /** 263 * Returns the list of users that will receive insights notifications. 264 * 265 * Feel free to overwrite if you need to but keep in mind that moodle/analytics:listinsights 266 * or moodle/analytics:listowninsights capability is required to access the list of insights. 267 * 268 * @param \context $context 269 * @return array 270 */ 271 public function get_insights_users(\context $context) { 272 if ($context->contextlevel === CONTEXT_USER) { 273 if (!has_capability('moodle/analytics:listowninsights', $context, $context->instanceid)) { 274 $users = []; 275 } else { 276 $users = [$context->instanceid => \core_user::get_user($context->instanceid)]; 277 } 278 279 } else if ($context->contextlevel >= CONTEXT_COURSE) { 280 // At course level or below only enrolled users although this is not ideal for 281 // teachers assigned at category level. 282 $users = get_enrolled_users($context, 'moodle/analytics:listinsights', 0, 'u.*', null, 0, 0, true); 283 } else { 284 $users = get_users_by_capability($context, 'moodle/analytics:listinsights'); 285 } 286 return $users; 287 } 288 289 /** 290 * URL to the insight. 291 * 292 * @param int $modelid 293 * @param \context $context 294 * @return \moodle_url 295 */ 296 public function get_insight_context_url($modelid, $context) { 297 return new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id); 298 } 299 300 /** 301 * The insight notification subject. 302 * 303 * This is just a default message, you should overwrite it for a custom insight message. 304 * 305 * @param int $modelid 306 * @param \context $context 307 * @return string 308 */ 309 public function get_insight_subject(int $modelid, \context $context) { 310 return get_string('insightmessagesubject', 'analytics', $context->get_context_name()); 311 } 312 313 /** 314 * Returns the body message for an insight with multiple predictions. 315 * 316 * This default method is executed when the analysable used by the model generates multiple insight 317 * for each analysable (one_sample_per_analysable === false) 318 * 319 * @param \context $context 320 * @param string $contextname 321 * @param \stdClass $user 322 * @param \moodle_url $insighturl 323 * @return string[] The plain text message and the HTML message 324 */ 325 public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array { 326 global $OUTPUT; 327 328 $fullmessage = get_string('insightinfomessageplain', 'analytics', $insighturl->out(false)); 329 $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message', 330 ['url' => $insighturl->out(false), 'insightinfomessage' => get_string('insightinfomessagehtml', 'analytics')] 331 ); 332 333 return [$fullmessage, $fullmessagehtml]; 334 } 335 336 /** 337 * Returns the body message for an insight for a single prediction. 338 * 339 * This default method is executed when the analysable used by the model generates one insight 340 * for each analysable (one_sample_per_analysable === true) 341 * 342 * @param \context $context 343 * @param \stdClass $user 344 * @param \core_analytics\prediction $prediction 345 * @param \core_analytics\action[] $actions Passed by reference to remove duplicate links to actions. 346 * @return array Plain text msg, HTML message and the main URL for this 347 * insight (you can return null if you are happy with the 348 * default insight URL calculated in prediction_info()) 349 */ 350 public function get_insight_body_for_prediction(\context $context, \stdClass $user, \core_analytics\prediction $prediction, 351 array &$actions) { 352 // No extra message by default. 353 return [FORMAT_PLAIN => '', FORMAT_HTML => '', 'url' => null]; 354 } 355 356 /** 357 * Returns an instance of the child class. 358 * 359 * Useful to reset cached data. 360 * 361 * @return \core_analytics\base\target 362 */ 363 public static function instance() { 364 return new static(); 365 } 366 367 /** 368 * Defines a boundary to ignore predictions below the specified prediction score. 369 * 370 * Value should go from 0 to 1. 371 * 372 * @return float 373 */ 374 protected function min_prediction_score() { 375 // The default minimum discards predictions with a low score. 376 return \core_analytics\model::PREDICTION_MIN_SCORE; 377 } 378 379 /** 380 * This method determines if a prediction is interesing for the model or not. 381 * 382 * @param mixed $predictedvalue 383 * @param float $predictionscore 384 * @return bool 385 */ 386 public function triggers_callback($predictedvalue, $predictionscore) { 387 388 $minscore = floatval($this->min_prediction_score()); 389 if ($minscore < 0) { 390 debugging(get_class($this) . ' minimum prediction score is below 0, please update it to a value between 0 and 1.'); 391 } else if ($minscore > 1) { 392 debugging(get_class($this) . ' minimum prediction score is above 1, please update it to a value between 0 and 1.'); 393 } 394 395 // We need to consider that targets may not have a min score. 396 if (!empty($minscore) && floatval($predictionscore) < $minscore) { 397 return false; 398 } 399 400 return true; 401 } 402 403 /** 404 * Calculates the target. 405 * 406 * Returns an array of values which size matches $sampleids size. 407 * 408 * Rows with null values will be skipped as invalid by time splitting methods. 409 * 410 * @param array $sampleids 411 * @param \core_analytics\analysable $analysable 412 * @param int $starttime 413 * @param int $endtime 414 * @return array The format to follow is [userid] = scalar|null 415 */ 416 public function calculate($sampleids, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) { 417 418 if (!PHPUNIT_TEST && CLI_SCRIPT) { 419 echo '.'; 420 } 421 422 $calculations = []; 423 foreach ($sampleids as $sampleid => $unusedsampleid) { 424 425 // No time limits when calculating the target to train models. 426 $calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime); 427 428 if (!is_null($calculatedvalue)) { 429 if ($this->is_linear() && 430 ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) { 431 throw new \coding_exception('Calculated values should be higher than ' . static::get_min_value() . 432 ' and lower than ' . static::get_max_value() . '. ' . $calculatedvalue . ' received'); 433 } else if (!$this->is_linear() && static::is_a_class($calculatedvalue) === false) { 434 throw new \coding_exception('Calculated values should be one of the target classes (' . 435 json_encode(static::get_classes()) . '). ' . $calculatedvalue . ' received'); 436 } 437 } 438 $calculations[$sampleid] = $calculatedvalue; 439 } 440 return $calculations; 441 } 442 443 /** 444 * Filters out invalid samples for training. 445 * 446 * @param int[] $sampleids 447 * @param \core_analytics\analysable $analysable 448 * @param bool $fortraining 449 * @return void 450 */ 451 public function filter_out_invalid_samples(&$sampleids, \core_analytics\analysable $analysable, $fortraining = true) { 452 foreach ($sampleids as $sampleid => $unusedsampleid) { 453 if (!$this->is_valid_sample($sampleid, $analysable, $fortraining)) { 454 // Skip it and remove the sample from the list of calculated samples. 455 unset($sampleids[$sampleid]); 456 } 457 } 458 } 459 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body