Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
/mod/lesson/ -> lib.php (source)

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Standard library of functions and constants for lesson
  20   *
  21   * @package mod_lesson
  22   * @copyright  1999 onwards Martin Dougiamas  {@link http://moodle.com}
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   **/
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  // Event types.
  29  define('LESSON_EVENT_TYPE_OPEN', 'open');
  30  define('LESSON_EVENT_TYPE_CLOSE', 'close');
  31  
  32  require_once (__DIR__ . '/deprecatedlib.php');
  33  /* Do not include any libraries here! */
  34  
  35  /**
  36   * Given an object containing all the necessary data,
  37   * (defined by the form in mod_form.php) this function
  38   * will create a new instance and return the id number
  39   * of the new instance.
  40   *
  41   * @global object
  42   * @global object
  43   * @param object $lesson Lesson post data from the form
  44   * @return int
  45   **/
  46  function lesson_add_instance($data, $mform) {
  47      global $DB;
  48  
  49      $cmid = $data->coursemodule;
  50      $draftitemid = $data->mediafile;
  51      $context = context_module::instance($cmid);
  52  
  53      lesson_process_pre_save($data);
  54  
  55      unset($data->mediafile);
  56      $lessonid = $DB->insert_record("lesson", $data);
  57      $data->id = $lessonid;
  58  
  59      lesson_update_media_file($lessonid, $context, $draftitemid);
  60  
  61      lesson_process_post_save($data);
  62  
  63      lesson_grade_item_update($data);
  64  
  65      return $lessonid;
  66  }
  67  
  68  /**
  69   * Given an object containing all the necessary data,
  70   * (defined by the form in mod_form.php) this function
  71   * will update an existing instance with new data.
  72   *
  73   * @global object
  74   * @param object $lesson Lesson post data from the form
  75   * @return boolean
  76   **/
  77  function lesson_update_instance($data, $mform) {
  78      global $DB;
  79  
  80      $data->id = $data->instance;
  81      $cmid = $data->coursemodule;
  82      $draftitemid = $data->mediafile;
  83      $context = context_module::instance($cmid);
  84  
  85      lesson_process_pre_save($data);
  86  
  87      unset($data->mediafile);
  88      $DB->update_record("lesson", $data);
  89  
  90      lesson_update_media_file($data->id, $context, $draftitemid);
  91  
  92      lesson_process_post_save($data);
  93  
  94      // update grade item definition
  95      lesson_grade_item_update($data);
  96  
  97      // update grades - TODO: do it only when grading style changes
  98      lesson_update_grades($data, 0, false);
  99  
 100      return true;
 101  }
 102  
 103  /**
 104   * This function updates the events associated to the lesson.
 105   * If $override is non-zero, then it updates only the events
 106   * associated with the specified override.
 107   *
 108   * @uses LESSON_MAX_EVENT_LENGTH
 109   * @param object $lesson the lesson object.
 110   * @param object $override (optional) limit to a specific override
 111   */
 112  function lesson_update_events($lesson, $override = null) {
 113      global $CFG, $DB;
 114  
 115      require_once($CFG->dirroot . '/mod/lesson/locallib.php');
 116      require_once($CFG->dirroot . '/calendar/lib.php');
 117  
 118      // Load the old events relating to this lesson.
 119      $conds = array('modulename' => 'lesson',
 120                     'instance' => $lesson->id);
 121      if (!empty($override)) {
 122          // Only load events for this override.
 123          if (isset($override->userid)) {
 124              $conds['userid'] = $override->userid;
 125          } else {
 126              $conds['groupid'] = $override->groupid;
 127          }
 128      }
 129      $oldevents = $DB->get_records('event', $conds, 'id ASC');
 130  
 131      // Now make a to-do list of all that needs to be updated.
 132      if (empty($override)) {
 133          // We are updating the primary settings for the lesson, so we need to add all the overrides.
 134          $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $lesson->id), 'id ASC');
 135          // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents
 136          // list contains the original (non-override) event for the module. If this is not included
 137          // the logic below will end up updating the wrong row when we try to reconcile this $overrides
 138          // list against the $oldevents list.
 139          array_unshift($overrides, new stdClass());
 140      } else {
 141          // Just do the one override.
 142          $overrides = array($override);
 143      }
 144  
 145      // Get group override priorities.
 146      $grouppriorities = lesson_get_group_override_priorities($lesson->id);
 147  
 148      foreach ($overrides as $current) {
 149          $groupid   = isset($current->groupid) ? $current->groupid : 0;
 150          $userid    = isset($current->userid) ? $current->userid : 0;
 151          $available  = isset($current->available) ? $current->available : $lesson->available;
 152          $deadline = isset($current->deadline) ? $current->deadline : $lesson->deadline;
 153  
 154          // Only add open/close events for an override if they differ from the lesson default.
 155          $addopen  = empty($current->id) || !empty($current->available);
 156          $addclose = empty($current->id) || !empty($current->deadline);
 157  
 158          if (!empty($lesson->coursemodule)) {
 159              $cmid = $lesson->coursemodule;
 160          } else {
 161              $cmid = get_coursemodule_from_instance('lesson', $lesson->id, $lesson->course)->id;
 162          }
 163  
 164          $event = new stdClass();
 165          $event->type = !$deadline ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD;
 166          $event->description = format_module_intro('lesson', $lesson, $cmid, false);
 167          $event->format = FORMAT_HTML;
 168          // Events module won't show user events when the courseid is nonzero.
 169          $event->courseid    = ($userid) ? 0 : $lesson->course;
 170          $event->groupid     = $groupid;
 171          $event->userid      = $userid;
 172          $event->modulename  = 'lesson';
 173          $event->instance    = $lesson->id;
 174          $event->timestart   = $available;
 175          $event->timeduration = max($deadline - $available, 0);
 176          $event->timesort    = $available;
 177          $event->visible     = instance_is_visible('lesson', $lesson);
 178          $event->eventtype   = LESSON_EVENT_TYPE_OPEN;
 179          $event->priority    = null;
 180  
 181          // Determine the event name and priority.
 182          if ($groupid) {
 183              // Group override event.
 184              $params = new stdClass();
 185              $params->lesson = $lesson->name;
 186              $params->group = groups_get_group_name($groupid);
 187              if ($params->group === false) {
 188                  // Group doesn't exist, just skip it.
 189                  continue;
 190              }
 191              $eventname = get_string('overridegroupeventname', 'lesson', $params);
 192              // Set group override priority.
 193              if ($grouppriorities !== null) {
 194                  $openpriorities = $grouppriorities['open'];
 195                  if (isset($openpriorities[$available])) {
 196                      $event->priority = $openpriorities[$available];
 197                  }
 198              }
 199          } else if ($userid) {
 200              // User override event.
 201              $params = new stdClass();
 202              $params->lesson = $lesson->name;
 203              $eventname = get_string('overrideusereventname', 'lesson', $params);
 204              // Set user override priority.
 205              $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY;
 206          } else {
 207              // The parent event.
 208              $eventname = $lesson->name;
 209          }
 210  
 211          if ($addopen or $addclose) {
 212              // Separate start and end events.
 213              $event->timeduration  = 0;
 214              if ($available && $addopen) {
 215                  if ($oldevent = array_shift($oldevents)) {
 216                      $event->id = $oldevent->id;
 217                  } else {
 218                      unset($event->id);
 219                  }
 220                  $event->name = get_string('lessoneventopens', 'lesson', $eventname);
 221                  // The method calendar_event::create will reuse a db record if the id field is set.
 222                  calendar_event::create($event, false);
 223              }
 224              if ($deadline && $addclose) {
 225                  if ($oldevent = array_shift($oldevents)) {
 226                      $event->id = $oldevent->id;
 227                  } else {
 228                      unset($event->id);
 229                  }
 230                  $event->type      = CALENDAR_EVENT_TYPE_ACTION;
 231                  $event->name      = get_string('lessoneventcloses', 'lesson', $eventname);
 232                  $event->timestart = $deadline;
 233                  $event->timesort  = $deadline;
 234                  $event->eventtype = LESSON_EVENT_TYPE_CLOSE;
 235                  if ($groupid && $grouppriorities !== null) {
 236                      $closepriorities = $grouppriorities['close'];
 237                      if (isset($closepriorities[$deadline])) {
 238                          $event->priority = $closepriorities[$deadline];
 239                      }
 240                  }
 241                  calendar_event::create($event, false);
 242              }
 243          }
 244      }
 245  
 246      // Delete any leftover events.
 247      foreach ($oldevents as $badevent) {
 248          $badevent = calendar_event::load($badevent);
 249          $badevent->delete();
 250      }
 251  }
 252  
 253  /**
 254   * Calculates the priorities of timeopen and timeclose values for group overrides for a lesson.
 255   *
 256   * @param int $lessonid The lesson ID.
 257   * @return array|null Array of group override priorities for open and close times. Null if there are no group overrides.
 258   */
 259  function lesson_get_group_override_priorities($lessonid) {
 260      global $DB;
 261  
 262      // Fetch group overrides.
 263      $where = 'lessonid = :lessonid AND groupid IS NOT NULL';
 264      $params = ['lessonid' => $lessonid];
 265      $overrides = $DB->get_records_select('lesson_overrides', $where, $params, '', 'id, groupid, available, deadline');
 266      if (!$overrides) {
 267          return null;
 268      }
 269  
 270      $grouptimeopen = [];
 271      $grouptimeclose = [];
 272      foreach ($overrides as $override) {
 273          if ($override->available !== null && !in_array($override->available, $grouptimeopen)) {
 274              $grouptimeopen[] = $override->available;
 275          }
 276          if ($override->deadline !== null && !in_array($override->deadline, $grouptimeclose)) {
 277              $grouptimeclose[] = $override->deadline;
 278          }
 279      }
 280  
 281      // Sort open times in ascending manner. The earlier open time gets higher priority.
 282      sort($grouptimeopen);
 283      // Set priorities.
 284      $opengrouppriorities = [];
 285      $openpriority = 1;
 286      foreach ($grouptimeopen as $timeopen) {
 287          $opengrouppriorities[$timeopen] = $openpriority++;
 288      }
 289  
 290      // Sort close times in descending manner. The later close time gets higher priority.
 291      rsort($grouptimeclose);
 292      // Set priorities.
 293      $closegrouppriorities = [];
 294      $closepriority = 1;
 295      foreach ($grouptimeclose as $timeclose) {
 296          $closegrouppriorities[$timeclose] = $closepriority++;
 297      }
 298  
 299      return [
 300          'open' => $opengrouppriorities,
 301          'close' => $closegrouppriorities
 302      ];
 303  }
 304  
 305  /**
 306   * This standard function will check all instances of this module
 307   * and make sure there are up-to-date events created for each of them.
 308   * If courseid = 0, then every lesson event in the site is checked, else
 309   * only lesson events belonging to the course specified are checked.
 310   * This function is used, in its new format, by restore_refresh_events()
 311   *
 312   * @param int $courseid
 313   * @param int|stdClass $instance Lesson module instance or ID.
 314   * @param int|stdClass $cm Course module object or ID (not used in this module).
 315   * @return bool
 316   */
 317  function lesson_refresh_events($courseid = 0, $instance = null, $cm = null) {
 318      global $DB;
 319  
 320      // If we have instance information then we can just update the one event instead of updating all events.
 321      if (isset($instance)) {
 322          if (!is_object($instance)) {
 323              $instance = $DB->get_record('lesson', array('id' => $instance), '*', MUST_EXIST);
 324          }
 325          lesson_update_events($instance);
 326          return true;
 327      }
 328  
 329      if ($courseid == 0) {
 330          if (!$lessons = $DB->get_records('lesson')) {
 331              return true;
 332          }
 333      } else {
 334          if (!$lessons = $DB->get_records('lesson', array('course' => $courseid))) {
 335              return true;
 336          }
 337      }
 338  
 339      foreach ($lessons as $lesson) {
 340          lesson_update_events($lesson);
 341      }
 342  
 343      return true;
 344  }
 345  
 346  /**
 347   * Given an ID of an instance of this module,
 348   * this function will permanently delete the instance
 349   * and any data that depends on it.
 350   *
 351   * @global object
 352   * @param int $id
 353   * @return bool
 354   */
 355  function lesson_delete_instance($id) {
 356      global $DB, $CFG;
 357      require_once($CFG->dirroot . '/mod/lesson/locallib.php');
 358  
 359      $lesson = $DB->get_record("lesson", array("id"=>$id), '*', MUST_EXIST);
 360      $lesson = new lesson($lesson);
 361      return $lesson->delete();
 362  }
 363  
 364  /**
 365   * Return a small object with summary information about what a
 366   * user has done with a given particular instance of this module
 367   * Used for user activity reports.
 368   * $return->time = the time they did it
 369   * $return->info = a short text description
 370   *
 371   * @global object
 372   * @param object $course
 373   * @param object $user
 374   * @param object $mod
 375   * @param object $lesson
 376   * @return object
 377   */
 378  function lesson_user_outline($course, $user, $mod, $lesson) {
 379      global $CFG, $DB;
 380  
 381      require_once("$CFG->libdir/gradelib.php");
 382      $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
 383      $return = new stdClass();
 384  
 385      if (empty($grades->items[0]->grades)) {
 386          $return->info = get_string("nolessonattempts", "lesson");
 387      } else {
 388          $grade = reset($grades->items[0]->grades);
 389          if (empty($grade->grade)) {
 390  
 391              // Check to see if it an ungraded / incomplete attempt.
 392              $sql = "SELECT *
 393                        FROM {lesson_timer}
 394                       WHERE lessonid = :lessonid
 395                         AND userid = :userid
 396                    ORDER BY starttime DESC";
 397              $params = array('lessonid' => $lesson->id, 'userid' => $user->id);
 398  
 399              if ($attempts = $DB->get_records_sql($sql, $params, 0, 1)) {
 400                  $attempt = reset($attempts);
 401                  if ($attempt->completed) {
 402                      $return->info = get_string("completed", "lesson");
 403                  } else {
 404                      $return->info = get_string("notyetcompleted", "lesson");
 405                  }
 406                  $return->time = $attempt->lessontime;
 407              } else {
 408                  $return->info = get_string("nolessonattempts", "lesson");
 409              }
 410          } else {
 411              if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
 412                  $return->info = get_string('gradenoun') . ': ' . $grade->str_long_grade;
 413              } else {
 414                  $return->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades');
 415              }
 416  
 417              $return->time = grade_get_date_for_user_grade($grade, $user);
 418          }
 419      }
 420      return $return;
 421  }
 422  
 423  /**
 424   * Print a detailed representation of what a  user has done with
 425   * a given particular instance of this module, for user activity reports.
 426   *
 427   * @global object
 428   * @param object $course
 429   * @param object $user
 430   * @param object $mod
 431   * @param object $lesson
 432   * @return bool
 433   */
 434  function lesson_user_complete($course, $user, $mod, $lesson) {
 435      global $DB, $OUTPUT, $CFG;
 436  
 437      require_once("$CFG->libdir/gradelib.php");
 438  
 439      $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
 440  
 441      // Display the grade and feedback.
 442      if (empty($grades->items[0]->grades)) {
 443          echo $OUTPUT->container(get_string("nolessonattempts", "lesson"));
 444      } else {
 445          $grade = reset($grades->items[0]->grades);
 446          if (empty($grade->grade)) {
 447              // Check to see if it an ungraded / incomplete attempt.
 448              $sql = "SELECT *
 449                        FROM {lesson_timer}
 450                       WHERE lessonid = :lessonid
 451                         AND userid = :userid
 452                       ORDER by starttime desc";
 453              $params = array('lessonid' => $lesson->id, 'userid' => $user->id);
 454  
 455              if ($attempt = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE)) {
 456                  if ($attempt->completed) {
 457                      $status = get_string("completed", "lesson");
 458                  } else {
 459                      $status = get_string("notyetcompleted", "lesson");
 460                  }
 461              } else {
 462                  $status = get_string("nolessonattempts", "lesson");
 463              }
 464          } else {
 465              if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
 466                  $status = get_string('gradenoun') . ': ' . $grade->str_long_grade;
 467              } else {
 468                  $status = get_string('gradenoun') . ': ' . get_string('hidden', 'grades');
 469              }
 470          }
 471  
 472          // Display the grade or lesson status if there isn't one.
 473          echo $OUTPUT->container($status);
 474  
 475          if ($grade->str_feedback &&
 476              (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id)))) {
 477              echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
 478          }
 479      }
 480  
 481      // Display the lesson progress.
 482      // Attempt, pages viewed, questions answered, correct answers, time.
 483      $params = array ("lessonid" => $lesson->id, "userid" => $user->id);
 484      $attempts = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND userid = :userid", $params, "retry, timeseen");
 485      $branches = $DB->get_records_select("lesson_branch", "lessonid = :lessonid AND userid = :userid", $params, "retry, timeseen");
 486      if (!empty($attempts) or !empty($branches)) {
 487          echo $OUTPUT->box_start();
 488          $table = new html_table();
 489          // Table Headings.
 490          $table->head = array (get_string("attemptheader", "lesson"),
 491              get_string("totalpagesviewedheader", "lesson"),
 492              get_string("numberofpagesviewedheader", "lesson"),
 493              get_string("numberofcorrectanswersheader", "lesson"),
 494              get_string("time"));
 495          $table->width = "100%";
 496          $table->align = array ("center", "center", "center", "center", "center");
 497          $table->size = array ("*", "*", "*", "*", "*");
 498          $table->cellpadding = 2;
 499          $table->cellspacing = 0;
 500  
 501          $retry = 0;
 502          $nquestions = 0;
 503          $npages = 0;
 504          $ncorrect = 0;
 505  
 506          // Filter question pages (from lesson_attempts).
 507          foreach ($attempts as $attempt) {
 508              if ($attempt->retry == $retry) {
 509                  $npages++;
 510                  $nquestions++;
 511                  if ($attempt->correct) {
 512                      $ncorrect++;
 513                  }
 514                  $timeseen = $attempt->timeseen;
 515              } else {
 516                  $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen));
 517                  $retry++;
 518                  $nquestions = 1;
 519                  $npages = 1;
 520                  if ($attempt->correct) {
 521                      $ncorrect = 1;
 522                  } else {
 523                      $ncorrect = 0;
 524                  }
 525              }
 526          }
 527  
 528          // Filter content pages (from lesson_branch).
 529          foreach ($branches as $branch) {
 530              if ($branch->retry == $retry) {
 531                  $npages++;
 532  
 533                  $timeseen = $branch->timeseen;
 534              } else {
 535                  $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen));
 536                  $retry++;
 537                  $npages = 1;
 538              }
 539          }
 540          if ($npages > 0) {
 541                  $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen));
 542          }
 543          echo html_writer::table($table);
 544          echo $OUTPUT->box_end();
 545      }
 546  
 547      return true;
 548  }
 549  
 550  /**
 551   * @deprecated since Moodle 3.3, when the block_course_overview block was removed.
 552   */
 553  function lesson_print_overview() {
 554      throw new coding_exception('lesson_print_overview() can not be used any more and is obsolete.');
 555  }
 556  
 557  /**
 558   * Function to be run periodically according to the moodle cron
 559   * This function searches for things that need to be done, such
 560   * as sending out mail, toggling flags etc ...
 561   * @global stdClass
 562   * @return bool true
 563   */
 564  function lesson_cron () {
 565      global $CFG;
 566  
 567      return true;
 568  }
 569  
 570  /**
 571   * Return grade for given user or all users.
 572   *
 573   * @global stdClass
 574   * @global object
 575   * @param int $lessonid id of lesson
 576   * @param int $userid optional user id, 0 means all users
 577   * @return array array of grades, false if none
 578   */
 579  function lesson_get_user_grades($lesson, $userid=0) {
 580      global $CFG, $DB;
 581  
 582      $params = array("lessonid" => $lesson->id,"lessonid2" => $lesson->id);
 583  
 584      if (!empty($userid)) {
 585          $params["userid"] = $userid;
 586          $params["userid2"] = $userid;
 587          $user = "AND u.id = :userid";
 588          $fuser = "AND uu.id = :userid2";
 589      }
 590      else {
 591          $user="";
 592          $fuser="";
 593      }
 594  
 595      if ($lesson->retake) {
 596          if ($lesson->usemaxgrade) {
 597              $sql = "SELECT u.id, u.id AS userid, MAX(g.grade) AS rawgrade
 598                        FROM {user} u, {lesson_grades} g
 599                       WHERE u.id = g.userid AND g.lessonid = :lessonid
 600                             $user
 601                    GROUP BY u.id";
 602          } else {
 603              $sql = "SELECT u.id, u.id AS userid, AVG(g.grade) AS rawgrade
 604                        FROM {user} u, {lesson_grades} g
 605                       WHERE u.id = g.userid AND g.lessonid = :lessonid
 606                             $user
 607                    GROUP BY u.id";
 608          }
 609          unset($params['lessonid2']);
 610          unset($params['userid2']);
 611      } else {
 612          // use only first attempts (with lowest id in lesson_grades table)
 613          $firstonly = "SELECT uu.id AS userid, MIN(gg.id) AS firstcompleted
 614                          FROM {user} uu, {lesson_grades} gg
 615                         WHERE uu.id = gg.userid AND gg.lessonid = :lessonid2
 616                               $fuser
 617                         GROUP BY uu.id";
 618  
 619          $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade
 620                    FROM {user} u, {lesson_grades} g, ($firstonly) f
 621                   WHERE u.id = g.userid AND g.lessonid = :lessonid
 622                         AND g.id = f.firstcompleted AND g.userid=f.userid
 623                         $user";
 624      }
 625  
 626      return $DB->get_records_sql($sql, $params);
 627  }
 628  
 629  /**
 630   * Update grades in central gradebook
 631   *
 632   * @category grade
 633   * @param object $lesson
 634   * @param int $userid specific user only, 0 means all
 635   * @param bool $nullifnone
 636   */
 637  function lesson_update_grades($lesson, $userid=0, $nullifnone=true) {
 638      global $CFG, $DB;
 639      require_once($CFG->libdir.'/gradelib.php');
 640  
 641      if ($lesson->grade == 0 || $lesson->practice) {
 642          lesson_grade_item_update($lesson);
 643  
 644      } else if ($grades = lesson_get_user_grades($lesson, $userid)) {
 645          lesson_grade_item_update($lesson, $grades);
 646  
 647      } else if ($userid and $nullifnone) {
 648          $grade = new stdClass();
 649          $grade->userid   = $userid;
 650          $grade->rawgrade = null;
 651          lesson_grade_item_update($lesson, $grade);
 652  
 653      } else {
 654          lesson_grade_item_update($lesson);
 655      }
 656  }
 657  
 658  /**
 659   * Create grade item for given lesson
 660   *
 661   * @category grade
 662   * @uses GRADE_TYPE_VALUE
 663   * @uses GRADE_TYPE_NONE
 664   * @param object $lesson object with extra cmidnumber
 665   * @param array|object $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
 666   * @return int 0 if ok, error code otherwise
 667   */
 668  function lesson_grade_item_update($lesson, $grades=null) {
 669      global $CFG;
 670      if (!function_exists('grade_update')) { //workaround for buggy PHP versions
 671          require_once($CFG->libdir.'/gradelib.php');
 672      }
 673  
 674      if (property_exists($lesson, 'cmidnumber')) { //it may not be always present
 675          $params = array('itemname'=>$lesson->name, 'idnumber'=>$lesson->cmidnumber);
 676      } else {
 677          $params = array('itemname'=>$lesson->name);
 678      }
 679  
 680      if (!$lesson->practice and $lesson->grade > 0) {
 681          $params['gradetype']  = GRADE_TYPE_VALUE;
 682          $params['grademax']   = $lesson->grade;
 683          $params['grademin']   = 0;
 684      } else if (!$lesson->practice and $lesson->grade < 0) {
 685          $params['gradetype']  = GRADE_TYPE_SCALE;
 686          $params['scaleid']   = -$lesson->grade;
 687  
 688          // Make sure current grade fetched correctly from $grades
 689          $currentgrade = null;
 690          if (!empty($grades)) {
 691              if (is_array($grades)) {
 692                  $currentgrade = reset($grades);
 693              } else {
 694                  $currentgrade = $grades;
 695              }
 696          }
 697  
 698          // When converting a score to a scale, use scale's grade maximum to calculate it.
 699          if (!empty($currentgrade) && $currentgrade->rawgrade !== null) {
 700              $grade = grade_get_grades($lesson->course, 'mod', 'lesson', $lesson->id, $currentgrade->userid);
 701              $params['grademax']   = reset($grade->items)->grademax;
 702          }
 703      } else {
 704          $params['gradetype']  = GRADE_TYPE_NONE;
 705      }
 706  
 707      if ($grades  === 'reset') {
 708          $params['reset'] = true;
 709          $grades = null;
 710      } else if (!empty($grades)) {
 711          // Need to calculate raw grade (Note: $grades has many forms)
 712          if (is_object($grades)) {
 713              $grades = array($grades->userid => $grades);
 714          } else if (array_key_exists('userid', $grades)) {
 715              $grades = array($grades['userid'] => $grades);
 716          }
 717          foreach ($grades as $key => $grade) {
 718              if (!is_array($grade)) {
 719                  $grades[$key] = $grade = (array) $grade;
 720              }
 721              //check raw grade isnt null otherwise we erroneously insert a grade of 0
 722              if ($grade['rawgrade'] !== null) {
 723                  $grades[$key]['rawgrade'] = ($grade['rawgrade'] * $params['grademax'] / 100);
 724              } else {
 725                  //setting rawgrade to null just in case user is deleting a grade
 726                  $grades[$key]['rawgrade'] = null;
 727              }
 728          }
 729      }
 730  
 731      return grade_update('mod/lesson', $lesson->course, 'mod', 'lesson', $lesson->id, 0, $grades, $params);
 732  }
 733  
 734  /**
 735   * List the actions that correspond to a view of this module.
 736   * This is used by the participation report.
 737   *
 738   * Note: This is not used by new logging system. Event with
 739   *       crud = 'r' and edulevel = LEVEL_PARTICIPATING will
 740   *       be considered as view action.
 741   *
 742   * @return array
 743   */
 744  function lesson_get_view_actions() {
 745      return array('view','view all');
 746  }
 747  
 748  /**
 749   * List the actions that correspond to a post of this module.
 750   * This is used by the participation report.
 751   *
 752   * Note: This is not used by new logging system. Event with
 753   *       crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
 754   *       will be considered as post action.
 755   *
 756   * @return array
 757   */
 758  function lesson_get_post_actions() {
 759      return array('end','start');
 760  }
 761  
 762  /**
 763   * Runs any processes that must run before
 764   * a lesson insert/update
 765   *
 766   * @global object
 767   * @param object $lesson Lesson form data
 768   * @return void
 769   **/
 770  function lesson_process_pre_save(&$lesson) {
 771      global $DB;
 772  
 773      $lesson->timemodified = time();
 774  
 775      if (empty($lesson->timelimit)) {
 776          $lesson->timelimit = 0;
 777      }
 778      if (empty($lesson->timespent) or !is_numeric($lesson->timespent) or $lesson->timespent < 0) {
 779          $lesson->timespent = 0;
 780      }
 781      if (!isset($lesson->completed)) {
 782          $lesson->completed = 0;
 783      }
 784      if (empty($lesson->gradebetterthan) or !is_numeric($lesson->gradebetterthan) or $lesson->gradebetterthan < 0) {
 785          $lesson->gradebetterthan = 0;
 786      } else if ($lesson->gradebetterthan > 100) {
 787          $lesson->gradebetterthan = 100;
 788      }
 789  
 790      if (empty($lesson->width)) {
 791          $lesson->width = 640;
 792      }
 793      if (empty($lesson->height)) {
 794          $lesson->height = 480;
 795      }
 796      if (empty($lesson->bgcolor)) {
 797          $lesson->bgcolor = '#FFFFFF';
 798      }
 799  
 800      // Conditions for dependency
 801      $conditions = new stdClass;
 802      $conditions->timespent = $lesson->timespent;
 803      $conditions->completed = $lesson->completed;
 804      $conditions->gradebetterthan = $lesson->gradebetterthan;
 805      $lesson->conditions = serialize($conditions);
 806      unset($lesson->timespent);
 807      unset($lesson->completed);
 808      unset($lesson->gradebetterthan);
 809  
 810      if (empty($lesson->password)) {
 811          unset($lesson->password);
 812      }
 813  }
 814  
 815  /**
 816   * Runs any processes that must be run
 817   * after a lesson insert/update
 818   *
 819   * @global object
 820   * @param object $lesson Lesson form data
 821   * @return void
 822   **/
 823  function lesson_process_post_save(&$lesson) {
 824      // Update the events relating to this lesson.
 825      lesson_update_events($lesson);
 826      $completionexpected = (!empty($lesson->completionexpected)) ? $lesson->completionexpected : null;
 827      \core_completion\api::update_completion_date_event($lesson->coursemodule, 'lesson', $lesson, $completionexpected);
 828  }
 829  
 830  
 831  /**
 832   * Implementation of the function for printing the form elements that control
 833   * whether the course reset functionality affects the lesson.
 834   *
 835   * @param $mform form passed by reference
 836   */
 837  function lesson_reset_course_form_definition(&$mform) {
 838      $mform->addElement('header', 'lessonheader', get_string('modulenameplural', 'lesson'));
 839      $mform->addElement('advcheckbox', 'reset_lesson', get_string('deleteallattempts','lesson'));
 840      $mform->addElement('advcheckbox', 'reset_lesson_user_overrides',
 841              get_string('removealluseroverrides', 'lesson'));
 842      $mform->addElement('advcheckbox', 'reset_lesson_group_overrides',
 843              get_string('removeallgroupoverrides', 'lesson'));
 844  }
 845  
 846  /**
 847   * Course reset form defaults.
 848   * @param object $course
 849   * @return array
 850   */
 851  function lesson_reset_course_form_defaults($course) {
 852      return array('reset_lesson' => 1,
 853              'reset_lesson_group_overrides' => 1,
 854              'reset_lesson_user_overrides' => 1);
 855  }
 856  
 857  /**
 858   * Removes all grades from gradebook
 859   *
 860   * @global stdClass
 861   * @global object
 862   * @param int $courseid
 863   * @param string optional type
 864   */
 865  function lesson_reset_gradebook($courseid, $type='') {
 866      global $CFG, $DB;
 867  
 868      $sql = "SELECT l.*, cm.idnumber as cmidnumber, l.course as courseid
 869                FROM {lesson} l, {course_modules} cm, {modules} m
 870               WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id AND l.course=:course";
 871      $params = array ("course" => $courseid);
 872      if ($lessons = $DB->get_records_sql($sql,$params)) {
 873          foreach ($lessons as $lesson) {
 874              lesson_grade_item_update($lesson, 'reset');
 875          }
 876      }
 877  }
 878  
 879  /**
 880   * Actual implementation of the reset course functionality, delete all the
 881   * lesson attempts for course $data->courseid.
 882   *
 883   * @global stdClass
 884   * @global object
 885   * @param object $data the data submitted from the reset course.
 886   * @return array status array
 887   */
 888  function lesson_reset_userdata($data) {
 889      global $CFG, $DB;
 890  
 891      $componentstr = get_string('modulenameplural', 'lesson');
 892      $status = array();
 893  
 894      if (!empty($data->reset_lesson)) {
 895          $lessonssql = "SELECT l.id
 896                           FROM {lesson} l
 897                          WHERE l.course=:course";
 898  
 899          $params = array ("course" => $data->courseid);
 900          $lessons = $DB->get_records_sql($lessonssql, $params);
 901  
 902          // Get rid of attempts files.
 903          $fs = get_file_storage();
 904          if ($lessons) {
 905              foreach ($lessons as $lessonid => $unused) {
 906                  if (!$cm = get_coursemodule_from_instance('lesson', $lessonid)) {
 907                      continue;
 908                  }
 909                  $context = context_module::instance($cm->id);
 910                  $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses');
 911                  $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers');
 912              }
 913          }
 914  
 915          $DB->delete_records_select('lesson_timer', "lessonid IN ($lessonssql)", $params);
 916          $DB->delete_records_select('lesson_grades', "lessonid IN ($lessonssql)", $params);
 917          $DB->delete_records_select('lesson_attempts', "lessonid IN ($lessonssql)", $params);
 918          $DB->delete_records_select('lesson_branch', "lessonid IN ($lessonssql)", $params);
 919  
 920          // remove all grades from gradebook
 921          if (empty($data->reset_gradebook_grades)) {
 922              lesson_reset_gradebook($data->courseid);
 923          }
 924  
 925          $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false);
 926      }
 927  
 928      $purgeoverrides = false;
 929  
 930      // Remove user overrides.
 931      if (!empty($data->reset_lesson_user_overrides)) {
 932          $DB->delete_records_select('lesson_overrides',
 933                  'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid));
 934          $status[] = array(
 935              'component' => $componentstr,
 936              'item' => get_string('useroverridesdeleted', 'lesson'),
 937              'error' => false);
 938          $purgeoverrides = true;
 939      }
 940      // Remove group overrides.
 941      if (!empty($data->reset_lesson_group_overrides)) {
 942          $DB->delete_records_select('lesson_overrides',
 943          'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid));
 944          $status[] = array(
 945              'component' => $componentstr,
 946              'item' => get_string('groupoverridesdeleted', 'lesson'),
 947              'error' => false);
 948          $purgeoverrides = true;
 949      }
 950      /// updating dates - shift may be negative too
 951      if ($data->timeshift) {
 952          $DB->execute("UPDATE {lesson_overrides}
 953                           SET available = available + ?
 954                         WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?)
 955                           AND available <> 0", array($data->timeshift, $data->courseid));
 956          $DB->execute("UPDATE {lesson_overrides}
 957                           SET deadline = deadline + ?
 958                         WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?)
 959                           AND deadline <> 0", array($data->timeshift, $data->courseid));
 960  
 961          $purgeoverrides = true;
 962  
 963          // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
 964          // See MDL-9367.
 965          shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid);
 966          $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
 967      }
 968  
 969      if ($purgeoverrides) {
 970          cache::make('mod_lesson', 'overrides')->purge();
 971      }
 972  
 973      return $status;
 974  }
 975  
 976  /**
 977   * @uses FEATURE_GROUPS
 978   * @uses FEATURE_GROUPINGS
 979   * @uses FEATURE_MOD_INTRO
 980   * @uses FEATURE_COMPLETION_TRACKS_VIEWS
 981   * @uses FEATURE_GRADE_HAS_GRADE
 982   * @uses FEATURE_GRADE_OUTCOMES
 983   * @param string $feature FEATURE_xx constant for requested feature
 984   * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose.
 985   */
 986  function lesson_supports($feature) {
 987      switch($feature) {
 988          case FEATURE_GROUPS:
 989              return true;
 990          case FEATURE_GROUPINGS:
 991              return true;
 992          case FEATURE_MOD_INTRO:
 993              return true;
 994          case FEATURE_COMPLETION_TRACKS_VIEWS:
 995              return true;
 996          case FEATURE_GRADE_HAS_GRADE:
 997              return true;
 998          case FEATURE_COMPLETION_HAS_RULES:
 999              return true;
1000          case FEATURE_GRADE_OUTCOMES:
1001              return true;
1002          case FEATURE_BACKUP_MOODLE2:
1003              return true;
1004          case FEATURE_SHOW_DESCRIPTION:
1005              return true;
1006          case FEATURE_MOD_PURPOSE:
1007              return MOD_PURPOSE_CONTENT;
1008          default:
1009              return null;
1010      }
1011  }
1012  
1013  /**
1014   * This function extends the settings navigation block for the site.
1015   *
1016   * It is safe to rely on PAGE here as we will only ever be within the module
1017   * context when this is called
1018   *
1019   * @param settings_navigation $settings
1020   * @param navigation_node $lessonnode
1021   */
1022  function lesson_extend_settings_navigation(settings_navigation $settings, navigation_node $lessonnode) {
1023      // We want to add these new nodes after the Edit settings node, and before the
1024      // Locally assigned roles node. Of course, both of those are controlled by capabilities.
1025      $keys = $lessonnode->get_children_key_list();
1026      $beforekey = null;
1027      $i = array_search('modedit', $keys);
1028      if ($i === false and array_key_exists(0, $keys)) {
1029          $beforekey = $keys[0];
1030      } else if (array_key_exists($i + 1, $keys)) {
1031          $beforekey = $keys[$i + 1];
1032      }
1033  
1034      if (has_capability('mod/lesson:manageoverrides', $settings->get_page()->cm->context)) {
1035          $url = new moodle_url('/mod/lesson/overrides.php', ['cmid' => $settings->get_page()->cm->id, 'mode' => 'user']);
1036          $node = navigation_node::create(get_string('overrides', 'lesson'), $url,
1037                  navigation_node::TYPE_SETTING, null, 'mod_lesson_useroverrides');
1038          $lessonnode->add_node($node, $beforekey);
1039      }
1040  
1041      if (has_capability('mod/lesson:viewreports', $settings->get_page()->cm->context)) {
1042          $reportsnode = $lessonnode->add(
1043              get_string('reports', 'lesson'),
1044              new moodle_url('/mod/lesson/report.php', ['id' => $settings->get_page()->cm->id,
1045                  'action' => 'reportoverview'])
1046          );
1047      }
1048  }
1049  
1050  /**
1051   * Get list of available import or export formats
1052   *
1053   * Copied and modified from lib/questionlib.php
1054   *
1055   * @param string $type 'import' if import list, otherwise export list assumed
1056   * @return array sorted list of import/export formats available
1057   */
1058  function lesson_get_import_export_formats($type) {
1059      global $CFG;
1060      $fileformats = core_component::get_plugin_list("qformat");
1061  
1062      $fileformatname=array();
1063      foreach ($fileformats as $fileformat=>$fdir) {
1064          $format_file = "$fdir/format.php";
1065          if (file_exists($format_file) ) {
1066              require_once($format_file);
1067          } else {
1068              continue;
1069          }
1070          $classname = "qformat_$fileformat";
1071          $format_class = new $classname();
1072          if ($type=='import') {
1073              $provided = $format_class->provide_import();
1074          } else {
1075              $provided = $format_class->provide_export();
1076          }
1077          if ($provided) {
1078              $fileformatnames[$fileformat] = get_string('pluginname', 'qformat_'.$fileformat);
1079          }
1080      }
1081      natcasesort($fileformatnames);
1082  
1083      return $fileformatnames;
1084  }
1085  
1086  /**
1087   * Serves the lesson attachments. Implements needed access control ;-)
1088   *
1089   * @package mod_lesson
1090   * @category files
1091   * @param stdClass $course course object
1092   * @param stdClass $cm course module object
1093   * @param stdClass $context context object
1094   * @param string $filearea file area
1095   * @param array $args extra arguments
1096   * @param bool $forcedownload whether or not force download
1097   * @param array $options additional options affecting the file serving
1098   * @return bool false if file not found, does not return if found - justsend the file
1099   */
1100  function lesson_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
1101      global $CFG, $DB;
1102  
1103      if ($context->contextlevel != CONTEXT_MODULE) {
1104          return false;
1105      }
1106  
1107      $fileareas = lesson_get_file_areas();
1108      if (!array_key_exists($filearea, $fileareas)) {
1109          return false;
1110      }
1111  
1112      if (!$lesson = $DB->get_record('lesson', array('id'=>$cm->instance))) {
1113          return false;
1114      }
1115  
1116      require_course_login($course, true, $cm);
1117  
1118      if ($filearea === 'page_contents') {
1119          $pageid = (int)array_shift($args);
1120          if (!$page = $DB->get_record('lesson_pages', array('id'=>$pageid))) {
1121              return false;
1122          }
1123          $fullpath = "/$context->id/mod_lesson/$filearea/$pageid/".implode('/', $args);
1124  
1125      } else if ($filearea === 'page_answers' || $filearea === 'page_responses') {
1126          $itemid = (int)array_shift($args);
1127          if (!$pageanswers = $DB->get_record('lesson_answers', array('id' => $itemid))) {
1128              return false;
1129          }
1130          $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args);
1131  
1132      } else if ($filearea === 'essay_responses' || $filearea === 'essay_answers') {
1133          $itemid = (int)array_shift($args);
1134          if (!$attempt = $DB->get_record('lesson_attempts', array('id' => $itemid))) {
1135              return false;
1136          }
1137          $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args);
1138  
1139      } else if ($filearea === 'mediafile') {
1140          if (count($args) > 1) {
1141              // Remove the itemid when it appears to be part of the arguments. If there is only one argument
1142              // then it is surely the file name. The itemid is sometimes used to prevent browser caching.
1143              array_shift($args);
1144          }
1145          $fullpath = "/$context->id/mod_lesson/$filearea/0/".implode('/', $args);
1146  
1147      } else {
1148          return false;
1149      }
1150  
1151      $fs = get_file_storage();
1152      if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1153          return false;
1154      }
1155  
1156      // finally send the file
1157      send_stored_file($file, 0, 0, $forcedownload, $options); // download MUST be forced - security!
1158  }
1159  
1160  /**
1161   * Returns an array of file areas
1162   *
1163   * @package  mod_lesson
1164   * @category files
1165   * @return array a list of available file areas
1166   */
1167  function lesson_get_file_areas() {
1168      $areas = array();
1169      $areas['page_contents'] = get_string('pagecontents', 'mod_lesson');
1170      $areas['mediafile'] = get_string('mediafile', 'mod_lesson');
1171      $areas['page_answers'] = get_string('pageanswers', 'mod_lesson');
1172      $areas['page_responses'] = get_string('pageresponses', 'mod_lesson');
1173      $areas['essay_responses'] = get_string('essayresponses', 'mod_lesson');
1174      $areas['essay_answers'] = get_string('essayresponses', 'mod_lesson');
1175      return $areas;
1176  }
1177  
1178  /**
1179   * Returns a file_info_stored object for the file being requested here
1180   *
1181   * @package  mod_lesson
1182   * @category files
1183   * @global stdClass $CFG
1184   * @param file_browse $browser file browser instance
1185   * @param array $areas file areas
1186   * @param stdClass $course course object
1187   * @param stdClass $cm course module object
1188   * @param stdClass $context context object
1189   * @param string $filearea file area
1190   * @param int $itemid item ID
1191   * @param string $filepath file path
1192   * @param string $filename file name
1193   * @return file_info_stored
1194   */
1195  function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
1196      global $CFG, $DB;
1197  
1198      if (!has_capability('moodle/course:managefiles', $context)) {
1199          // No peaking here for students!
1200          return null;
1201      }
1202  
1203      // Mediafile area does not have sub directories, so let's select the default itemid to prevent
1204      // the user from selecting a directory to access the mediafile content.
1205      if ($filearea == 'mediafile' && is_null($itemid)) {
1206          $itemid = 0;
1207      }
1208  
1209      if (is_null($itemid)) {
1210          return new mod_lesson_file_info($browser, $course, $cm, $context, $areas, $filearea);
1211      }
1212  
1213      $fs = get_file_storage();
1214      $filepath = is_null($filepath) ? '/' : $filepath;
1215      $filename = is_null($filename) ? '.' : $filename;
1216      if (!$storedfile = $fs->get_file($context->id, 'mod_lesson', $filearea, $itemid, $filepath, $filename)) {
1217          return null;
1218      }
1219  
1220      $itemname = $filearea;
1221      if ($filearea == 'page_contents') {
1222          $itemname = $DB->get_field('lesson_pages', 'title', array('lessonid' => $cm->instance, 'id' => $itemid));
1223          $itemname = format_string($itemname, true, array('context' => $context));
1224      } else {
1225          $areas = lesson_get_file_areas();
1226          if (isset($areas[$filearea])) {
1227              $itemname = $areas[$filearea];
1228          }
1229      }
1230  
1231      $urlbase = $CFG->wwwroot . '/pluginfile.php';
1232      return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemname, $itemid, true, true, false);
1233  }
1234  
1235  
1236  /**
1237   * Return a list of page types
1238   * @param string $pagetype current page type
1239   * @param stdClass $parentcontext Block's parent context
1240   * @param stdClass $currentcontext Current context of block
1241   */
1242  function lesson_page_type_list($pagetype, $parentcontext, $currentcontext) {
1243      $module_pagetype = array(
1244          'mod-lesson-*'=>get_string('page-mod-lesson-x', 'lesson'),
1245          'mod-lesson-view'=>get_string('page-mod-lesson-view', 'lesson'),
1246          'mod-lesson-edit'=>get_string('page-mod-lesson-edit', 'lesson'));
1247      return $module_pagetype;
1248  }
1249  
1250  /**
1251   * Update the lesson activity to include any file
1252   * that was uploaded, or if there is none, set the
1253   * mediafile field to blank.
1254   *
1255   * @param int $lessonid the lesson id
1256   * @param stdClass $context the context
1257   * @param int $draftitemid the draft item
1258   */
1259  function lesson_update_media_file($lessonid, $context, $draftitemid) {
1260      global $DB;
1261  
1262      // Set the filestorage object.
1263      $fs = get_file_storage();
1264      // Save the file if it exists that is currently in the draft area.
1265      file_save_draft_area_files($draftitemid, $context->id, 'mod_lesson', 'mediafile', 0);
1266      // Get the file if it exists.
1267      $files = $fs->get_area_files($context->id, 'mod_lesson', 'mediafile', 0, 'itemid, filepath, filename', false);
1268      // Check that there is a file to process.
1269      if (count($files) == 1) {
1270          // Get the first (and only) file.
1271          $file = reset($files);
1272          // Set the mediafile column in the lessons table.
1273          $DB->set_field('lesson', 'mediafile', '/' . $file->get_filename(), array('id' => $lessonid));
1274      } else {
1275          // Set the mediafile column in the lessons table.
1276          $DB->set_field('lesson', 'mediafile', '', array('id' => $lessonid));
1277      }
1278  }
1279  
1280  /**
1281   * Get icon mapping for font-awesome.
1282   */
1283  function mod_lesson_get_fontawesome_icon_map() {
1284      return [
1285          'mod_lesson:e/copy' => 'fa-clone',
1286      ];
1287  }
1288  
1289  /*
1290   * Check if the module has any update that affects the current user since a given time.
1291   *
1292   * @param  cm_info $cm course module data
1293   * @param  int $from the time to check updates from
1294   * @param  array $filter  if we need to check only specific updates
1295   * @return stdClass an object with the different type of areas indicating if they were updated or not
1296   * @since Moodle 3.3
1297   */
1298  function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
1299      global $DB, $USER;
1300  
1301      $updates = course_check_module_updates_since($cm, $from, array(), $filter);
1302  
1303      // Check if there are new pages or answers in the lesson.
1304      $updates->pages = (object) array('updated' => false);
1305      $updates->answers = (object) array('updated' => false);
1306      $select = 'lessonid = ? AND (timecreated > ? OR timemodified > ?)';
1307      $params = array($cm->instance, $from, $from);
1308  
1309      $pages = $DB->get_records_select('lesson_pages', $select, $params, '', 'id');
1310      if (!empty($pages)) {
1311          $updates->pages->updated = true;
1312          $updates->pages->itemids = array_keys($pages);
1313      }
1314      $answers = $DB->get_records_select('lesson_answers', $select, $params, '', 'id');
1315      if (!empty($answers)) {
1316          $updates->answers->updated = true;
1317          $updates->answers->itemids = array_keys($answers);
1318      }
1319  
1320      // Check for new question attempts, grades, pages viewed and timers.
1321      $updates->questionattempts = (object) array('updated' => false);
1322      $updates->grades = (object) array('updated' => false);
1323      $updates->pagesviewed = (object) array('updated' => false);
1324      $updates->timers = (object) array('updated' => false);
1325  
1326      $select = 'lessonid = ? AND userid = ? AND timeseen > ?';
1327      $params = array($cm->instance, $USER->id, $from);
1328  
1329      $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id');
1330      if (!empty($questionattempts)) {
1331          $updates->questionattempts->updated = true;
1332          $updates->questionattempts->itemids = array_keys($questionattempts);
1333      }
1334      $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id');
1335      if (!empty($pagesviewed)) {
1336          $updates->pagesviewed->updated = true;
1337          $updates->pagesviewed->itemids = array_keys($pagesviewed);
1338      }
1339  
1340      $select = 'lessonid = ? AND userid = ? AND completed > ?';
1341      $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
1342      if (!empty($grades)) {
1343          $updates->grades->updated = true;
1344          $updates->grades->itemids = array_keys($grades);
1345      }
1346  
1347      $select = 'lessonid = ? AND userid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
1348      $params = array($cm->instance, $USER->id, $from, $from, $from);
1349      $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
1350      if (!empty($timers)) {
1351          $updates->timers->updated = true;
1352          $updates->timers->itemids = array_keys($timers);
1353      }
1354  
1355      // Now, teachers should see other students updates.
1356      if (has_capability('mod/lesson:viewreports', $cm->context)) {
1357          $select = 'lessonid = ? AND timeseen > ?';
1358          $params = array($cm->instance, $from);
1359  
1360          $insql = '';
1361          $inparams = [];
1362          if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
1363              $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
1364              if (empty($groupusers)) {
1365                  return $updates;
1366              }
1367              list($insql, $inparams) = $DB->get_in_or_equal($groupusers);
1368              $select .= ' AND userid ' . $insql;
1369              $params = array_merge($params, $inparams);
1370          }
1371  
1372          $updates->userquestionattempts = (object) array('updated' => false);
1373          $updates->usergrades = (object) array('updated' => false);
1374          $updates->userpagesviewed = (object) array('updated' => false);
1375          $updates->usertimers = (object) array('updated' => false);
1376  
1377          $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id');
1378          if (!empty($questionattempts)) {
1379              $updates->userquestionattempts->updated = true;
1380              $updates->userquestionattempts->itemids = array_keys($questionattempts);
1381          }
1382          $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id');
1383          if (!empty($pagesviewed)) {
1384              $updates->userpagesviewed->updated = true;
1385              $updates->userpagesviewed->itemids = array_keys($pagesviewed);
1386          }
1387  
1388          $select = 'lessonid = ? AND completed > ?';
1389          if (!empty($insql)) {
1390              $select .= ' AND userid ' . $insql;
1391          }
1392          $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
1393          if (!empty($grades)) {
1394              $updates->usergrades->updated = true;
1395              $updates->usergrades->itemids = array_keys($grades);
1396          }
1397  
1398          $select = 'lessonid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
1399          $params = array($cm->instance, $from, $from, $from);
1400          if (!empty($insql)) {
1401              $select .= ' AND userid ' . $insql;
1402              $params = array_merge($params, $inparams);
1403          }
1404          $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
1405          if (!empty($timers)) {
1406              $updates->usertimers->updated = true;
1407              $updates->usertimers->itemids = array_keys($timers);
1408          }
1409      }
1410      return $updates;
1411  }
1412  
1413  /**
1414   * This function receives a calendar event and returns the action associated with it, or null if there is none.
1415   *
1416   * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
1417   * is not displayed on the block.
1418   *
1419   * @param calendar_event $event
1420   * @param \core_calendar\action_factory $factory
1421   * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
1422   * @return \core_calendar\local\event\entities\action_interface|null
1423   */
1424  function mod_lesson_core_calendar_provide_event_action(calendar_event $event,
1425                                                         \core_calendar\action_factory $factory,
1426                                                         int $userid = 0) {
1427      global $DB, $CFG, $USER;
1428      require_once($CFG->dirroot . '/mod/lesson/locallib.php');
1429  
1430      if (!$userid) {
1431          $userid = $USER->id;
1432      }
1433  
1434      $cm = get_fast_modinfo($event->courseid, $userid)->instances['lesson'][$event->instance];
1435  
1436      if (!$cm->uservisible) {
1437          // The module is not visible to the user for any reason.
1438          return null;
1439      }
1440  
1441      $completion = new \completion_info($cm->get_course());
1442  
1443      $completiondata = $completion->get_data($cm, false, $userid);
1444  
1445      if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
1446          return null;
1447      }
1448  
1449      $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
1450  
1451      if ($lesson->count_user_retries($userid)) {
1452          // If the user has attempted the lesson then there is no further action for the user.
1453          return null;
1454      }
1455  
1456      // Apply overrides.
1457      $lesson->update_effective_access($userid);
1458  
1459      if (!$lesson->is_participant($userid)) {
1460          // If the user is not a participant then they have
1461          // no action to take. This will filter out the events for teachers.
1462          return null;
1463      }
1464  
1465      return $factory->create_instance(
1466          get_string('startlesson', 'lesson'),
1467          new \moodle_url('/mod/lesson/view.php', ['id' => $cm->id]),
1468          1,
1469          $lesson->is_accessible()
1470      );
1471  }
1472  
1473  /**
1474   * Add a get_coursemodule_info function in case any lesson type wants to add 'extra' information
1475   * for the course (see resource).
1476   *
1477   * Given a course_module object, this function returns any "extra" information that may be needed
1478   * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.
1479   *
1480   * @param stdClass $coursemodule The coursemodule object (record).
1481   * @return cached_cm_info An object on information that the courses
1482   *                        will know about (most noticeably, an icon).
1483   */
1484  function lesson_get_coursemodule_info($coursemodule) {
1485      global $DB;
1486  
1487      $dbparams = ['id' => $coursemodule->instance];
1488      $fields = 'id, name, intro, introformat, completionendreached, completiontimespent, available, deadline';
1489      if (!$lesson = $DB->get_record('lesson', $dbparams, $fields)) {
1490          return false;
1491      }
1492  
1493      $result = new cached_cm_info();
1494      $result->name = $lesson->name;
1495  
1496      if ($coursemodule->showdescription) {
1497          // Convert intro to html. Do not filter cached version, filters run at display time.
1498          $result->content = format_module_intro('lesson', $lesson, $coursemodule->id, false);
1499      }
1500  
1501      // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
1502      if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
1503          $result->customdata['customcompletionrules']['completionendreached'] = $lesson->completionendreached;
1504          $result->customdata['customcompletionrules']['completiontimespent'] = $lesson->completiontimespent;
1505      }
1506  
1507      // Populate some other values that can be used in calendar or on dashboard.
1508      if ($lesson->available) {
1509          $result->customdata['available'] = $lesson->available;
1510      }
1511      if ($lesson->deadline) {
1512          $result->customdata['deadline'] = $lesson->deadline;
1513      }
1514  
1515      return $result;
1516  }
1517  
1518  /**
1519   * Sets dynamic information about a course module
1520   *
1521   * This function is called from cm_info when displaying the module
1522   *
1523   * @param cm_info $cm
1524   */
1525  function mod_lesson_cm_info_dynamic(cm_info $cm) {
1526      global $USER;
1527  
1528      $cache = cache::make('mod_lesson', 'overrides');
1529      $override = $cache->get("{$cm->instance}_u_{$USER->id}");
1530  
1531      if (!$override) {
1532          $override = (object) [
1533              'available' => null,
1534              'deadline' => null,
1535          ];
1536      }
1537  
1538      // No need to look for group overrides if there are user overrides for both available and deadline.
1539      if (is_null($override->available) || is_null($override->deadline)) {
1540          $availables = [];
1541          $deadlines = [];
1542          $groupings = groups_get_user_groups($cm->course, $USER->id);
1543          foreach ($groupings[0] as $groupid) {
1544              $groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
1545              if (isset($groupoverride->available)) {
1546                  $availables[] = $groupoverride->available;
1547              }
1548              if (isset($groupoverride->deadline)) {
1549                  $deadlines[] = $groupoverride->deadline;
1550              }
1551          }
1552          // If there is a user override for a setting, ignore the group override.
1553          if (is_null($override->available) && count($availables)) {
1554              $override->available = min($availables);
1555          }
1556          if (is_null($override->deadline) && count($deadlines)) {
1557              if (in_array(0, $deadlines)) {
1558                  $override->deadline = 0;
1559              } else {
1560                  $override->deadline = max($deadlines);
1561              }
1562          }
1563      }
1564  
1565      // Populate some other values that can be used in calendar or on dashboard.
1566      if (!is_null($override->available)) {
1567          $cm->override_customdata('available', $override->available);
1568      }
1569      if (!is_null($override->deadline)) {
1570          $cm->override_customdata('deadline', $override->deadline);
1571      }
1572  }
1573  
1574  /**
1575   * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
1576   *
1577   * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
1578   * @return array $descriptions the array of descriptions for the custom rules.
1579   */
1580  function mod_lesson_get_completion_active_rule_descriptions($cm) {
1581      // Values will be present in cm_info, and we assume these are up to date.
1582      if (empty($cm->customdata['customcompletionrules'])
1583          || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
1584          return [];
1585      }
1586  
1587      $descriptions = [];
1588      foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
1589          switch ($key) {
1590              case 'completionendreached':
1591                  if (!empty($val)) {
1592                      $descriptions[] = get_string('completionendreached_desc', 'lesson', $val);
1593                  }
1594                  break;
1595              case 'completiontimespent':
1596                  if (!empty($val)) {
1597                      $descriptions[] = get_string('completiontimespentdesc', 'lesson', format_time($val));
1598                  }
1599                  break;
1600              default:
1601                  break;
1602          }
1603      }
1604      return $descriptions;
1605  }
1606  
1607  /**
1608   * This function calculates the minimum and maximum cutoff values for the timestart of
1609   * the given event.
1610   *
1611   * It will return an array with two values, the first being the minimum cutoff value and
1612   * the second being the maximum cutoff value. Either or both values can be null, which
1613   * indicates there is no minimum or maximum, respectively.
1614   *
1615   * If a cutoff is required then the function must return an array containing the cutoff
1616   * timestamp and error string to display to the user if the cutoff value is violated.
1617   *
1618   * A minimum and maximum cutoff return value will look like:
1619   * [
1620   *     [1505704373, 'The due date must be after the start date'],
1621   *     [1506741172, 'The due date must be before the cutoff date']
1622   * ]
1623   *
1624   * @param calendar_event $event The calendar event to get the time range for
1625   * @param stdClass $instance The module instance to get the range from
1626   * @return array
1627   */
1628  function mod_lesson_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
1629      $mindate = null;
1630      $maxdate = null;
1631  
1632      if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
1633          // The start time of the open event can't be equal to or after the
1634          // close time of the lesson activity.
1635          if (!empty($instance->deadline)) {
1636              $maxdate = [
1637                  $instance->deadline,
1638                  get_string('openafterclose', 'lesson')
1639              ];
1640          }
1641      } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
1642          // The start time of the close event can't be equal to or earlier than the
1643          // open time of the lesson activity.
1644          if (!empty($instance->available)) {
1645              $mindate = [
1646                  $instance->available,
1647                  get_string('closebeforeopen', 'lesson')
1648              ];
1649          }
1650      }
1651  
1652      return [$mindate, $maxdate];
1653  }
1654  
1655  /**
1656   * This function will update the lesson module according to the
1657   * event that has been modified.
1658   *
1659   * It will set the available or deadline value of the lesson instance
1660   * according to the type of event provided.
1661   *
1662   * @throws \moodle_exception
1663   * @param \calendar_event $event
1664   * @param stdClass $lesson The module instance to get the range from
1665   */
1666  function mod_lesson_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $lesson) {
1667      global $DB;
1668  
1669      if (empty($event->instance) || $event->modulename != 'lesson') {
1670          return;
1671      }
1672  
1673      if ($event->instance != $lesson->id) {
1674          return;
1675      }
1676  
1677      if (!in_array($event->eventtype, [LESSON_EVENT_TYPE_OPEN, LESSON_EVENT_TYPE_CLOSE])) {
1678          return;
1679      }
1680  
1681      $courseid = $event->courseid;
1682      $modulename = $event->modulename;
1683      $instanceid = $event->instance;
1684      $modified = false;
1685  
1686      $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
1687      $context = context_module::instance($coursemodule->id);
1688  
1689      // The user does not have the capability to modify this activity.
1690      if (!has_capability('moodle/course:manageactivities', $context)) {
1691          return;
1692      }
1693  
1694      if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
1695          // If the event is for the lesson activity opening then we should
1696          // set the start time of the lesson activity to be the new start
1697          // time of the event.
1698          if ($lesson->available != $event->timestart) {
1699              $lesson->available = $event->timestart;
1700              $lesson->timemodified = time();
1701              $modified = true;
1702          }
1703      } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
1704          // If the event is for the lesson activity closing then we should
1705          // set the end time of the lesson activity to be the new start
1706          // time of the event.
1707          if ($lesson->deadline != $event->timestart) {
1708              $lesson->deadline = $event->timestart;
1709              $modified = true;
1710          }
1711      }
1712  
1713      if ($modified) {
1714          $lesson->timemodified = time();
1715          $DB->update_record('lesson', $lesson);
1716          $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
1717          $event->trigger();
1718      }
1719  }
1720  
1721  /**
1722   * Callback to fetch the activity event type lang string.
1723   *
1724   * @param string $eventtype The event type.
1725   * @return lang_string The event type lang string.
1726   */
1727  function mod_lesson_core_calendar_get_event_action_string(string $eventtype): string {
1728      $modulename = get_string('modulename', 'lesson');
1729  
1730      switch ($eventtype) {
1731          case LESSON_EVENT_TYPE_OPEN:
1732              $identifier = 'lessoneventopens';
1733              break;
1734          case LESSON_EVENT_TYPE_CLOSE:
1735              $identifier = 'lessoneventcloses';
1736              break;
1737          default:
1738              return get_string('requiresaction', 'calendar', $modulename);
1739      }
1740  
1741      return get_string($identifier, 'lesson', $modulename);
1742  }