MDL-65085 analytics: noreply user sends insights
[moodle.git] / analytics / classes / local / target / base.php
CommitLineData
369389c9
DM
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
25namespace core_analytics\local\target;
26
27defined('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 */
36abstract 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 *
1611308b 48 * @return string The full class name as a string
369389c9
DM
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
a40952d3 64 /**
1611308b 65 * Is this sample from the $analysable valid?
a40952d3
DM
66 *
67 * @param int $sampleid
68 * @param \core_analytics\analysable $analysable
1611308b
DM
69 * @param bool $fortraining
70 * @return bool
a40952d3 71 */
1611308b 72 abstract public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true);
a40952d3 73
369389c9
DM
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 *
1611308b 79 * @param int $sampleid
369389c9 80 * @param \core_analytics\analysable $analysable
a40952d3
DM
81 * @param int|false $starttime Limit calculations to start time
82 * @param int|false $endtime Limit calculations to end time
369389c9
DM
83 * @return float|null
84 */
a40952d3
DM
85 abstract protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false);
86
f9e7447f
DM
87 /**
88 * Is this target generating insights?
89 *
90 * Defaults to true.
91 *
92 * @return bool
93 */
94 public static function uses_insights() {
95 return true;
96 }
97
a40952d3
DM
98 /**
99 * Based on facts (processed by machine learning backends) by default.
100 *
101 * @return bool
102 */
103 public static function based_on_assumptions() {
104 return false;
105 }
369389c9 106
1611308b
DM
107 /**
108 * Suggested actions for a user.
109 *
110 * @param \core_analytics\prediction $prediction
111 * @param bool $includedetailsaction
112 * @return \core_analytics\prediction_action[]
113 */
114 public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
025363d1
DM
115 global $PAGE;
116
0c0a72e7 117 $predictionid = $prediction->get_prediction_data()->id;
025363d1 118
0c0a72e7 119 $PAGE->requires->js_call_amd('report_insights/actions', 'init', array($predictionid));
1611308b 120
0c0a72e7 121 $actions = array();
025363d1 122
1611308b
DM
123 if ($includedetailsaction) {
124
125 $predictionurl = new \moodle_url('/report/insights/prediction.php',
025363d1 126 array('id' => $predictionid));
369389c9 127
025363d1 128 $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
1611308b
DM
129 $predictionurl, new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
130 get_string('viewprediction', 'analytics'));
369389c9
DM
131 }
132
025363d1
DM
133 // Flag as fixed / solved.
134 $fixedattrs = array(
135 'data-prediction-id' => $predictionid,
136 'data-prediction-methodname' => 'report_insights_set_fixed_prediction'
137 );
138 $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_FIXED,
139 $prediction, new \moodle_url(''), new \pix_icon('t/check', get_string('fixedack', 'analytics')),
140 get_string('fixedack', 'analytics'), false, $fixedattrs);
141
0c0a72e7
DM
142 // Flag as not useful.
143 $notusefulattrs = array(
144 'data-prediction-id' => $predictionid,
145 'data-prediction-methodname' => 'report_insights_set_notuseful_prediction'
146 );
147 $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_NOT_USEFUL,
148 $prediction, new \moodle_url(''), new \pix_icon('t/delete', get_string('notuseful', 'analytics')),
149 get_string('notuseful', 'analytics'), false, $notusefulattrs);
150
1611308b 151 return $actions;
369389c9
DM
152 }
153
369389c9
DM
154 /**
155 * Callback to execute once a prediction has been returned from the predictions processor.
156 *
cab7abec
DM
157 * Note that the analytics_predictions db record is not yet inserted.
158 *
1611308b 159 * @param int $modelid
369389c9 160 * @param int $sampleid
1611308b
DM
161 * @param int $rangeindex
162 * @param \context $samplecontext
369389c9
DM
163 * @param float|int $prediction
164 * @param float $predictionscore
165 * @return void
166 */
1611308b 167 public function prediction_callback($modelid, $sampleid, $rangeindex, \context $samplecontext, $prediction, $predictionscore) {
369389c9
DM
168 return;
169 }
170
1611308b
DM
171 /**
172 * Generates insights notifications
173 *
174 * @param int $modelid
175 * @param \context[] $samplecontexts
176 * @return void
177 */
178 public function generate_insight_notifications($modelid, $samplecontexts) {
369389c9
DM
179
180 foreach ($samplecontexts as $context) {
181
182 $insightinfo = new \stdClass();
183 $insightinfo->insightname = $this->get_name();
184 $insightinfo->contextname = $context->get_context_name();
185 $subject = get_string('insightmessagesubject', 'analytics', $insightinfo);
186
1611308b 187 $users = $this->get_insights_users($context);
369389c9
DM
188
189 if (!$coursecontext = $context->get_course_context(false)) {
190 $coursecontext = \context_course::instance(SITEID);
191 }
192
193 foreach ($users as $user) {
194
195 $message = new \core\message\message();
6ec2ae0f 196 $message->component = 'moodle';
369389c9
DM
197 $message->name = 'insights';
198
ead38829 199 $message->userfrom = \core_user::get_noreply_user();
369389c9
DM
200 $message->userto = $user;
201
202 $insighturl = new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id);
203 $message->subject = $subject;
204 // Same than the subject.
205 $message->contexturlname = $message->subject;
206 $message->courseid = $coursecontext->instanceid;
207
63f2c0c6 208 $message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
369389c9 209 $message->fullmessageformat = FORMAT_PLAIN;
690ad875 210 $message->fullmessagehtml = get_string('insightinfomessagehtml', 'analytics', $insighturl->out());
63f2c0c6 211 $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
369389c9
DM
212 $message->contexturl = $insighturl->out(false);
213
369389c9
DM
214 message_send($message);
215 }
216 }
217
218 }
219
1611308b
DM
220 /**
221 * Returns the list of users that will receive insights notifications.
222 *
223 * Feel free to overwrite if you need to but keep in mind that moodle/analytics:listinsights
224 * capability is required to access the list of insights.
225 *
226 * @param \context $context
227 * @return array
228 */
229 protected function get_insights_users(\context $context) {
230 if ($context->contextlevel >= CONTEXT_COURSE) {
231 // At course level or below only enrolled users although this is not ideal for
232 // teachers assigned at category level.
233 $users = get_enrolled_users($context, 'moodle/analytics:listinsights');
234 } else {
235 $users = get_users_by_capability($context, 'moodle/analytics:listinsights');
236 }
237 return $users;
238 }
239
240 /**
241 * Returns an instance of the child class.
242 *
243 * Useful to reset cached data.
244 *
245 * @return \core_analytics\base\target
246 */
369389c9
DM
247 public static function instance() {
248 return new static();
249 }
250
251 /**
252 * Defines a boundary to ignore predictions below the specified prediction score.
253 *
254 * Value should go from 0 to 1.
255 *
256 * @return float
257 */
258 protected function min_prediction_score() {
259 // The default minimum discards predictions with a low score.
5c5cb3ee 260 return \core_analytics\model::PREDICTION_MIN_SCORE;
369389c9
DM
261 }
262
263 /**
325b3bdd 264 * This method determines if a prediction is interesing for the model or not.
369389c9 265 *
1611308b 266 * @param mixed $predictedvalue
413f19bc 267 * @param float $predictionscore
369389c9
DM
268 * @return bool
269 */
270 public function triggers_callback($predictedvalue, $predictionscore) {
271
272 $minscore = floatval($this->min_prediction_score());
273 if ($minscore < 0) {
274 debugging(get_class($this) . ' minimum prediction score is below 0, please update it to a value between 0 and 1.');
275 } else if ($minscore > 1) {
276 debugging(get_class($this) . ' minimum prediction score is above 1, please update it to a value between 0 and 1.');
277 }
278
279 // We need to consider that targets may not have a min score.
280 if (!empty($minscore) && floatval($predictionscore) < $minscore) {
281 return false;
282 }
283
369389c9
DM
284 return true;
285 }
286
287 /**
288 * Calculates the target.
289 *
290 * Returns an array of values which size matches $sampleids size.
291 *
292 * Rows with null values will be skipped as invalid by time splitting methods.
293 *
294 * @param array $sampleids
295 * @param \core_analytics\analysable $analysable
1611308b
DM
296 * @param int $starttime
297 * @param int $endtime
369389c9
DM
298 * @return array The format to follow is [userid] = scalar|null
299 */
1611308b 300 public function calculate($sampleids, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
369389c9
DM
301
302 if (!PHPUNIT_TEST && CLI_SCRIPT) {
303 echo '.';
304 }
305
306 $calculations = [];
307 foreach ($sampleids as $sampleid => $unusedsampleid) {
a40952d3 308
a40952d3 309 // No time limits when calculating the target to train models.
1611308b 310 $calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime);
369389c9
DM
311
312 if (!is_null($calculatedvalue)) {
413f19bc
DM
313 if ($this->is_linear() &&
314 ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
369389c9
DM
315 throw new \coding_exception('Calculated values should be higher than ' . static::get_min_value() .
316 ' and lower than ' . static::get_max_value() . '. ' . $calculatedvalue . ' received');
317 } else if (!$this->is_linear() && static::is_a_class($calculatedvalue) === false) {
318 throw new \coding_exception('Calculated values should be one of the target classes (' .
319 json_encode(static::get_classes()) . '). ' . $calculatedvalue . ' received');
320 }
321 }
322 $calculations[$sampleid] = $calculatedvalue;
323 }
324 return $calculations;
325 }
1611308b
DM
326
327 /**
328 * Filters out invalid samples for training.
329 *
330 * @param int[] $sampleids
331 * @param \core_analytics\analysable $analysable
413f19bc 332 * @param bool $fortraining
1611308b
DM
333 * @return void
334 */
335 public function filter_out_invalid_samples(&$sampleids, \core_analytics\analysable $analysable, $fortraining = true) {
336 foreach ($sampleids as $sampleid => $unusedsampleid) {
337 if (!$this->is_valid_sample($sampleid, $analysable, $fortraining)) {
338 // Skip it and remove the sample from the list of calculated samples.
339 unset($sampleids[$sampleid]);
340 }
341 }
342 }
369389c9 343}