Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   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  }