MDL-65085 analytics: noreply user sends insights
[moodle.git] / analytics / classes / local / target / base.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  * 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  */
25 namespace core_analytics\local\target;
27 defined('MOODLE_INTERNAL') || die();
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 {
38     /**
39      * This target have linear or discrete values.
40      *
41      * @return bool
42      */
43     abstract public function is_linear();
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();
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);
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);
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);
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     }
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     }
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) {
115         global $PAGE;
117         $predictionid = $prediction->get_prediction_data()->id;
119         $PAGE->requires->js_call_amd('report_insights/actions', 'init', array($predictionid));
121         $actions = array();
123         if ($includedetailsaction) {
125             $predictionurl = new \moodle_url('/report/insights/prediction.php',
126                 array('id' => $predictionid));
128             $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
129                 $predictionurl, new \pix_icon('t/preview', get_string('viewprediction', 'analytics')),
130                 get_string('viewprediction', 'analytics'));
131         }
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);
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);
151         return $actions;
152     }
154     /**
155      * Callback to execute once a prediction has been returned from the predictions processor.
156      *
157      * Note that the analytics_predictions db record is not yet inserted.
158      *
159      * @param int $modelid
160      * @param int $sampleid
161      * @param int $rangeindex
162      * @param \context $samplecontext
163      * @param float|int $prediction
164      * @param float $predictionscore
165      * @return void
166      */
167     public function prediction_callback($modelid, $sampleid, $rangeindex, \context $samplecontext, $prediction, $predictionscore) {
168         return;
169     }
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) {
180         foreach ($samplecontexts as $context) {
182             $insightinfo = new \stdClass();
183             $insightinfo->insightname = $this->get_name();
184             $insightinfo->contextname = $context->get_context_name();
185             $subject = get_string('insightmessagesubject', 'analytics', $insightinfo);
187             $users = $this->get_insights_users($context);
189             if (!$coursecontext = $context->get_course_context(false)) {
190                 $coursecontext = \context_course::instance(SITEID);
191             }
193             foreach ($users as $user) {
195                 $message = new \core\message\message();
196                 $message->component = 'moodle';
197                 $message->name = 'insights';
199                 $message->userfrom = \core_user::get_noreply_user();
200                 $message->userto = $user;
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;
208                 $message->fullmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
209                 $message->fullmessageformat = FORMAT_PLAIN;
210                 $message->fullmessagehtml = get_string('insightinfomessagehtml', 'analytics', $insighturl->out());
211                 $message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out(false));
212                 $message->contexturl = $insighturl->out(false);
214                 message_send($message);
215             }
216         }
218     }
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     }
240     /**
241      * Returns an instance of the child class.
242      *
243      * Useful to reset cached data.
244      *
245      * @return \core_analytics\base\target
246      */
247     public static function instance() {
248         return new static();
249     }
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.
260         return \core_analytics\model::PREDICTION_MIN_SCORE;
261     }
263     /**
264      * This method determines if a prediction is interesing for the model or not.
265      *
266      * @param mixed $predictedvalue
267      * @param float $predictionscore
268      * @return bool
269      */
270     public function triggers_callback($predictedvalue, $predictionscore) {
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         }
279         // We need to consider that targets may not have a min score.
280         if (!empty($minscore) && floatval($predictionscore) < $minscore) {
281             return false;
282         }
284         return true;
285     }
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
296      * @param int $starttime
297      * @param int $endtime
298      * @return array The format to follow is [userid] = scalar|null
299      */
300     public function calculate($sampleids, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
302         if (!PHPUNIT_TEST && CLI_SCRIPT) {
303             echo '.';
304         }
306         $calculations = [];
307         foreach ($sampleids as $sampleid => $unusedsampleid) {
309             // No time limits when calculating the target to train models.
310             $calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime);
312             if (!is_null($calculatedvalue)) {
313                 if ($this->is_linear() &&
314                         ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
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     }
327     /**
328      * Filters out invalid samples for training.
329      *
330      * @param int[] $sampleids
331      * @param \core_analytics\analysable $analysable
332      * @param bool $fortraining
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     }