See Release Notes
Long Term Support Release
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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body