Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}