Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
/mod/lesson/ -> lib.php (source)

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

   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
 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          default:
1007              return null;
1008      }
1009  }
1010  
1011  /**
1012   * This function extends the settings navigation block for the site.
1013   *
1014   * It is safe to rely on PAGE here as we will only ever be within the module
1015   * context when this is called
1016   *
1017   * @param settings_navigation $settings
1018   * @param navigation_node $lessonnode
1019   */
1020  function lesson_extend_settings_navigation($settings, $lessonnode) {
1021      global $PAGE, $DB;
1022  
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', $PAGE->cm->context)) {
1035          $url = new moodle_url('/mod/lesson/overrides.php', array('cmid' => $PAGE->cm->id));
1036          $node = navigation_node::create(get_string('groupoverrides', 'lesson'),
1037                  new moodle_url($url, array('mode' => 'group')),
1038                  navigation_node::TYPE_SETTING, null, 'mod_lesson_groupoverrides');
1039          $lessonnode->add_node($node, $beforekey);
1040  
1041          $node = navigation_node::create(get_string('useroverrides', 'lesson'),
1042                  new moodle_url($url, array('mode' => 'user')),
1043                  navigation_node::TYPE_SETTING, null, 'mod_lesson_useroverrides');
1044          $lessonnode->add_node($node, $beforekey);
1045      }
1046  
1047      if (has_capability('mod/lesson:edit', $PAGE->cm->context)) {
1048          $url = new moodle_url('/mod/lesson/view.php', array('id' => $PAGE->cm->id));
1049          $lessonnode->add(get_string('preview', 'lesson'), $url);
1050          $editnode = $lessonnode->add(get_string('edit', 'lesson'));
1051          $url = new moodle_url('/mod/lesson/edit.php', array('id' => $PAGE->cm->id, 'mode' => 'collapsed'));
1052          $editnode->add(get_string('collapsed', 'lesson'), $url);
1053          $url = new moodle_url('/mod/lesson/edit.php', array('id' => $PAGE->cm->id, 'mode' => 'full'));
1054          $editnode->add(get_string('full', 'lesson'), $url);
1055      }
1056  
1057      if (has_capability('mod/lesson:viewreports', $PAGE->cm->context)) {
1058          $reportsnode = $lessonnode->add(get_string('reports', 'lesson'));
1059          $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportoverview'));
1060          $reportsnode->add(get_string('overview', 'lesson'), $url);
1061          $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportdetail'));
1062          $reportsnode->add(get_string('detailedstats', 'lesson'), $url);
1063      }
1064  
1065      if (has_capability('mod/lesson:grade', $PAGE->cm->context)) {
1066          $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$PAGE->cm->id));
1067          $lessonnode->add(get_string('manualgrading', 'lesson'), $url);
1068      }
1069  
1070  }
1071  
1072  /**
1073   * Get list of available import or export formats
1074   *
1075   * Copied and modified from lib/questionlib.php
1076   *
1077   * @param string $type 'import' if import list, otherwise export list assumed
1078   * @return array sorted list of import/export formats available
1079   */
1080  function lesson_get_import_export_formats($type) {
1081      global $CFG;
1082      $fileformats = core_component::get_plugin_list("qformat");
1083  
1084      $fileformatname=array();
1085      foreach ($fileformats as $fileformat=>$fdir) {
1086          $format_file = "$fdir/format.php";
1087          if (file_exists($format_file) ) {
1088              require_once($format_file);
1089          } else {
1090              continue;
1091          }
1092          $classname = "qformat_$fileformat";
1093          $format_class = new $classname();
1094          if ($type=='import') {
1095              $provided = $format_class->provide_import();
1096          } else {
1097              $provided = $format_class->provide_export();
1098          }
1099          if ($provided) {
1100              $fileformatnames[$fileformat] = get_string('pluginname', 'qformat_'.$fileformat);
1101          }
1102      }
1103      natcasesort($fileformatnames);
1104  
1105      return $fileformatnames;
1106  }
1107  
1108  /**
1109   * Serves the lesson attachments. Implements needed access control ;-)
1110   *
1111   * @package mod_lesson
1112   * @category files
1113   * @param stdClass $course course object
1114   * @param stdClass $cm course module object
1115   * @param stdClass $context context object
1116   * @param string $filearea file area
1117   * @param array $args extra arguments
1118   * @param bool $forcedownload whether or not force download
1119   * @param array $options additional options affecting the file serving
1120   * @return bool false if file not found, does not return if found - justsend the file
1121   */
1122  function lesson_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
1123      global $CFG, $DB;
1124  
1125      if ($context->contextlevel != CONTEXT_MODULE) {
1126          return false;
1127      }
1128  
1129      $fileareas = lesson_get_file_areas();
1130      if (!array_key_exists($filearea, $fileareas)) {
1131          return false;
1132      }
1133  
1134      if (!$lesson = $DB->get_record('lesson', array('id'=>$cm->instance))) {
1135          return false;
1136      }
1137  
1138      require_course_login($course, true, $cm);
1139  
1140      if ($filearea === 'page_contents') {
1141          $pageid = (int)array_shift($args);
1142          if (!$page = $DB->get_record('lesson_pages', array('id'=>$pageid))) {
1143              return false;
1144          }
1145          $fullpath = "/$context->id/mod_lesson/$filearea/$pageid/".implode('/', $args);
1146  
1147      } else if ($filearea === 'page_answers' || $filearea === 'page_responses') {
1148          $itemid = (int)array_shift($args);
1149          if (!$pageanswers = $DB->get_record('lesson_answers', array('id' => $itemid))) {
1150              return false;
1151          }
1152          $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args);
1153  
1154      } else if ($filearea === 'essay_responses' || $filearea === 'essay_answers') {
1155          $itemid = (int)array_shift($args);
1156          if (!$attempt = $DB->get_record('lesson_attempts', array('id' => $itemid))) {
1157              return false;
1158          }
1159          $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args);
1160  
1161      } else if ($filearea === 'mediafile') {
1162          if (count($args) > 1) {
1163              // Remove the itemid when it appears to be part of the arguments. If there is only one argument
1164              // then it is surely the file name. The itemid is sometimes used to prevent browser caching.
1165              array_shift($args);
1166          }
1167          $fullpath = "/$context->id/mod_lesson/$filearea/0/".implode('/', $args);
1168  
1169      } else {
1170          return false;
1171      }
1172  
1173      $fs = get_file_storage();
1174      if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
1175          return false;
1176      }
1177  
1178      // finally send the file
1179      send_stored_file($file, 0, 0, $forcedownload, $options); // download MUST be forced - security!
1180  }
1181  
1182  /**
1183   * Returns an array of file areas
1184   *
1185   * @package  mod_lesson
1186   * @category files
1187   * @return array a list of available file areas
1188   */
1189  function lesson_get_file_areas() {
1190      $areas = array();
1191      $areas['page_contents'] = get_string('pagecontents', 'mod_lesson');
1192      $areas['mediafile'] = get_string('mediafile', 'mod_lesson');
1193      $areas['page_answers'] = get_string('pageanswers', 'mod_lesson');
1194      $areas['page_responses'] = get_string('pageresponses', 'mod_lesson');
1195      $areas['essay_responses'] = get_string('essayresponses', 'mod_lesson');
1196      $areas['essay_answers'] = get_string('essayresponses', 'mod_lesson');
1197      return $areas;
1198  }
1199  
1200  /**
1201   * Returns a file_info_stored object for the file being requested here
1202   *
1203   * @package  mod_lesson
1204   * @category files
1205   * @global stdClass $CFG
1206   * @param file_browse $browser file browser instance
1207   * @param array $areas file areas
1208   * @param stdClass $course course object
1209   * @param stdClass $cm course module object
1210   * @param stdClass $context context object
1211   * @param string $filearea file area
1212   * @param int $itemid item ID
1213   * @param string $filepath file path
1214   * @param string $filename file name
1215   * @return file_info_stored
1216   */
1217  function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
1218      global $CFG, $DB;
1219  
1220      if (!has_capability('moodle/course:managefiles', $context)) {
1221          // No peaking here for students!
1222          return null;
1223      }
1224  
1225      // Mediafile area does not have sub directories, so let's select the default itemid to prevent
1226      // the user from selecting a directory to access the mediafile content.
1227      if ($filearea == 'mediafile' && is_null($itemid)) {
1228          $itemid = 0;
1229      }
1230  
1231      if (is_null($itemid)) {
1232          return new mod_lesson_file_info($browser, $course, $cm, $context, $areas, $filearea);
1233      }
1234  
1235      $fs = get_file_storage();
1236      $filepath = is_null($filepath) ? '/' : $filepath;
1237      $filename = is_null($filename) ? '.' : $filename;
1238      if (!$storedfile = $fs->get_file($context->id, 'mod_lesson', $filearea, $itemid, $filepath, $filename)) {
1239          return null;
1240      }
1241  
1242      $itemname = $filearea;
1243      if ($filearea == 'page_contents') {
1244          $itemname = $DB->get_field('lesson_pages', 'title', array('lessonid' => $cm->instance, 'id' => $itemid));
1245          $itemname = format_string($itemname, true, array('context' => $context));
1246      } else {
1247          $areas = lesson_get_file_areas();
1248          if (isset($areas[$filearea])) {
1249              $itemname = $areas[$filearea];
1250          }
1251      }
1252  
1253      $urlbase = $CFG->wwwroot . '/pluginfile.php';
1254      return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemname, $itemid, true, true, false);
1255  }
1256  
1257  
1258  /**
1259   * Return a list of page types
1260   * @param string $pagetype current page type
1261   * @param stdClass $parentcontext Block's parent context
1262   * @param stdClass $currentcontext Current context of block
1263   */
1264  function lesson_page_type_list($pagetype, $parentcontext, $currentcontext) {
1265      $module_pagetype = array(
1266          'mod-lesson-*'=>get_string('page-mod-lesson-x', 'lesson'),
1267          'mod-lesson-view'=>get_string('page-mod-lesson-view', 'lesson'),
1268          'mod-lesson-edit'=>get_string('page-mod-lesson-edit', 'lesson'));
1269      return $module_pagetype;
1270  }
1271  
1272  /**
1273   * Update the lesson activity to include any file
1274   * that was uploaded, or if there is none, set the
1275   * mediafile field to blank.
1276   *
1277   * @param int $lessonid the lesson id
1278   * @param stdClass $context the context
1279   * @param int $draftitemid the draft item
1280   */
1281  function lesson_update_media_file($lessonid, $context, $draftitemid) {
1282      global $DB;
1283  
1284      // Set the filestorage object.
1285      $fs = get_file_storage();
1286      // Save the file if it exists that is currently in the draft area.
1287      file_save_draft_area_files($draftitemid, $context->id, 'mod_lesson', 'mediafile', 0);
1288      // Get the file if it exists.
1289      $files = $fs->get_area_files($context->id, 'mod_lesson', 'mediafile', 0, 'itemid, filepath, filename', false);
1290      // Check that there is a file to process.
1291      if (count($files) == 1) {
1292          // Get the first (and only) file.
1293          $file = reset($files);
1294          // Set the mediafile column in the lessons table.
1295          $DB->set_field('lesson', 'mediafile', '/' . $file->get_filename(), array('id' => $lessonid));
1296      } else {
1297          // Set the mediafile column in the lessons table.
1298          $DB->set_field('lesson', 'mediafile', '', array('id' => $lessonid));
1299      }
1300  }
1301  
1302  /**
1303   * Get icon mapping for font-awesome.
1304   */
1305  function mod_lesson_get_fontawesome_icon_map() {
1306      return [
1307          'mod_lesson:e/copy' => 'fa-clone',
1308      ];
1309  }
1310  
1311  /*
1312   * Check if the module has any update that affects the current user since a given time.
1313   *
1314   * @param  cm_info $cm course module data
1315   * @param  int $from the time to check updates from
1316   * @param  array $filter  if we need to check only specific updates
1317   * @return stdClass an object with the different type of areas indicating if they were updated or not
1318   * @since Moodle 3.3
1319   */
1320  function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) {
1321      global $DB, $USER;
1322  
1323      $updates = course_check_module_updates_since($cm, $from, array(), $filter);
1324  
1325      // Check if there are new pages or answers in the lesson.
1326      $updates->pages = (object) array('updated' => false);
1327      $updates->answers = (object) array('updated' => false);
1328      $select = 'lessonid = ? AND (timecreated > ? OR timemodified > ?)';
1329      $params = array($cm->instance, $from, $from);
1330  
1331      $pages = $DB->get_records_select('lesson_pages', $select, $params, '', 'id');
1332      if (!empty($pages)) {
1333          $updates->pages->updated = true;
1334          $updates->pages->itemids = array_keys($pages);
1335      }
1336      $answers = $DB->get_records_select('lesson_answers', $select, $params, '', 'id');
1337      if (!empty($answers)) {
1338          $updates->answers->updated = true;
1339          $updates->answers->itemids = array_keys($answers);
1340      }
1341  
1342      // Check for new question attempts, grades, pages viewed and timers.
1343      $updates->questionattempts = (object) array('updated' => false);
1344      $updates->grades = (object) array('updated' => false);
1345      $updates->pagesviewed = (object) array('updated' => false);
1346      $updates->timers = (object) array('updated' => false);
1347  
1348      $select = 'lessonid = ? AND userid = ? AND timeseen > ?';
1349      $params = array($cm->instance, $USER->id, $from);
1350  
1351      $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id');
1352      if (!empty($questionattempts)) {
1353          $updates->questionattempts->updated = true;
1354          $updates->questionattempts->itemids = array_keys($questionattempts);
1355      }
1356      $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id');
1357      if (!empty($pagesviewed)) {
1358          $updates->pagesviewed->updated = true;
1359          $updates->pagesviewed->itemids = array_keys($pagesviewed);
1360      }
1361  
1362      $select = 'lessonid = ? AND userid = ? AND completed > ?';
1363      $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
1364      if (!empty($grades)) {
1365          $updates->grades->updated = true;
1366          $updates->grades->itemids = array_keys($grades);
1367      }
1368  
1369      $select = 'lessonid = ? AND userid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
1370      $params = array($cm->instance, $USER->id, $from, $from, $from);
1371      $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
1372      if (!empty($timers)) {
1373          $updates->timers->updated = true;
1374          $updates->timers->itemids = array_keys($timers);
1375      }
1376  
1377      // Now, teachers should see other students updates.
1378      if (has_capability('mod/lesson:viewreports', $cm->context)) {
1379          $select = 'lessonid = ? AND timeseen > ?';
1380          $params = array($cm->instance, $from);
1381  
1382          $insql = '';
1383          $inparams = [];
1384          if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
1385              $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
1386              if (empty($groupusers)) {
1387                  return $updates;
1388              }
1389              list($insql, $inparams) = $DB->get_in_or_equal($groupusers);
1390              $select .= ' AND userid ' . $insql;
1391              $params = array_merge($params, $inparams);
1392          }
1393  
1394          $updates->userquestionattempts = (object) array('updated' => false);
1395          $updates->usergrades = (object) array('updated' => false);
1396          $updates->userpagesviewed = (object) array('updated' => false);
1397          $updates->usertimers = (object) array('updated' => false);
1398  
1399          $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id');
1400          if (!empty($questionattempts)) {
1401              $updates->userquestionattempts->updated = true;
1402              $updates->userquestionattempts->itemids = array_keys($questionattempts);
1403          }
1404          $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id');
1405          if (!empty($pagesviewed)) {
1406              $updates->userpagesviewed->updated = true;
1407              $updates->userpagesviewed->itemids = array_keys($pagesviewed);
1408          }
1409  
1410          $select = 'lessonid = ? AND completed > ?';
1411          if (!empty($insql)) {
1412              $select .= ' AND userid ' . $insql;
1413          }
1414          $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id');
1415          if (!empty($grades)) {
1416              $updates->usergrades->updated = true;
1417              $updates->usergrades->itemids = array_keys($grades);
1418          }
1419  
1420          $select = 'lessonid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)';
1421          $params = array($cm->instance, $from, $from, $from);
1422          if (!empty($insql)) {
1423              $select .= ' AND userid ' . $insql;
1424              $params = array_merge($params, $inparams);
1425          }
1426          $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id');
1427          if (!empty($timers)) {
1428              $updates->usertimers->updated = true;
1429              $updates->usertimers->itemids = array_keys($timers);
1430          }
1431      }
1432      return $updates;
1433  }
1434  
1435  /**
1436   * This function receives a calendar event and returns the action associated with it, or null if there is none.
1437   *
1438   * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
1439   * is not displayed on the block.
1440   *
1441   * @param calendar_event $event
1442   * @param \core_calendar\action_factory $factory
1443   * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default).
1444   * @return \core_calendar\local\event\entities\action_interface|null
1445   */
1446  function mod_lesson_core_calendar_provide_event_action(calendar_event $event,
1447                                                         \core_calendar\action_factory $factory,
1448                                                         int $userid = 0) {
1449      global $DB, $CFG, $USER;
1450      require_once($CFG->dirroot . '/mod/lesson/locallib.php');
1451  
1452      if (!$userid) {
1453          $userid = $USER->id;
1454      }
1455  
1456      $cm = get_fast_modinfo($event->courseid, $userid)->instances['lesson'][$event->instance];
1457  
1458      if (!$cm->uservisible) {
1459          // The module is not visible to the user for any reason.
1460          return null;
1461      }
1462  
1463      $completion = new \completion_info($cm->get_course());
1464  
1465      $completiondata = $completion->get_data($cm, false, $userid);
1466  
1467      if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
1468          return null;
1469      }
1470  
1471      $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST));
1472  
1473      if ($lesson->count_user_retries($userid)) {
1474          // If the user has attempted the lesson then there is no further action for the user.
1475          return null;
1476      }
1477  
1478      // Apply overrides.
1479      $lesson->update_effective_access($userid);
1480  
1481      if (!$lesson->is_participant($userid)) {
1482          // If the user is not a participant then they have
1483          // no action to take. This will filter out the events for teachers.
1484          return null;
1485      }
1486  
1487      return $factory->create_instance(
1488          get_string('startlesson', 'lesson'),
1489          new \moodle_url('/mod/lesson/view.php', ['id' => $cm->id]),
1490          1,
1491          $lesson->is_accessible()
1492      );
1493  }
1494  
1495  /**
1496   * Add a get_coursemodule_info function in case any lesson type wants to add 'extra' information
1497   * for the course (see resource).
1498   *
1499   * Given a course_module object, this function returns any "extra" information that may be needed
1500   * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.
1501   *
1502   * @param stdClass $coursemodule The coursemodule object (record).
1503   * @return cached_cm_info An object on information that the courses
1504   *                        will know about (most noticeably, an icon).
1505   */
1506  function lesson_get_coursemodule_info($coursemodule) {
1507      global $DB;
1508  
1509      $dbparams = ['id' => $coursemodule->instance];
1510      $fields = 'id, name, intro, introformat, completionendreached, completiontimespent, available, deadline';
1511      if (!$lesson = $DB->get_record('lesson', $dbparams, $fields)) {
1512          return false;
1513      }
1514  
1515      $result = new cached_cm_info();
1516      $result->name = $lesson->name;
1517  
1518      if ($coursemodule->showdescription) {
1519          // Convert intro to html. Do not filter cached version, filters run at display time.
1520          $result->content = format_module_intro('lesson', $lesson, $coursemodule->id, false);
1521      }
1522  
1523      // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
1524      if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
1525          $result->customdata['customcompletionrules']['completionendreached'] = $lesson->completionendreached;
1526          $result->customdata['customcompletionrules']['completiontimespent'] = $lesson->completiontimespent;
1527      }
1528  
1529      // Populate some other values that can be used in calendar or on dashboard.
1530      if ($lesson->available) {
1531          $result->customdata['available'] = $lesson->available;
1532      }
1533      if ($lesson->deadline) {
1534          $result->customdata['deadline'] = $lesson->deadline;
1535      }
1536  
1537      return $result;
1538  }
1539  
1540  /**
1541   * Sets dynamic information about a course module
1542   *
1543   * This function is called from cm_info when displaying the module
1544   *
1545   * @param cm_info $cm
1546   */
1547  function mod_lesson_cm_info_dynamic(cm_info $cm) {
1548      global $USER;
1549  
1550      $cache = cache::make('mod_lesson', 'overrides');
1551      $override = $cache->get("{$cm->instance}_u_{$USER->id}");
1552  
1553      if (!$override) {
1554          $override = (object) [
1555              'available' => null,
1556              'deadline' => null,
1557          ];
1558      }
1559  
1560      // No need to look for group overrides if there are user overrides for both available and deadline.
1561      if (is_null($override->available) || is_null($override->deadline)) {
1562          $availables = [];
1563          $deadlines = [];
1564          $groupings = groups_get_user_groups($cm->course, $USER->id);
1565          foreach ($groupings[0] as $groupid) {
1566              $groupoverride = $cache->get("{$cm->instance}_g_{$groupid}");
1567              if (isset($groupoverride->available)) {
1568                  $availables[] = $groupoverride->available;
1569              }
1570              if (isset($groupoverride->deadline)) {
1571                  $deadlines[] = $groupoverride->deadline;
1572              }
1573          }
1574          // If there is a user override for a setting, ignore the group override.
1575          if (is_null($override->available) && count($availables)) {
1576              $override->available = min($availables);
1577          }
1578          if (is_null($override->deadline) && count($deadlines)) {
1579              if (in_array(0, $deadlines)) {
1580                  $override->deadline = 0;
1581              } else {
1582                  $override->deadline = max($deadlines);
1583              }
1584          }
1585      }
1586  
1587      // Populate some other values that can be used in calendar or on dashboard.
1588      if (!is_null($override->available)) {
1589          $cm->override_customdata('available', $override->available);
1590      }
1591      if (!is_null($override->deadline)) {
1592          $cm->override_customdata('deadline', $override->deadline);
1593      }
1594  }
1595  
1596  /**
1597   * Callback which returns human-readable strings describing the active completion custom rules for the module instance.
1598   *
1599   * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
1600   * @return array $descriptions the array of descriptions for the custom rules.
1601   */
1602  function mod_lesson_get_completion_active_rule_descriptions($cm) {
1603      // Values will be present in cm_info, and we assume these are up to date.
1604      if (empty($cm->customdata['customcompletionrules'])
1605          || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
1606          return [];
1607      }
1608  
1609      $descriptions = [];
1610      foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
1611          switch ($key) {
1612              case 'completionendreached':
1613                  if (!empty($val)) {
1614                      $descriptions[] = get_string('completionendreached_desc', 'lesson', $val);
1615                  }
1616                  break;
1617              case 'completiontimespent':
1618                  if (!empty($val)) {
1619                      $descriptions[] = get_string('completiontimespentdesc', 'lesson', format_time($val));
1620                  }
1621                  break;
1622              default:
1623                  break;
1624          }
1625      }
1626      return $descriptions;
1627  }
1628  
1629  /**
1630   * This function calculates the minimum and maximum cutoff values for the timestart of
1631   * the given event.
1632   *
1633   * It will return an array with two values, the first being the minimum cutoff value and
1634   * the second being the maximum cutoff value. Either or both values can be null, which
1635   * indicates there is no minimum or maximum, respectively.
1636   *
1637   * If a cutoff is required then the function must return an array containing the cutoff
1638   * timestamp and error string to display to the user if the cutoff value is violated.
1639   *
1640   * A minimum and maximum cutoff return value will look like:
1641   * [
1642   *     [1505704373, 'The due date must be after the start date'],
1643   *     [1506741172, 'The due date must be before the cutoff date']
1644   * ]
1645   *
1646   * @param calendar_event $event The calendar event to get the time range for
1647   * @param stdClass $instance The module instance to get the range from
1648   * @return array
1649   */
1650  function mod_lesson_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
1651      $mindate = null;
1652      $maxdate = null;
1653  
1654      if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
1655          // The start time of the open event can't be equal to or after the
1656          // close time of the lesson activity.
1657          if (!empty($instance->deadline)) {
1658              $maxdate = [
1659                  $instance->deadline,
1660                  get_string('openafterclose', 'lesson')
1661              ];
1662          }
1663      } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
1664          // The start time of the close event can't be equal to or earlier than the
1665          // open time of the lesson activity.
1666          if (!empty($instance->available)) {
1667              $mindate = [
1668                  $instance->available,
1669                  get_string('closebeforeopen', 'lesson')
1670              ];
1671          }
1672      }
1673  
1674      return [$mindate, $maxdate];
1675  }
1676  
1677  /**
1678   * This function will update the lesson module according to the
1679   * event that has been modified.
1680   *
1681   * It will set the available or deadline value of the lesson instance
1682   * according to the type of event provided.
1683   *
1684   * @throws \moodle_exception
1685   * @param \calendar_event $event
1686   * @param stdClass $lesson The module instance to get the range from
1687   */
1688  function mod_lesson_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $lesson) {
1689      global $DB;
1690  
1691      if (empty($event->instance) || $event->modulename != 'lesson') {
1692          return;
1693      }
1694  
1695      if ($event->instance != $lesson->id) {
1696          return;
1697      }
1698  
1699      if (!in_array($event->eventtype, [LESSON_EVENT_TYPE_OPEN, LESSON_EVENT_TYPE_CLOSE])) {
1700          return;
1701      }
1702  
1703      $courseid = $event->courseid;
1704      $modulename = $event->modulename;
1705      $instanceid = $event->instance;
1706      $modified = false;
1707  
1708      $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
1709      $context = context_module::instance($coursemodule->id);
1710  
1711      // The user does not have the capability to modify this activity.
1712      if (!has_capability('moodle/course:manageactivities', $context)) {
1713          return;
1714      }
1715  
1716      if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) {
1717          // If the event is for the lesson activity opening then we should
1718          // set the start time of the lesson activity to be the new start
1719          // time of the event.
1720          if ($lesson->available != $event->timestart) {
1721              $lesson->available = $event->timestart;
1722              $lesson->timemodified = time();
1723              $modified = true;
1724          }
1725      } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) {
1726          // If the event is for the lesson activity closing then we should
1727          // set the end time of the lesson activity to be the new start
1728          // time of the event.
1729          if ($lesson->deadline != $event->timestart) {
1730              $lesson->deadline = $event->timestart;
1731              $modified = true;
1732          }
1733      }
1734  
1735      if ($modified) {
1736          $lesson->timemodified = time();
1737          $DB->update_record('lesson', $lesson);
1738          $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
1739          $event->trigger();
1740      }
1741  }