Proyectos de Subversion Moodle

Rev

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