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
 * Community of inquiry abstract indicator.
19
 *
20
 * @package   core_analytics
21
 * @copyright 2017 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\indicator;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Community of inquire abstract indicator.
31
 *
32
 * @package   core_analytics
33
 * @copyright 2017 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 community_of_inquiry_activity extends linear {
37
 
38
    /**
39
     * instancedata
40
     *
41
     * @var array
42
     */
43
    protected $instancedata = array();
44
 
45
    /**
46
     * @var \core_analytics\course
47
     */
48
    protected $course = null;
49
 
50
    /**
51
     * @var array Array of logs by [contextid][userid]
52
     */
53
    protected $activitylogs = null;
54
 
55
    /**
56
     * @var array Array of grades by [contextid][userid]
57
     */
58
    protected $grades = null;
59
 
60
    /**
61
     * Constant cognitive indicator type.
62
     */
63
    const INDICATOR_COGNITIVE = "cognitve";
64
 
65
    /**
66
     * Constant social indicator type.
67
     */
68
    const INDICATOR_SOCIAL = "social";
69
 
70
    /**
71
     * Constant for this cognitive level.
72
     */
73
    const COGNITIVE_LEVEL_1 = 1;
74
 
75
    /**
76
     * Constant for this cognitive level.
77
     */
78
    const COGNITIVE_LEVEL_2 = 2;
79
 
80
    /**
81
     * Constant for this cognitive level.
82
     */
83
    const COGNITIVE_LEVEL_3 = 3;
84
 
85
    /**
86
     * Constant for this cognitive level.
87
     */
88
    const COGNITIVE_LEVEL_4 = 4;
89
 
90
    /**
91
     * Constant for this cognitive level.
92
     */
93
    const COGNITIVE_LEVEL_5 = 5;
94
 
95
    /**
96
     * Constant for this social level.
97
     */
98
    const SOCIAL_LEVEL_1 = 1;
99
 
100
    /**
101
     * Constant for this social level.
102
     */
103
    const SOCIAL_LEVEL_2 = 2;
104
 
105
    /**
106
     * Constant for this social level.
107
     */
108
    const SOCIAL_LEVEL_3 = 3;
109
 
110
    /**
111
     * Constant for this social level.
112
     */
113
    const SOCIAL_LEVEL_4 = 4;
114
 
115
    /**
116
     * Constant for this social level.
117
     */
118
    const SOCIAL_LEVEL_5 = 5;
119
 
120
    /**
121
     * Max cognitive depth level accepted.
122
     */
123
    const MAX_COGNITIVE_LEVEL = 5;
124
 
125
    /**
126
     * Max social breadth level accepted.
127
     */
128
    const MAX_SOCIAL_LEVEL = 5;
129
 
130
    /**
131
     * Fetch the course grades of this activity type instances.
132
     *
133
     * @param \core_analytics\analysable $analysable
134
     * @return void
135
     */
136
    public function fill_per_analysable_caches(\core_analytics\analysable $analysable) {
137
 
138
        // Better to check it, we can not be 100% it will be a \core_analytics\course object.
139
        if ($analysable instanceof \core_analytics\course) {
140
            $this->fetch_student_grades($analysable);
141
        }
142
    }
143
 
144
    /**
145
     * Returns the activity type. No point in changing this class in children classes.
146
     *
147
     * @var string The activity name (e.g. assign or quiz)
148
     */
149
    final public function get_activity_type() {
150
        $class = get_class($this);
151
        $package = stristr($class, "\\", true);
152
        $type = str_replace("mod_", "", $package);
153
        if ($type === $package) {
154
            throw new \coding_exception("$class does not belong to any module specific namespace");
155
        }
156
        return $type;
157
    }
158
 
159
    /**
160
     * Returns the potential level of cognitive depth.
161
     *
162
     * @param \cm_info $cm
163
     * @return int
164
     */
165
    public function get_cognitive_depth_level(\cm_info $cm) {
166
        throw new \coding_exception('Overwrite get_cognitive_depth_level method to set your activity potential cognitive ' .
167
            'depth level');
168
    }
169
 
170
    /**
171
     * Returns the potential level of social breadth.
172
     *
173
     * @param \cm_info $cm
174
     * @return int
175
     */
176
    public function get_social_breadth_level(\cm_info $cm) {
177
        throw new \coding_exception('Overwrite get_social_breadth_level method to set your activity potential social ' .
178
            'breadth level');
179
    }
180
 
181
    /**
182
     * required_sample_data
183
     *
184
     * @return string[]
185
     */
186
    public static function required_sample_data() {
187
        // Only course because the indicator is valid even without students.
188
        return array('course');
189
    }
190
 
191
    /**
192
     * Do activity logs contain any log of user in this context?
193
     *
194
     * If user is empty we look for any log in this context.
195
     *
196
     * @param int $contextid
197
     * @param \stdClass|false $user
198
     * @return bool
199
     */
200
    final protected function any_log($contextid, $user) {
201
        if (empty($this->activitylogs[$contextid])) {
202
            return false;
203
        }
204
 
205
        // Someone interacted with the activity if there is no user or the user interacted with the
206
        // activity if there is a user.
207
        if (empty($user) ||
208
                (!empty($user) && !empty($this->activitylogs[$contextid][$user->id]))) {
209
            return true;
210
        }
211
 
212
        return false;
213
    }
214
 
215
    /**
216
     * Do activity logs contain any write log of user in this context?
217
     *
218
     * If user is empty we look for any write log in this context.
219
     *
220
     * @param int $contextid
221
     * @param \stdClass|false $user
222
     * @return bool
223
     */
224
    final protected function any_write_log($contextid, $user) {
225
        if (empty($this->activitylogs[$contextid])) {
226
            return false;
227
        }
228
 
229
        // No specific user, we look at all activity logs.
230
        $it = $this->activitylogs[$contextid];
231
        if ($user) {
232
            if (empty($this->activitylogs[$contextid][$user->id])) {
233
                return false;
234
            }
235
            $it = array($user->id => $this->activitylogs[$contextid][$user->id]);
236
        }
237
        foreach ($it as $events) {
238
            foreach ($events as $log) {
239
                if ($log->crud === 'c' || $log->crud === 'u') {
240
                    return true;
241
                }
242
            }
243
        }
244
 
245
        return false;
246
    }
247
 
248
    /**
249
     * Is there any feedback activity log for this user in this context?
250
     *
251
     * This method returns true if $user is empty and there is any feedback activity logs.
252
     *
253
     * @param string $action
254
     * @param \cm_info $cm
255
     * @param int $contextid
256
     * @param \stdClass|false $user
257
     * @return bool
258
     */
259
    protected function any_feedback($action, \cm_info $cm, $contextid, $user) {
260
 
261
        if (!in_array($action, ['submitted', 'replied', 'viewed'])) {
262
            throw new \coding_exception('Provided action "' . $action . '" is not valid.');
263
        }
264
 
265
        if (empty($this->activitylogs[$contextid])) {
266
            return false;
267
        }
268
 
269
        if (empty($this->grades[$contextid]) && $this->feedback_check_grades()) {
270
            // If there are no grades there is no feedback.
271
            return false;
272
        }
273
 
274
        $it = $this->activitylogs[$contextid];
275
        if ($user) {
276
            if (empty($this->activitylogs[$contextid][$user->id])) {
277
                return false;
278
            }
279
            $it = array($user->id => $this->activitylogs[$contextid][$user->id]);
280
        }
281
 
282
        foreach ($this->activitylogs[$contextid] as $userid => $events) {
283
            $methodname = 'feedback_' . $action;
284
            if ($this->{$methodname}($cm, $contextid, $userid)) {
285
                return true;
286
            }
287
            // If it wasn't viewed try with the next user.
288
        }
289
        return false;
290
    }
291
 
292
    /**
293
     * $cm is used for this method overrides.
294
     *
295
     * This function must be fast.
296
     *
297
     * @param \cm_info $cm
298
     * @param mixed $contextid
299
     * @param mixed $userid
300
     * @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
301
     * @return bool
302
     */
303
    protected function feedback_viewed(\cm_info $cm, $contextid, $userid, $after = null) {
304
        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_viewed_events(), $after);
305
    }
306
 
307
    /**
308
     * $cm is used for this method overrides.
309
     *
310
     * This function must be fast.
311
     *
312
     * @param \cm_info $cm
313
     * @param mixed $contextid
314
     * @param mixed $userid
315
     * @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
316
     * @return bool
317
     */
318
    protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = null) {
319
        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_replied_events(), $after);
320
    }
321
 
322
    /**
323
     * $cm is used for this method overrides.
324
     *
325
     * This function must be fast.
326
     *
327
     * @param \cm_info $cm
328
     * @param mixed $contextid
329
     * @param mixed $userid
330
     * @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
331
     * @return bool
332
     */
333
    protected function feedback_submitted(\cm_info $cm, $contextid, $userid, $after = null) {
334
        return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_submitted_events(), $after);
335
    }
336
 
337
    /**
338
     * Returns the list of events that involve viewing feedback from other users.
339
     *
340
     * @return string[]
341
     */
342
    protected function feedback_viewed_events() {
343
        throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
344
            'should define "feedback_viewed_events" method or should override feedback_viewed method.');
345
    }
346
 
347
    /**
348
     * Returns the list of events that involve replying to feedback from other users.
349
     *
350
     * @return string[]
351
     */
352
    protected function feedback_replied_events() {
353
        throw new \coding_exception('Activities with a potential cognitive or social level that include replying to feedback ' .
354
            'should define "feedback_replied_events" method or should override feedback_replied method.');
355
    }
356
 
357
    /**
358
     * Returns the list of events that involve submitting something after receiving feedback from other users.
359
     *
360
     * @return string[]
361
     */
362
    protected function feedback_submitted_events() {
363
        throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
364
            'should define "feedback_submitted_events" method or should override feedback_submitted method.');
365
    }
366
 
367
    /**
368
     * Whether this user in this context did any of the provided actions (events)
369
     *
370
     * @param \cm_info $cm
371
     * @param int $contextid
372
     * @param int $userid
373
     * @param string[] $eventnames
374
     * @param int|false $after
375
     * @return bool
376
     */
377
    protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = null) {
378
        if ($after === null) {
379
            if ($this->feedback_check_grades()) {
380
                if (!$after = $this->get_graded_date($contextid, $userid)) {
381
                    return false;
382
                }
383
            } else {
384
                $after = false;
385
            }
386
        }
387
 
388
        if (empty($this->activitylogs[$contextid][$userid])) {
389
            return false;
390
        }
391
 
392
        foreach ($eventnames as $eventname) {
393
            if (!$after) {
394
                if (!empty($this->activitylogs[$contextid][$userid][$eventname])) {
395
                    // If we don't care about when the feedback has been seen we consider this enough.
396
                    return true;
397
                }
398
            } else {
399
                if (empty($this->activitylogs[$contextid][$userid][$eventname])) {
400
                    continue;
401
                }
402
                $timestamps = $this->activitylogs[$contextid][$userid][$eventname]->timecreated;
403
                // Faster to start by the end.
404
                rsort($timestamps);
405
                foreach ($timestamps as $timestamp) {
406
                    if ($timestamp > $after) {
407
                        return true;
408
                    }
409
                }
410
            }
411
        }
412
        return false;
413
    }
414
 
415
    /**
416
     * Returns the date a user was graded.
417
     *
418
     * @param int $contextid
419
     * @param int $userid
420
     * @param bool $checkfeedback Check that the student was graded or check that feedback was given
421
     * @return int|false
422
     */
423
    protected function get_graded_date($contextid, $userid, $checkfeedback = false) {
424
        if (empty($this->grades[$contextid][$userid])) {
425
            return false;
426
        }
427
        foreach ($this->grades[$contextid][$userid] as $gradeitemid => $gradeitem) {
428
 
429
            // We check that either feedback or the grade is set.
430
            if (($checkfeedback && $gradeitem->feedback) || $gradeitem->grade) {
431
 
432
                // Grab the first graded date.
433
                if ($gradeitem->dategraded && (empty($after) || $gradeitem->dategraded < $after)) {
434
                    $after = $gradeitem->dategraded;
435
                }
436
            }
437
        }
438
 
439
        if (!isset($after)) {
440
            // False if there are no graded items.
441
            return false;
442
        }
443
 
444
        return $after;
445
    }
446
 
447
    /**
448
     * Returns the activities the user had access to between a time period.
449
     *
450
     * @param int $sampleid
451
     * @param string $tablename
452
     * @param int $starttime
453
     * @param int $endtime
454
     * @return array
455
     */
456
    protected function get_student_activities($sampleid, $tablename, $starttime, $endtime) {
457
 
458
        // May not be available.
459
        $user = $this->retrieve('user', $sampleid);
460
 
461
        if ($this->course === null) {
462
            // The indicator scope is a range, so all activities belong to the same course.
463
            $this->course = \core_analytics\course::instance($this->retrieve('course', $sampleid));
464
        }
465
 
466
        if ($this->activitylogs === null) {
467
            // Fetch all activity logs in each activity in the course, not restricted to a specific sample so we can cache it.
468
 
469
            $courseactivities = $this->course->get_all_activities($this->get_activity_type());
470
 
471
            // Null if no activities of this type in this course.
472
            if (empty($courseactivities)) {
473
                $this->activitylogs = false;
474
                return null;
475
            }
476
            $this->activitylogs = $this->fetch_activity_logs($courseactivities, $starttime, $endtime);
477
        }
478
 
479
        if ($this->grades === null) {
480
            // Even if this is probably already filled during fill_per_analysable_caches.
481
            $this->fetch_student_grades($this->course);
482
        }
483
 
484
        if ($cm = $this->retrieve('cm', $sampleid)) {
485
            // Samples are at cm level or below.
486
            $useractivities = array(\context_module::instance($cm->id)->id => $cm);
487
        } else {
488
            // Activities that should be completed during this time period.
489
            $useractivities = $this->get_activities($starttime, $endtime, $user);
490
        }
491
 
492
        return $useractivities;
493
    }
494
 
495
    /**
496
     * Fetch acitivity logs from database
497
     *
498
     * @param array $activities
499
     * @param int $starttime
500
     * @param int $endtime
501
     * @return array
502
     */
503
    protected function fetch_activity_logs($activities, $starttime = false, $endtime = false) {
504
        global $DB;
505
 
506
        // Filter by context to use the db table index.
507
        list($contextsql, $contextparams) = $DB->get_in_or_equal(array_keys($activities), SQL_PARAMS_NAMED);
508
        $select = "contextid $contextsql AND timecreated > :starttime AND timecreated <= :endtime";
509
        $params = $contextparams + array('starttime' => $starttime, 'endtime' => $endtime);
510
 
511
        // Pity that we need to pass through logging readers API when most of the people just uses the standard one.
512
        if (!$logstore = \core_analytics\manager::get_analytics_logstore()) {
513
            throw new \coding_exception('No log store available');
514
        }
515
        $events = $logstore->get_events_select_iterator($select, $params, 'timecreated ASC', 0, 0);
516
 
517
        // Returs the logs organised by contextid, userid and eventname so it is easier to calculate activities data later.
518
        // At the same time we want to keep this array reasonably "not-massive".
519
        $processedevents = array();
520
        foreach ($events as $event) {
521
            if (!isset($processedevents[$event->contextid])) {
522
                $processedevents[$event->contextid] = array();
523
            }
524
            if (!isset($processedevents[$event->contextid][$event->userid])) {
525
                $processedevents[$event->contextid][$event->userid] = array();
526
            }
527
 
528
            // Contextid and userid have already been used to index the events, the next field to index by is eventname:
529
            // crud is unique per eventname, courseid is the same for all records and we append timecreated.
530
            if (!isset($processedevents[$event->contextid][$event->userid][$event->eventname])) {
531
 
532
                // Remove all data that can change between events of the same type.
533
                $data = (object)$event->get_data();
534
                unset($data->id);
535
                unset($data->anonymous);
536
                unset($data->relateduserid);
537
                unset($data->other);
538
                unset($data->origin);
539
                unset($data->ip);
540
                $processedevents[$event->contextid][$event->userid][$event->eventname] = $data;
541
                // We want timecreated attribute to be an array containing all user access times.
542
                $processedevents[$event->contextid][$event->userid][$event->eventname]->timecreated = array();
543
            }
544
 
545
            // Add the event timecreated.
546
            $processedevents[$event->contextid][$event->userid][$event->eventname]->timecreated[] = intval($event->timecreated);
547
        }
548
        $events->close();
549
 
550
        return $processedevents;
551
    }
552
 
553
    /**
554
     * Whether grades should be checked or not when looking for feedback.
555
     *
556
     * @return bool
557
     */
558
    protected function feedback_check_grades() {
559
        return true;
560
    }
561
 
562
    /**
563
     * Calculates the cognitive depth of a sample.
564
     *
565
     * @param int $sampleid
566
     * @param string $tablename
567
     * @param int $starttime
568
     * @param int $endtime
569
     * @return float|int|null
570
     * @throws \coding_exception
571
     */
572
    protected function cognitive_calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
573
 
574
        // May not be available.
575
        $user = $this->retrieve('user', $sampleid);
576
 
577
        if (!$useractivities = $this->get_student_activities($sampleid, $tablename, $starttime, $endtime)) {
578
            // Null if no activities.
579
            return null;
580
        }
581
 
582
        $scoreperactivity = (self::get_max_value() - self::get_min_value()) / count($useractivities);
583
 
584
        $score = self::get_min_value();
585
 
586
        // Iterate through the module activities/resources which due date is part of this time range.
587
        foreach ($useractivities as $contextid => $cm) {
588
 
589
            $potentiallevel = $this->get_cognitive_depth_level($cm);
590
            if (!is_int($potentiallevel)
591
                    || $potentiallevel > self::MAX_COGNITIVE_LEVEL
592
                    || $potentiallevel < self::COGNITIVE_LEVEL_1) {
593
                throw new \coding_exception('Activities\' potential cognitive depth go from 1 to 5.');
594
            }
595
            $scoreperlevel = $scoreperactivity / $potentiallevel;
596
 
597
            switch ($potentiallevel) {
598
                case self::COGNITIVE_LEVEL_5:
599
                    // Cognitive level 5 is to submit after feedback.
600
                    if ($this->any_feedback('submitted', $cm, $contextid, $user)) {
601
                        $score += $scoreperlevel * 5;
602
                        break;
603
                    }
604
                    // The user didn't reach the activity max cognitive depth, continue with level 2.
605
 
606
                case self::COGNITIVE_LEVEL_4:
607
                    // Cognitive level 4 is to comment on feedback.
608
                    if ($this->any_feedback('replied', $cm, $contextid, $user)) {
609
                        $score += $scoreperlevel * 4;
610
                        break;
611
                    }
612
                    // The user didn't reach the activity max cognitive depth, continue with level 2.
613
 
614
                case self::COGNITIVE_LEVEL_3:
615
                    // Cognitive level 3 is to view feedback.
616
 
617
                    if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
618
                        // Max score for level 3.
619
                        $score += $scoreperlevel * 3;
620
                        break;
621
                    }
622
                    // The user didn't reach the activity max cognitive depth, continue with level 2.
623
 
624
                case self::COGNITIVE_LEVEL_2:
625
                    // Cognitive depth level 2 is to submit content.
626
 
627
                    if ($this->any_write_log($contextid, $user)) {
628
                        $score += $scoreperlevel * 2;
629
                        break;
630
                    }
631
                    // The user didn't reach the activity max cognitive depth, continue with level 1.
632
 
633
                case self::COGNITIVE_LEVEL_1:
634
                    // Cognitive depth level 1 is just accessing the activity.
635
 
636
                    if ($this->any_log($contextid, $user)) {
637
                        $score += $scoreperlevel;
638
                    }
639
 
640
                default:
641
            }
642
        }
643
 
644
        // To avoid decimal problems.
645
        if ($score > self::MAX_VALUE) {
646
            return self::MAX_VALUE;
647
        } else if ($score < self::MIN_VALUE) {
648
            return self::MIN_VALUE;
649
        }
650
        return $score;
651
    }
652
 
653
    /**
654
     * Calculates the social breadth of a sample.
655
     *
656
     * @param int $sampleid
657
     * @param string $tablename
658
     * @param int $starttime
659
     * @param int $endtime
660
     * @return float|int|null
661
     */
662
    protected function social_calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
663
 
664
        // May not be available.
665
        $user = $this->retrieve('user', $sampleid);
666
 
667
        if (!$useractivities = $this->get_student_activities($sampleid, $tablename, $starttime, $endtime)) {
668
            // Null if no activities.
669
            return null;
670
        }
671
 
672
        $scoreperactivity = (self::get_max_value() - self::get_min_value()) / count($useractivities);
673
 
674
        $score = self::get_min_value();
675
 
676
        foreach ($useractivities as $contextid => $cm) {
677
 
678
            $potentiallevel = $this->get_social_breadth_level($cm);
679
            if (!is_int($potentiallevel)
680
                    || $potentiallevel > self::MAX_SOCIAL_LEVEL
681
                    || $potentiallevel < self::SOCIAL_LEVEL_1) {
682
                throw new \coding_exception('Activities\' potential social breadth go from 1 to ' .
683
                    community_of_inquiry_activity::MAX_SOCIAL_LEVEL . '.');
684
            }
685
            $scoreperlevel = $scoreperactivity / $potentiallevel;
686
            switch ($potentiallevel) {
687
                case self::SOCIAL_LEVEL_2:
688
                case self::SOCIAL_LEVEL_3:
689
                case self::SOCIAL_LEVEL_4:
690
                case self::SOCIAL_LEVEL_5:
691
                    // Core activities social breadth only reaches level 2, until core activities social
692
                    // breadth do not reach level 5 we limit it to what we currently support, which is level 2.
693
 
694
                    // Social breadth level 2 is to view feedback. (Same as cognitive level 3).
695
 
696
                    if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
697
                        // Max score for level 2.
698
                        $score += $scoreperlevel * 2;
699
                        break;
700
                    }
701
                    // The user didn't reach the activity max social breadth, continue with level 1.
702
 
703
                case self::SOCIAL_LEVEL_1:
704
                    // Social breadth level 1 is just accessing the activity.
705
                    if ($this->any_log($contextid, $user)) {
706
                        $score += $scoreperlevel;
707
                    }
708
            }
709
 
710
        }
711
 
712
        // To avoid decimal problems.
713
        if ($score > self::MAX_VALUE) {
714
            return self::MAX_VALUE;
715
        } else if ($score < self::MIN_VALUE) {
716
            return self::MIN_VALUE;
717
        }
718
        return $score;
719
    }
720
 
721
    /**
722
     * calculate_sample
723
     *
724
     * @throws \coding_exception
725
     * @param int $sampleid
726
     * @param string $tablename
727
     * @param int $starttime
728
     * @param int $endtime
729
     * @return float|int|null
730
     */
731
    protected function calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
732
        if ($this->get_indicator_type() == self::INDICATOR_COGNITIVE) {
733
            return $this->cognitive_calculate_sample($sampleid, $tablename, $starttime, $endtime);
734
        } else if ($this->get_indicator_type() == self::INDICATOR_SOCIAL) {
735
            return $this->social_calculate_sample($sampleid, $tablename, $starttime, $endtime);
736
        }
737
        throw new \coding_exception("Indicator type is invalid.");
738
    }
739
 
740
    /**
741
     * Gets the course student grades.
742
     *
743
     * @param \core_analytics\course $course
744
     * @return void
745
     */
746
    protected function fetch_student_grades(\core_analytics\course $course) {
747
        $courseactivities = $course->get_all_activities($this->get_activity_type());
748
        $this->grades = $course->get_student_grades($courseactivities);
749
    }
750
 
751
    /**
752
     * Guesses all activities that were available during a period of time.
753
     *
754
     * @param int $starttime
755
     * @param int $endtime
756
     * @param \stdClass|false $student
757
     * @return array
758
     */
759
    protected function get_activities($starttime, $endtime, $student = false) {
760
 
761
        $activitytype = $this->get_activity_type();
762
 
763
        // Var $student may not be available, default to not calculating dynamic data.
764
        $studentid = -1;
765
        if ($student) {
766
            $studentid = $student->id;
767
        }
768
        $modinfo = get_fast_modinfo($this->course->get_course_data(), $studentid);
769
        $activities = $modinfo->get_instances_of($activitytype);
770
 
771
        $timerangeactivities = array();
772
        foreach ($activities as $activity) {
773
 
774
            if (!$this->activity_completed_by($activity, $starttime, $endtime, $student)) {
775
                continue;
776
            }
777
 
778
            $timerangeactivities[$activity->context->id] = $activity;
779
        }
780
 
781
        return $timerangeactivities;
782
    }
783
 
784
    /**
785
     * Was the activity supposed to be completed during the provided time range?.
786
     *
787
     * @param \cm_info $activity
788
     * @param int $starttime
789
     * @param int $endtime
790
     * @param \stdClass|false $student
791
     * @return bool
792
     */
793
    protected function activity_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
794
 
795
        // We can't check uservisible because:
796
        // - Any activity with available until would not be counted.
797
        // - Sites may block student's course view capabilities once the course is closed.
798
 
799
        // Students can not view hidden activities by default, this is not reliable 100% but accurate in most of the cases.
800
        if ($activity->visible === false) {
801
            return false;
802
        }
803
 
804
        // Give priority to the different methods activities have to set a "due" date.
805
        $return = $this->activity_type_completed_by($activity, $starttime, $endtime, $student);
806
        if (!is_null($return)) {
807
            // Method activity_type_completed_by returns null if there is no due date method or there is but it is not set.
808
            return $return;
809
        }
810
 
811
        // We skip activities that were not yet visible or their 'until' was not in this $starttime - $endtime range.
812
        if ($activity->availability) {
813
            $info = new \core_availability\info_module($activity);
814
            $activityavailability = $this->availability_completed_by($info, $starttime, $endtime);
815
            if ($activityavailability === false) {
816
                return false;
817
            } else if ($activityavailability === true) {
818
                // This activity belongs to this time range.
819
                return true;
820
            }
821
        }
822
 
823
        // We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
824
        $section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
825
        if ($section->availability) {
826
            $info = new \core_availability\info_section($section);
827
            $sectionavailability = $this->availability_completed_by($info, $starttime, $endtime);
828
            if ($sectionavailability === false) {
829
                return false;
830
            } else if ($sectionavailability === true) {
831
                // This activity belongs to this section time range.
832
                return true;
833
            }
834
        }
835
 
836
        // When the course is using format weeks we use the week's end date.
837
        $format = course_get_format($activity->get_modinfo()->get_course());
838
        // We should change this in MDL-60702.
839
        if (get_class($format) == 'format_weeks' || is_subclass_of($format, 'format_weeks')
840
             && method_exists($format, 'get_section_dates')) {
841
            $dates = $format->get_section_dates($section);
842
 
843
            // We need to consider the +2 hours added by get_section_dates.
844
            // Avoid $starttime <= $dates->end because $starttime may be the start of the next week.
845
            if ($starttime < ($dates->end - 7200) && $endtime >= ($dates->end - 7200)) {
846
                return true;
847
            } else {
848
                return false;
849
            }
850
        }
851
 
852
        if ($activity->sectionnum == 0) {
853
            return false;
854
        }
855
 
856
        if (!$this->course->get_end() || !$this->course->get_start()) {
857
            debugging('Activities which due date is in a time range can not be calculated ' .
858
                'if the course doesn\'t have start and end date', DEBUG_DEVELOPER);
859
            return false;
860
        }
861
 
862
        if (!course_format_uses_sections($this->course->get_course_data()->format)) {
863
            // If it does not use sections and there are no availability conditions to access it it is available
864
            // and we can not magically classify it into any other time range than this one.
865
            return true;
866
        }
867
 
868
        // Split the course duration in the number of sections and consider the end of each section the due
869
        // date of all activities contained in that section.
870
        $formatoptions = $format->get_format_options();
871
        if (!empty($formatoptions['numsections'])) {
872
            $nsections = $formatoptions['numsections'];
873
        } else {
874
            // There are course format that use sections but without numsections, we fallback to the number
875
            // of cached sections in get_section_info_all, not that accurate though.
876
            $coursesections = $activity->get_modinfo()->get_section_info_all();
877
            $nsections = count($coursesections);
878
            if (isset($coursesections[0])) {
879
                // We don't count section 0 if it exists.
880
                $nsections--;
881
            }
882
        }
883
 
884
        $courseduration = $this->course->get_end() - $this->course->get_start();
885
        $sectionduration = round($courseduration / $nsections);
886
        $activitysectionenddate = $this->course->get_start() + ($sectionduration * $activity->sectionnum);
887
        if ($activitysectionenddate > $starttime && $activitysectionenddate <= $endtime) {
888
            return true;
889
        }
890
 
891
        return false;
892
    }
893
 
894
    /**
895
     * True if the activity is due or it has been closed during this period, false if during another period, null if no due time.
896
     *
897
     * It can be overwritten by activities that allow teachers to set a due date or a time close separately
898
     * from Moodle availability system. Note that in most of the cases overwriting get_timeclose_field should
899
     * be enough.
900
     *
901
     * Returns true or false if the time close date falls into the provided time range. Null otherwise.
902
     *
903
     * @param \cm_info $activity
904
     * @param int $starttime
905
     * @param int $endtime
906
     * @param \stdClass|false $student
907
     * @return null
908
     */
909
    protected function activity_type_completed_by(\cm_info $activity, $starttime, $endtime, $student = false) {
910
 
911
        $fieldname = $this->get_timeclose_field();
912
        if (!$fieldname) {
913
            // This activity type do not have its own availability control.
914
            return null;
915
        }
916
 
917
        $this->fill_instance_data($activity);
918
        $instance = $this->instancedata[$activity->instance];
919
 
920
        if (!$instance->{$fieldname}) {
921
            return null;
922
        }
923
 
924
        if ($starttime < $instance->{$fieldname} && $endtime >= $instance->{$fieldname}) {
925
            return true;
926
        }
927
 
928
        return false;
929
    }
930
 
931
    /**
932
     * Returns the name of the field that controls activity availability.
933
     *
934
     * Should be overwritten by activities that allow teachers to set a due date or a time close separately
935
     * from Moodle availability system.
936
     *
937
     * Just 1 field will not be enough for all cases, but for the most simple ones without
938
     * overrides and stuff like that.
939
     *
940
     * @return null|string
941
     */
942
    protected function get_timeclose_field() {
943
        return null;
944
    }
945
 
946
    /**
947
     * Check if the activity/section should have been completed during the provided period according to its availability rules.
948
     *
949
     * @param \core_availability\info $info
950
     * @param int $starttime
951
     * @param int $endtime
952
     * @return bool|null
953
     */
954
    protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
955
 
956
        $dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
957
        foreach ($dateconditions as $condition) {
958
            // Availability API does not allow us to check from / to dates nicely, we need to be naughty.
959
            $conditiondata = $condition->save();
960
 
961
            if ($conditiondata->d === \availability_date\condition::DIRECTION_FROM &&
962
                    $conditiondata->t > $endtime) {
963
                // Skip this activity if any 'from' date is later than the end time.
964
                return false;
965
 
966
            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
967
                    ($conditiondata->t < $starttime || $conditiondata->t > $endtime)) {
968
                // Skip activity if any 'until' date is not in $starttime - $endtime range.
969
                return false;
970
            } else if ($conditiondata->d === \availability_date\condition::DIRECTION_UNTIL &&
971
                    $conditiondata->t < $endtime && $conditiondata->t > $starttime) {
972
                return true;
973
            }
974
        }
975
 
976
        // This can be interpreted as 'the activity was available but we don't know if its expected completion date
977
        // was during this period.
978
        return null;
979
    }
980
 
981
    /**
982
     * Fills in activity instance data.
983
     *
984
     * @param \cm_info $cm
985
     * @return void
986
     */
987
    protected function fill_instance_data(\cm_info $cm) {
988
        global $DB;
989
 
990
        if (!isset($this->instancedata[$cm->instance])) {
991
            $this->instancedata[$cm->instance] = $DB->get_record($this->get_activity_type(), array('id' => $cm->instance),
992
                '*', MUST_EXIST);
993
        }
994
    }
995
 
996
    /**
997
     * Defines indicator type.
998
     *
999
     * @return string
1000
     */
1001
    abstract public function get_indicator_type();
1002
}