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
 * Moodle course analysable
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;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
require_once($CFG->dirroot . '/course/lib.php');
30
require_once($CFG->dirroot . '/lib/gradelib.php');
31
require_once($CFG->dirroot . '/lib/enrollib.php');
32
 
33
/**
34
 * Moodle course analysable
35
 *
36
 * @package   core_analytics
37
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
38
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39
 */
40
class course implements \core_analytics\analysable {
41
 
42
    /**
43
     * @var bool Has this course data been already loaded.
44
     */
45
    protected $loaded = false;
46
 
47
    /**
48
     * @var int $cachedid self::$cachedinstance analysable id.
49
     */
50
    protected static $cachedid = 0;
51
 
52
    /**
53
     * @var \core_analytics\course $cachedinstance
54
     */
55
    protected static $cachedinstance = null;
56
 
57
    /**
58
     * Course object
59
     *
60
     * @var \stdClass
61
     */
62
    protected $course = null;
63
 
64
    /**
65
     * The course context.
66
     *
67
     * @var \context_course
68
     */
69
    protected $coursecontext = null;
70
 
71
    /**
72
     * The course activities organized by activity type.
73
     *
74
     * @var array
75
     */
76
    protected $courseactivities = array();
77
 
78
    /**
79
     * Course start time.
80
     *
81
     * @var int
82
     */
83
    protected $starttime = null;
84
 
85
 
86
    /**
87
     * Has the course already started?
88
     *
89
     * @var bool
90
     */
91
    protected $started = null;
92
 
93
    /**
94
     * Course end time.
95
     *
96
     * @var int
97
     */
98
    protected $endtime = null;
99
 
100
    /**
101
     * Is the course finished?
102
     *
103
     * @var bool
104
     */
105
    protected $finished = null;
106
 
107
    /**
108
     * Course students ids.
109
     *
110
     * @var int[]
111
     */
112
    protected $studentids = [];
113
 
114
 
115
    /**
116
     * Course teachers ids
117
     *
118
     * @var int[]
119
     */
120
    protected $teacherids = [];
121
 
122
    /**
123
     * Cached copy of the total number of logs in the course.
124
     *
125
     * @var int
126
     */
127
    protected $ntotallogs = null;
128
 
129
    /** @var int Store current Unix timestamp. */
130
    protected int $now = 0;
131
 
132
    /**
133
     * Course manager constructor.
134
     *
135
     * Use self::instance() instead to get cached copies of the course. Instances obtained
136
     * through this constructor will not be cached.
137
     *
138
     * @param int|\stdClass $course Course id or mdl_course record
139
     * @param \context|null $context
140
     * @return void
141
     */
142
    public function __construct($course, ?\context $context = null) {
143
 
144
        if (is_scalar($course)) {
145
            $this->course = new \stdClass();
146
            $this->course->id = $course;
147
        } else {
148
            $this->course = $course;
149
        }
150
 
151
        if (!is_null($context)) {
152
            $this->coursecontext = $context;
153
        }
154
    }
155
 
156
    /**
157
     * Returns an analytics course instance.
158
     *
159
     * Lazy load of course data, students and teachers.
160
     *
161
     * @param int|\stdClass $course Course object or course id
162
     * @param \context|null $context
163
     * @return \core_analytics\course
164
     */
165
    public static function instance($course, ?\context $context = null) {
166
 
167
        $courseid = $course;
168
        if (!is_scalar($courseid)) {
169
            $courseid = $course->id;
170
        }
171
 
172
        if (self::$cachedid === $courseid) {
173
            return self::$cachedinstance;
174
        }
175
 
176
        $cachedinstance = new \core_analytics\course($course, $context);
177
        self::$cachedinstance = $cachedinstance;
178
        self::$cachedid = (int)$courseid;
179
        return self::$cachedinstance;
180
    }
181
 
182
    /**
183
     * get_id
184
     *
185
     * @return int
186
     */
187
    public function get_id() {
188
        return $this->course->id;
189
    }
190
 
191
    /**
192
     * Loads the analytics course object.
193
     *
194
     * @return void
195
     */
196
    protected function load() {
197
 
198
        // The instance constructor could be already loaded with the full course object. Using shortname
199
        // because it is a required course field.
200
        if (empty($this->course->shortname)) {
201
            $this->course = get_course($this->course->id);
202
        }
203
 
204
        $this->coursecontext = $this->get_context();
205
 
206
        $this->now = time();
207
 
208
        // Get the course users, including users assigned to student and teacher roles at an higher context.
209
        $cache = \cache::make_from_params(\cache_store::MODE_REQUEST, 'core_analytics', 'rolearchetypes');
210
 
211
        // Flag the instance as loaded.
212
        $this->loaded = true;
213
 
214
        if (!$studentroles = $cache->get('student')) {
215
            $studentroles = array_keys(get_archetype_roles('student'));
216
            $cache->set('student', $studentroles);
217
        }
218
        $this->studentids = $this->get_user_ids($studentroles);
219
 
220
        if (!$teacherroles = $cache->get('teacher')) {
221
            $teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
222
            $cache->set('teacher', $teacherroles);
223
        }
224
        $this->teacherids = $this->get_user_ids($teacherroles);
225
    }
226
 
227
    /**
228
     * The course short name
229
     *
230
     * @return string
231
     */
232
    public function get_name() {
233
        return format_string($this->get_course_data()->shortname, true, array('context' => $this->get_context()));
234
    }
235
 
236
    /**
237
     * get_context
238
     *
239
     * @return \context
240
     */
241
    public function get_context() {
242
        if ($this->coursecontext === null) {
243
            $this->coursecontext = \context_course::instance($this->course->id);
244
        }
245
        return $this->coursecontext;
246
    }
247
 
248
    /**
249
     * Get the course start timestamp.
250
     *
251
     * @return int Timestamp or 0 if has not started yet.
252
     */
253
    public function get_start() {
254
 
255
        if ($this->starttime !== null) {
256
            return $this->starttime;
257
        }
258
 
259
        // The field always exist but may have no valid if the course is created through a sync process.
260
        if (!empty($this->get_course_data()->startdate)) {
261
            $this->starttime = (int)$this->get_course_data()->startdate;
262
        } else {
263
            $this->starttime = 0;
264
        }
265
 
266
        return $this->starttime;
267
    }
268
 
269
    /**
270
     * Guesses the start of the course based on students' activity and enrolment start dates.
271
     *
272
     * @return int
273
     */
274
    public function guess_start() {
275
        global $DB;
276
 
277
        if (!$this->get_total_logs()) {
278
            // Can't guess.
279
            return 0;
280
        }
281
 
282
        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
283
            return 0;
284
        }
285
 
286
        // We first try to find current course student logs.
287
        $firstlogs = array();
288
        foreach ($this->get_students() as $studentid) {
289
            // Grrr, we are limited by logging API, we could do this easily with a
290
            // select min(timecreated) from xx where courseid = yy group by userid.
291
 
292
            // Filters based on the premise that more than 90% of people will be using
293
            // standard logstore, which contains a userid, contextlevel, contextinstanceid index.
294
            $select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid";
295
            $params = array('userid' => $studentid, 'contextlevel' => CONTEXT_COURSE, 'contextinstanceid' => $this->get_id());
296
            $events = $logstore->get_events_select($select, $params, 'timecreated ASC', 0, 1);
297
            if ($events) {
298
                $event = reset($events);
299
                $firstlogs[] = $event->timecreated;
300
            }
301
        }
302
        if (empty($firstlogs)) {
303
            // Can't guess if no student accesses.
304
            return 0;
305
        }
306
 
307
        sort($firstlogs);
308
        $firstlogsmedian = $this->median($firstlogs);
309
 
310
        $studentenrolments = enrol_get_course_users($this->get_id(), $this->get_students());
311
        if (empty($studentenrolments)) {
312
            return 0;
313
        }
314
 
315
        $enrolstart = array();
316
        foreach ($studentenrolments as $studentenrolment) {
317
            $enrolstart[] = ($studentenrolment->uetimestart) ? $studentenrolment->uetimestart : $studentenrolment->uetimecreated;
318
        }
319
        sort($enrolstart);
320
        $enrolstartmedian = $this->median($enrolstart);
321
 
322
        return intval(($enrolstartmedian + $firstlogsmedian) / 2);
323
    }
324
 
325
    /**
326
     * Get the course end timestamp.
327
     *
328
     * @return int Timestamp or 0 if time end was not set.
329
     */
330
    public function get_end() {
331
        global $DB;
332
 
333
        if ($this->endtime !== null) {
334
            return $this->endtime;
335
        }
336
 
337
        // The enddate field is only available from Moodle 3.2 (MDL-22078).
338
        if (!empty($this->get_course_data()->enddate)) {
339
            $this->endtime = (int)$this->get_course_data()->enddate;
340
            return $this->endtime;
341
        }
342
 
343
        return 0;
344
    }
345
 
346
    /**
347
     * Get the course end timestamp.
348
     *
349
     * @return int Timestamp, \core_analytics\analysable::MAX_TIME if we don't know but ongoing and 0 if we can not work it out.
350
     */
351
    public function guess_end() {
352
        global $DB;
353
 
354
        if ($this->get_total_logs() === 0) {
355
            // No way to guess if there are no logs.
356
            $this->endtime = 0;
357
            return $this->endtime;
358
        }
359
 
360
        list($filterselect, $filterparams) = $this->course_students_query_filter('ula');
361
 
362
        // Consider the course open if there are still student accesses.
363
        $monthsago = time() - (WEEKSECS * 4 * 2);
364
        $select = $filterselect . ' AND timeaccess > :timeaccess';
365
        $params = $filterparams + array('timeaccess' => $monthsago);
366
        $sql = "SELECT DISTINCT timeaccess FROM {user_lastaccess} ula
367
                  JOIN {enrol} e ON e.courseid = ula.courseid
368
                  JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ue.userid = ula.userid
369
                 WHERE $select";
370
        if ($records = $DB->get_records_sql($sql, $params)) {
371
            return 0;
372
        }
373
 
374
        $sql = "SELECT DISTINCT timeaccess FROM {user_lastaccess} ula
375
                  JOIN {enrol} e ON e.courseid = ula.courseid
376
                  JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ue.userid = ula.userid
377
                 WHERE $filterselect AND ula.timeaccess != 0
378
                 ORDER BY timeaccess DESC";
379
        $studentlastaccesses = $DB->get_fieldset_sql($sql, $filterparams);
380
        if (empty($studentlastaccesses)) {
381
            return 0;
382
        }
383
        sort($studentlastaccesses);
384
 
385
        return $this->median($studentlastaccesses);
386
    }
387
 
388
    /**
389
     * Returns a course plain object.
390
     *
391
     * @return \stdClass
392
     */
393
    public function get_course_data() {
394
 
395
        if (!$this->loaded) {
396
            $this->load();
397
        }
398
 
399
        return $this->course;
400
    }
401
 
402
    /**
403
     * Has the course started?
404
     *
405
     * @return bool
406
     */
407
    public function was_started() {
408
 
409
        if ($this->started === null) {
410
            if ($this->get_start() === 0 || $this->now < $this->get_start()) {
411
                // Not yet started.
412
                $this->started = false;
413
            } else {
414
                $this->started = true;
415
            }
416
        }
417
 
418
        return $this->started;
419
    }
420
 
421
    /**
422
     * Has the course finished?
423
     *
424
     * @return bool
425
     */
426
    public function is_finished() {
427
 
428
        if ($this->finished === null) {
429
            $endtime = $this->get_end();
430
            if ($endtime === 0 || $this->now < $endtime) {
431
                // It is not yet finished or no idea when it finishes.
432
                $this->finished = false;
433
            } else {
434
                $this->finished = true;
435
            }
436
        }
437
 
438
        return $this->finished;
439
    }
440
 
441
    /**
442
     * Returns a list of user ids matching the specified roles in this course.
443
     *
444
     * @param array $roleids
445
     * @return array
446
     */
447
    public function get_user_ids($roleids) {
448
 
449
        // We need to index by ra.id as a user may have more than 1 $roles role.
450
        $records = get_role_users($roleids, $this->get_context(), true, 'ra.id, u.id AS userid, r.id AS roleid', 'ra.id ASC');
451
 
452
        // If a user have more than 1 $roles role array_combine will discard the duplicate.
453
        $callable = array($this, 'filter_user_id');
454
        $userids = array_values(array_map($callable, $records));
455
        return array_combine($userids, $userids);
456
    }
457
 
458
    /**
459
     * Returns the course students.
460
     *
461
     * @return int[]
462
     */
463
    public function get_students() {
464
 
465
        if (!$this->loaded) {
466
            $this->load();
467
        }
468
 
469
        return $this->studentids;
470
    }
471
 
472
    /**
473
     * Returns the total number of student logs in the course
474
     *
475
     * @return int
476
     */
477
    public function get_total_logs() {
478
        global $DB;
479
 
480
        // No logs if no students.
481
        if (empty($this->get_students())) {
482
            return 0;
483
        }
484
 
485
        if ($this->ntotallogs === null) {
486
            list($filterselect, $filterparams) = $this->course_students_query_filter();
487
            if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
488
                $this->ntotallogs = 0;
489
            } else {
490
                $this->ntotallogs = $logstore->get_events_select_count($filterselect, $filterparams);
491
            }
492
        }
493
 
494
        return $this->ntotallogs;
495
    }
496
 
497
    /**
498
     * Returns all the activities of the provided type the course has.
499
     *
500
     * @param string $activitytype
501
     * @return array
502
     */
503
    public function get_all_activities($activitytype) {
504
 
505
        // Using is set because we set it to false if there are no activities.
506
        if (!isset($this->courseactivities[$activitytype])) {
507
            $modinfo = get_fast_modinfo($this->get_course_data(), -1);
508
            $instances = $modinfo->get_instances_of($activitytype);
509
 
510
            if ($instances) {
511
                $this->courseactivities[$activitytype] = array();
512
                foreach ($instances as $instance) {
513
                    // By context.
514
                    $this->courseactivities[$activitytype][$instance->context->id] = $instance;
515
                }
516
            } else {
517
                $this->courseactivities[$activitytype] = false;
518
            }
519
        }
520
 
521
        return $this->courseactivities[$activitytype];
522
    }
523
 
524
    /**
525
     * Returns the course students grades.
526
     *
527
     * @param array $courseactivities
528
     * @return array
529
     */
530
    public function get_student_grades($courseactivities) {
531
 
532
        if (empty($courseactivities)) {
533
            return array();
534
        }
535
 
536
        $grades = array();
537
        foreach ($courseactivities as $contextid => $instance) {
538
            $gradesinfo = grade_get_grades($this->course->id, 'mod', $instance->modname, $instance->instance, $this->studentids);
539
 
540
            // Sort them by activity context and user.
541
            if ($gradesinfo && $gradesinfo->items) {
542
                foreach ($gradesinfo->items as $gradeitem) {
543
                    foreach ($gradeitem->grades as $userid => $grade) {
544
                        if (empty($grades[$contextid][$userid])) {
545
                            // Initialise it as array because a single activity can have multiple grade items (e.g. workshop).
546
                            $grades[$contextid][$userid] = array();
547
                        }
548
                        $grades[$contextid][$userid][$gradeitem->id] = $grade;
549
                    }
550
                }
551
            }
552
        }
553
 
554
        return $grades;
555
    }
556
 
557
    /**
558
     * Used by get_user_ids to extract the user id.
559
     *
560
     * @param \stdClass $record
561
     * @return int The user id.
562
     */
563
    protected function filter_user_id($record) {
564
        return $record->userid;
565
    }
566
 
567
    /**
568
     * Returns the average time between 2 timestamps.
569
     *
570
     * @param int $start
571
     * @param int $end
572
     * @return array [starttime, averagetime, endtime]
573
     */
574
    protected function update_loop_times($start, $end) {
575
        $avg = intval(($start + $end) / 2);
576
        return array($start, $avg, $end);
577
    }
578
 
579
    /**
580
     * Returns the query and params used to filter the logstore by this course students.
581
     *
582
     * @param string $prefix
583
     * @return array
584
     */
585
    protected function course_students_query_filter($prefix = false) {
586
        global $DB;
587
 
588
        if ($prefix) {
589
            $prefix = $prefix . '.';
590
        }
591
 
592
        // Check the amount of student logs in the 4 previous weeks.
593
        list($studentssql, $studentsparams) = $DB->get_in_or_equal($this->get_students(), SQL_PARAMS_NAMED);
594
        $filterselect = $prefix . 'courseid = :courseid AND ' . $prefix . 'userid ' . $studentssql;
595
        $filterparams = array('courseid' => $this->course->id) + $studentsparams;
596
 
597
        return array($filterselect, $filterparams);
598
    }
599
 
600
    /**
601
     * Calculate median
602
     *
603
     * Keys are ignored.
604
     *
605
     * @param int[]|float[] $values Sorted array of values
606
     * @return int
607
     */
608
    protected function median($values) {
609
        $count = count($values);
610
 
611
        if ($count === 1) {
612
            return reset($values);
613
        }
614
 
615
        $middlevalue = (int)floor(($count - 1) / 2);
616
 
617
        if ($count % 2) {
618
            // Odd number, middle is the median.
619
            $median = $values[$middlevalue];
620
        } else {
621
            // Even number, calculate avg of 2 medians.
622
            $low = $values[$middlevalue];
623
            $high = $values[$middlevalue + 1];
624
            $median = (($low + $high) / 2);
625
        }
626
        return intval($median);
627
    }
628
}