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
25namespace core_analytics\local\indicator;
26
27defined('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 */
36abstract 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    public final 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    protected final 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    protected final 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}
1003