Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/mod/lesson/ -> lib.php (source)

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