[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/mod/lesson/ -> locallib.php (source)

   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   * Local library file for Lesson.  These are non-standard functions that are used
  20   * only by Lesson.
  21   *
  22   * @package mod_lesson
  23   * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
  25   **/
  26  
  27  /** Make sure this isn't being directly accessed */
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  /** Include the files that are required by this module */
  31  require_once($CFG->dirroot.'/course/moodleform_mod.php');
  32  require_once($CFG->dirroot . '/mod/lesson/lib.php');
  33  require_once($CFG->libdir . '/filelib.php');
  34  
  35  /** This page */
  36  define('LESSON_THISPAGE', 0);
  37  /** Next page -> any page not seen before */
  38  define("LESSON_UNSEENPAGE", 1);
  39  /** Next page -> any page not answered correctly */
  40  define("LESSON_UNANSWEREDPAGE", 2);
  41  /** Jump to Next Page */
  42  define("LESSON_NEXTPAGE", -1);
  43  /** End of Lesson */
  44  define("LESSON_EOL", -9);
  45  /** Jump to an unseen page within a branch and end of branch or end of lesson */
  46  define("LESSON_UNSEENBRANCHPAGE", -50);
  47  /** Jump to Previous Page */
  48  define("LESSON_PREVIOUSPAGE", -40);
  49  /** Jump to a random page within a branch and end of branch or end of lesson */
  50  define("LESSON_RANDOMPAGE", -60);
  51  /** Jump to a random Branch */
  52  define("LESSON_RANDOMBRANCH", -70);
  53  /** Cluster Jump */
  54  define("LESSON_CLUSTERJUMP", -80);
  55  /** Undefined */
  56  define("LESSON_UNDEFINED", -99);
  57  
  58  /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  59  define("LESSON_MAX_EVENT_LENGTH", "432000");
  60  
  61  
  62  //////////////////////////////////////////////////////////////////////////////////////
  63  /// Any other lesson functions go here.  Each of them must have a name that
  64  /// starts with lesson_
  65  
  66  /**
  67   * Checks to see if a LESSON_CLUSTERJUMP or
  68   * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
  69   *
  70   * This function is only executed when a teacher is
  71   * checking the navigation for a lesson.
  72   *
  73   * @param stdClass $lesson Id of the lesson that is to be checked.
  74   * @return boolean True or false.
  75   **/
  76  function lesson_display_teacher_warning($lesson) {
  77      global $DB;
  78  
  79      // get all of the lesson answers
  80      $params = array ("lessonid" => $lesson->id);
  81      if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
  82          // no answers, then not using cluster or unseen
  83          return false;
  84      }
  85      // just check for the first one that fulfills the requirements
  86      foreach ($lessonanswers as $lessonanswer) {
  87          if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
  88              return true;
  89          }
  90      }
  91  
  92      // if no answers use either of the two jumps
  93      return false;
  94  }
  95  
  96  /**
  97   * Interprets the LESSON_UNSEENBRANCHPAGE jump.
  98   *
  99   * will return the pageid of a random unseen page that is within a branch
 100   *
 101   * @param lesson $lesson
 102   * @param int $userid Id of the user.
 103   * @param int $pageid Id of the page from which we are jumping.
 104   * @return int Id of the next page.
 105   **/
 106  function lesson_unseen_question_jump($lesson, $user, $pageid) {
 107      global $DB;
 108  
 109      // get the number of retakes
 110      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
 111          $retakes = 0;
 112      }
 113  
 114      // get all the lesson_attempts aka what the user has seen
 115      if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
 116          foreach($viewedpages as $viewed) {
 117              $seenpages[] = $viewed->pageid;
 118          }
 119      } else {
 120          $seenpages = array();
 121      }
 122  
 123      // get the lesson pages
 124      $lessonpages = $lesson->load_all_pages();
 125  
 126      if ($pageid == LESSON_UNSEENBRANCHPAGE) {  // this only happens when a student leaves in the middle of an unseen question within a branch series
 127          $pageid = $seenpages[0];  // just change the pageid to the last page viewed inside the branch table
 128      }
 129  
 130      // go up the pages till branch table
 131      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 132          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 133              break;
 134          }
 135          $pageid = $lessonpages[$pageid]->prevpageid;
 136      }
 137  
 138      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 139  
 140      // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
 141      $unseen = array();
 142      foreach($pagesinbranch as $page) {
 143          if (!in_array($page->id, $seenpages)) {
 144              $unseen[] = $page->id;
 145          }
 146      }
 147  
 148      if(count($unseen) == 0) {
 149          if(isset($pagesinbranch)) {
 150              $temp = end($pagesinbranch);
 151              $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
 152          } else {
 153              // there are no pages inside the branch, so return the next page
 154              $nextpage = $lessonpages[$pageid]->nextpageid;
 155          }
 156          if ($nextpage == 0) {
 157              return LESSON_EOL;
 158          } else {
 159              return $nextpage;
 160          }
 161      } else {
 162          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 163      }
 164  }
 165  
 166  /**
 167   * Handles the unseen branch table jump.
 168   *
 169   * @param lesson $lesson
 170   * @param int $userid User id.
 171   * @return int Will return the page id of a branch table or end of lesson
 172   **/
 173  function lesson_unseen_branch_jump($lesson, $userid) {
 174      global $DB;
 175  
 176      if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
 177          $retakes = 0;
 178      }
 179  
 180      $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $retakes);
 181      if (!$seenbranches = $DB->get_records_select("lesson_branch", "lessonid = :lessonid AND userid = :userid AND retry = :retry", $params,
 182                  "timeseen DESC")) {
 183          print_error('cannotfindrecords', 'lesson');
 184      }
 185  
 186      // get the lesson pages
 187      $lessonpages = $lesson->load_all_pages();
 188  
 189      // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
 190      // which is the branch table that starts the unseenbranch function
 191      $seen = array();
 192      foreach ($seenbranches as $seenbranch) {
 193          if (!$seenbranch->flag) {
 194              $seen[$seenbranch->pageid] = $seenbranch->pageid;
 195          } else {
 196              $start = $seenbranch->pageid;
 197              break;
 198          }
 199      }
 200      // this function searches through the lesson pages to find all the branch tables
 201      // that follow the flagged branch table
 202      $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
 203      $branchtables = array();
 204      while ($pageid != 0) {  // grab all of the branch table till eol
 205          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 206              $branchtables[] = $lessonpages[$pageid]->id;
 207          }
 208          $pageid = $lessonpages[$pageid]->nextpageid;
 209      }
 210      $unseen = array();
 211      foreach ($branchtables as $branchtable) {
 212          // load all of the unseen branch tables into unseen
 213          if (!array_key_exists($branchtable, $seen)) {
 214              $unseen[] = $branchtable;
 215          }
 216      }
 217      if (count($unseen) > 0) {
 218          return $unseen[rand(0, count($unseen)-1)];  // returns a random page id for the next page
 219      } else {
 220          return LESSON_EOL;  // has viewed all of the branch tables
 221      }
 222  }
 223  
 224  /**
 225   * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
 226   *
 227   * @param lesson $lesson
 228   * @param int $pageid The id of the page that we are jumping from (?)
 229   * @return int The pageid of a random page that is within a branch table
 230   **/
 231  function lesson_random_question_jump($lesson, $pageid) {
 232      global $DB;
 233  
 234      // get the lesson pages
 235      $params = array ("lessonid" => $lesson->id);
 236      if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
 237          print_error('cannotfindpages', 'lesson');
 238      }
 239  
 240      // go up the pages till branch table
 241      while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
 242  
 243          if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
 244              break;
 245          }
 246          $pageid = $lessonpages[$pageid]->prevpageid;
 247      }
 248  
 249      // get the pages within the branch
 250      $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
 251  
 252      if(count($pagesinbranch) == 0) {
 253          // there are no pages inside the branch, so return the next page
 254          return $lessonpages[$pageid]->nextpageid;
 255      } else {
 256          return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id;  // returns a random page id for the next page
 257      }
 258  }
 259  
 260  /**
 261   * Calculates a user's grade for a lesson.
 262   *
 263   * @param object $lesson The lesson that the user is taking.
 264   * @param int $retries The attempt number.
 265   * @param int $userid Id of the user (optional, default current user).
 266   * @return object { nquestions => number of questions answered
 267                      attempts => number of question attempts
 268                      total => max points possible
 269                      earned => points earned by student
 270                      grade => calculated percentage grade
 271                      nmanual => number of manually graded questions
 272                      manualpoints => point value for manually graded questions }
 273   */
 274  function lesson_grade($lesson, $ntries, $userid = 0) {
 275      global $USER, $DB;
 276  
 277      if (empty($userid)) {
 278          $userid = $USER->id;
 279      }
 280  
 281      // Zero out everything
 282      $ncorrect     = 0;
 283      $nviewed      = 0;
 284      $score        = 0;
 285      $nmanual      = 0;
 286      $manualpoints = 0;
 287      $thegrade     = 0;
 288      $nquestions   = 0;
 289      $total        = 0;
 290      $earned       = 0;
 291  
 292      $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
 293      if ($useranswers = $DB->get_records_select("lesson_attempts",  "lessonid = :lessonid AND
 294              userid = :userid AND retry = :retry", $params, "timeseen")) {
 295          // group each try with its page
 296          $attemptset = array();
 297          foreach ($useranswers as $useranswer) {
 298              $attemptset[$useranswer->pageid][] = $useranswer;
 299          }
 300  
 301          // Drop all attempts that go beyond max attempts for the lesson
 302          foreach ($attemptset as $key => $set) {
 303              $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
 304          }
 305  
 306          // get only the pages and their answers that the user answered
 307          list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
 308          array_unshift($parameters, $lesson->id);
 309          $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
 310          $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
 311  
 312          // Number of pages answered
 313          $nquestions = count($pages);
 314  
 315          foreach ($attemptset as $attempts) {
 316              $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
 317              if ($lesson->custom) {
 318                  $attempt = end($attempts);
 319                  // If essay question, handle it, otherwise add to score
 320                  if ($page->requires_manual_grading()) {
 321                      $useranswerobj = unserialize($attempt->useranswer);
 322                      if (isset($useranswerobj->score)) {
 323                          $earned += $useranswerobj->score;
 324                      }
 325                      $nmanual++;
 326                      $manualpoints += $answers[$attempt->answerid]->score;
 327                  } else if (!empty($attempt->answerid)) {
 328                      $earned += $page->earned_score($answers, $attempt);
 329                  }
 330              } else {
 331                  foreach ($attempts as $attempt) {
 332                      $earned += $attempt->correct;
 333                  }
 334                  $attempt = end($attempts); // doesn't matter which one
 335                  // If essay question, increase numbers
 336                  if ($page->requires_manual_grading()) {
 337                      $nmanual++;
 338                      $manualpoints++;
 339                  }
 340              }
 341              // Number of times answered
 342              $nviewed += count($attempts);
 343          }
 344  
 345          if ($lesson->custom) {
 346              $bestscores = array();
 347              // Find the highest possible score per page to get our total
 348              foreach ($answers as $answer) {
 349                  if(!isset($bestscores[$answer->pageid])) {
 350                      $bestscores[$answer->pageid] = $answer->score;
 351                  } else if ($bestscores[$answer->pageid] < $answer->score) {
 352                      $bestscores[$answer->pageid] = $answer->score;
 353                  }
 354              }
 355              $total = array_sum($bestscores);
 356          } else {
 357              // Check to make sure the student has answered the minimum questions
 358              if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
 359                  // Nope, increase number viewed by the amount of unanswered questions
 360                  $total =  $nviewed + ($lesson->minquestions - $nquestions);
 361              } else {
 362                  $total = $nviewed;
 363              }
 364          }
 365      }
 366  
 367      if ($total) { // not zero
 368          $thegrade = round(100 * $earned / $total, 5);
 369      }
 370  
 371      // Build the grade information object
 372      $gradeinfo               = new stdClass;
 373      $gradeinfo->nquestions   = $nquestions;
 374      $gradeinfo->attempts     = $nviewed;
 375      $gradeinfo->total        = $total;
 376      $gradeinfo->earned       = $earned;
 377      $gradeinfo->grade        = $thegrade;
 378      $gradeinfo->nmanual      = $nmanual;
 379      $gradeinfo->manualpoints = $manualpoints;
 380  
 381      return $gradeinfo;
 382  }
 383  
 384  /**
 385   * Determines if a user can view the left menu.  The determining factor
 386   * is whether a user has a grade greater than or equal to the lesson setting
 387   * of displayleftif
 388   *
 389   * @param object $lesson Lesson object of the current lesson
 390   * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
 391   **/
 392  function lesson_displayleftif($lesson) {
 393      global $CFG, $USER, $DB;
 394  
 395      if (!empty($lesson->displayleftif)) {
 396          // get the current user's max grade for this lesson
 397          $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
 398          if ($maxgrade = $DB->get_record_sql('SELECT userid, MAX(grade) AS maxgrade FROM {lesson_grades} WHERE userid = :userid AND lessonid = :lessonid GROUP BY userid', $params)) {
 399              if ($maxgrade->maxgrade < $lesson->displayleftif) {
 400                  return 0;  // turn off the displayleft
 401              }
 402          } else {
 403              return 0; // no grades
 404          }
 405      }
 406  
 407      // if we get to here, keep the original state of displayleft lesson setting
 408      return $lesson->displayleft;
 409  }
 410  
 411  /**
 412   *
 413   * @param $cm
 414   * @param $lesson
 415   * @param $page
 416   * @return unknown_type
 417   */
 418  function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
 419      $bc = lesson_menu_block_contents($cm->id, $lesson);
 420      if (!empty($bc)) {
 421          $regions = $page->blocks->get_regions();
 422          $firstregion = reset($regions);
 423          $page->blocks->add_fake_block($bc, $firstregion);
 424      }
 425  
 426      $bc = lesson_mediafile_block_contents($cm->id, $lesson);
 427      if (!empty($bc)) {
 428          $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 429      }
 430  
 431      if (!empty($timer)) {
 432          $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
 433          if (!empty($bc)) {
 434              $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
 435          }
 436      }
 437  }
 438  
 439  /**
 440   * If there is a media file associated with this
 441   * lesson, return a block_contents that displays it.
 442   *
 443   * @param int $cmid Course Module ID for this lesson
 444   * @param object $lesson Full lesson record object
 445   * @return block_contents
 446   **/
 447  function lesson_mediafile_block_contents($cmid, $lesson) {
 448      global $OUTPUT;
 449      if (empty($lesson->mediafile)) {
 450          return null;
 451      }
 452  
 453      $options = array();
 454      $options['menubar'] = 0;
 455      $options['location'] = 0;
 456      $options['left'] = 5;
 457      $options['top'] = 5;
 458      $options['scrollbars'] = 1;
 459      $options['resizable'] = 1;
 460      $options['width'] = $lesson->mediawidth;
 461      $options['height'] = $lesson->mediaheight;
 462  
 463      $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
 464      $action = new popup_action('click', $link, 'lessonmediafile', $options);
 465      $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
 466  
 467      $bc = new block_contents();
 468      $bc->title = get_string('linkedmedia', 'lesson');
 469      $bc->attributes['class'] = 'mediafile';
 470      $bc->content = $content;
 471  
 472      return $bc;
 473  }
 474  
 475  /**
 476   * If a timed lesson and not a teacher, then
 477   * return a block_contents containing the clock.
 478   *
 479   * @param int $cmid Course Module ID for this lesson
 480   * @param object $lesson Full lesson record object
 481   * @param object $timer Full timer record object
 482   * @return block_contents
 483   **/
 484  function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
 485      // Display for timed lessons and for students only
 486      $context = context_module::instance($cmid);
 487      if(!$lesson->timed || has_capability('mod/lesson:manage', $context)) {
 488          return null;
 489      }
 490  
 491      $content = '<div id="lesson-timer">';
 492      $content .=  $lesson->time_remaining($timer->starttime);
 493      $content .= '</div>';
 494  
 495      $clocksettings = array('starttime'=>$timer->starttime, 'servertime'=>time(),'testlength'=>($lesson->maxtime * 60));
 496      $page->requires->data_for_js('clocksettings', $clocksettings, true);
 497      $page->requires->strings_for_js(array('timeisup'), 'lesson');
 498      $page->requires->js('/mod/lesson/timer.js');
 499      $page->requires->js_init_call('show_clock');
 500  
 501      $bc = new block_contents();
 502      $bc->title = get_string('timeremaining', 'lesson');
 503      $bc->attributes['class'] = 'clock block';
 504      $bc->content = $content;
 505  
 506      return $bc;
 507  }
 508  
 509  /**
 510   * If left menu is turned on, then this will
 511   * print the menu in a block
 512   *
 513   * @param int $cmid Course Module ID for this lesson
 514   * @param lesson $lesson Full lesson record object
 515   * @return void
 516   **/
 517  function lesson_menu_block_contents($cmid, $lesson) {
 518      global $CFG, $DB;
 519  
 520      if (!$lesson->displayleft) {
 521          return null;
 522      }
 523  
 524      $pages = $lesson->load_all_pages();
 525      foreach ($pages as $page) {
 526          if ((int)$page->prevpageid === 0) {
 527              $pageid = $page->id;
 528              break;
 529          }
 530      }
 531      $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
 532  
 533      if (!$pageid || !$pages) {
 534          return null;
 535      }
 536  
 537      $content = '<a href="#maincontent" class="skip">'.get_string('skip', 'lesson')."</a>\n<div class=\"menuwrapper\">\n<ul>\n";
 538  
 539      while ($pageid != 0) {
 540          $page = $pages[$pageid];
 541  
 542          // Only process branch tables with display turned on
 543          if ($page->displayinmenublock && $page->display) {
 544              if ($page->id == $currentpageid) {
 545                  $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
 546              } else {
 547                  $content .= "<li class=\"notselected\"><a href=\"$CFG->wwwroot/mod/lesson/view.php?id=$cmid&amp;pageid=$page->id\">".format_string($page->title,true)."</a></li>\n";
 548              }
 549  
 550          }
 551          $pageid = $page->nextpageid;
 552      }
 553      $content .= "</ul>\n</div>\n";
 554  
 555      $bc = new block_contents();
 556      $bc->title = get_string('lessonmenu', 'lesson');
 557      $bc->attributes['class'] = 'menu block';
 558      $bc->content = $content;
 559  
 560      return $bc;
 561  }
 562  
 563  /**
 564   * Adds header buttons to the page for the lesson
 565   *
 566   * @param object $cm
 567   * @param object $context
 568   * @param bool $extraeditbuttons
 569   * @param int $lessonpageid
 570   */
 571  function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
 572      global $CFG, $PAGE, $OUTPUT;
 573      if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
 574          if ($lessonpageid === null) {
 575              print_error('invalidpageid', 'lesson');
 576          }
 577          if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
 578              $url = new moodle_url('/mod/lesson/editpage.php', array('id'=>$cm->id, 'pageid'=>$lessonpageid, 'edit'=>1));
 579              $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
 580          }
 581      }
 582  }
 583  
 584  /**
 585   * This is a function used to detect media types and generate html code.
 586   *
 587   * @global object $CFG
 588   * @global object $PAGE
 589   * @param object $lesson
 590   * @param object $context
 591   * @return string $code the html code of media
 592   */
 593  function lesson_get_media_html($lesson, $context) {
 594      global $CFG, $PAGE, $OUTPUT;
 595      require_once("$CFG->libdir/resourcelib.php");
 596  
 597      // get the media file link
 598      if (strpos($lesson->mediafile, '://') !== false) {
 599          $url = new moodle_url($lesson->mediafile);
 600      } else {
 601          // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
 602          $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
 603      }
 604      $title = $lesson->mediafile;
 605  
 606      $clicktoopen = html_writer::link($url, get_string('download'));
 607  
 608      $mimetype = resourcelib_guess_url_mimetype($url);
 609  
 610      $extension = resourcelib_get_extension($url->out(false));
 611  
 612      $mediarenderer = $PAGE->get_renderer('core', 'media');
 613      $embedoptions = array(
 614          core_media::OPTION_TRUSTED => true,
 615          core_media::OPTION_BLOCK => true
 616      );
 617  
 618      // find the correct type and print it out
 619      if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) {  // It's an image
 620          $code = resourcelib_embed_image($url, $title);
 621  
 622      } else if ($mediarenderer->can_embed_url($url, $embedoptions)) {
 623          // Media (audio/video) file.
 624          $code = $mediarenderer->embed_url($url, $title, 0, 0, $embedoptions);
 625  
 626      } else {
 627          // anything else - just try object tag enlarged as much as possible
 628          $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
 629      }
 630  
 631      return $code;
 632  }
 633  
 634  
 635  /**
 636   * Abstract class that page type's MUST inherit from.
 637   *
 638   * This is the abstract class that ALL add page type forms must extend.
 639   * You will notice that all but two of the methods this class contains are final.
 640   * Essentially the only thing that extending classes can do is extend custom_definition.
 641   * OR if it has a special requirement on creation it can extend construction_override
 642   *
 643   * @abstract
 644   * @copyright  2009 Sam Hemelryk
 645   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 646   */
 647  abstract class lesson_add_page_form_base extends moodleform {
 648  
 649      /**
 650       * This is the classic define that is used to identify this pagetype.
 651       * Will be one of LESSON_*
 652       * @var int
 653       */
 654      public $qtype;
 655  
 656      /**
 657       * The simple string that describes the page type e.g. truefalse, multichoice
 658       * @var string
 659       */
 660      public $qtypestring;
 661  
 662      /**
 663       * An array of options used in the htmleditor
 664       * @var array
 665       */
 666      protected $editoroptions = array();
 667  
 668      /**
 669       * True if this is a standard page of false if it does something special.
 670       * Questions are standard pages, branch tables are not
 671       * @var bool
 672       */
 673      protected $standard = true;
 674  
 675      /**
 676       * Each page type can and should override this to add any custom elements to
 677       * the basic form that they want
 678       */
 679      public function custom_definition() {}
 680  
 681      /**
 682       * Used to determine if this is a standard page or a special page
 683       * @return bool
 684       */
 685      public final function is_standard() {
 686          return (bool)$this->standard;
 687      }
 688  
 689      /**
 690       * Add the required basic elements to the form.
 691       *
 692       * This method adds the basic elements to the form including title and contents
 693       * and then calls custom_definition();
 694       */
 695      public final function definition() {
 696          $mform = $this->_form;
 697          $editoroptions = $this->_customdata['editoroptions'];
 698  
 699          $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
 700  
 701          $mform->addElement('hidden', 'id');
 702          $mform->setType('id', PARAM_INT);
 703  
 704          $mform->addElement('hidden', 'pageid');
 705          $mform->setType('pageid', PARAM_INT);
 706  
 707          if ($this->standard === true) {
 708              $mform->addElement('hidden', 'qtype');
 709              $mform->setType('qtype', PARAM_INT);
 710  
 711              $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
 712              $mform->setType('title', PARAM_TEXT);
 713              $mform->addRule('title', get_string('required'), 'required', null, 'client');
 714  
 715              $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
 716              $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
 717              $mform->setType('contents_editor', PARAM_RAW);
 718              $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
 719          }
 720  
 721          $this->custom_definition();
 722  
 723          if ($this->_customdata['edit'] === true) {
 724              $mform->addElement('hidden', 'edit', 1);
 725              $mform->setType('edit', PARAM_BOOL);
 726              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
 727          } else if ($this->qtype === 'questiontype') {
 728              $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
 729          } else {
 730              $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
 731          }
 732      }
 733  
 734      /**
 735       * Convenience function: Adds a jumpto select element
 736       *
 737       * @param string $name
 738       * @param string|null $label
 739       * @param int $selected The page to select by default
 740       */
 741      protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
 742          $title = get_string("jump", "lesson");
 743          if ($label === null) {
 744              $label = $title;
 745          }
 746          if (is_int($name)) {
 747              $name = "jumpto[$name]";
 748          }
 749          $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
 750          $this->_form->setDefault($name, $selected);
 751          $this->_form->addHelpButton($name, 'jumps', 'lesson');
 752      }
 753  
 754      /**
 755       * Convenience function: Adds a score input element
 756       *
 757       * @param string $name
 758       * @param string|null $label
 759       * @param mixed $value The default value
 760       */
 761      protected final function add_score($name, $label=null, $value=null) {
 762          if ($label === null) {
 763              $label = get_string("score", "lesson");
 764          }
 765  
 766          if (is_int($name)) {
 767              $name = "score[$name]";
 768          }
 769          $this->_form->addElement('text', $name, $label, array('size'=>5));
 770          $this->_form->setType($name, PARAM_INT);
 771          if ($value !== null) {
 772              $this->_form->setDefault($name, $value);
 773          }
 774          $this->_form->addHelpButton($name, 'score', 'lesson');
 775  
 776          // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
 777          if (!$this->_customdata['lesson']->custom) {
 778              $this->_form->freeze($name);
 779          }
 780      }
 781  
 782      /**
 783       * Convenience function: Adds an answer editor
 784       *
 785       * @param int $count The count of the element to add
 786       * @param string $label, null means default
 787       * @param bool $required
 788       * @return void
 789       */
 790      protected final function add_answer($count, $label = null, $required = false) {
 791          if ($label === null) {
 792              $label = get_string('answer', 'lesson');
 793          }
 794  
 795          if ($this->qtype != 'multichoice' && $this->qtype != 'matching') {
 796              $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
 797                      array('rows' => '4', 'columns' => '80'), array('noclean' => true));
 798              $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_MOODLE));
 799          } else {
 800              $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
 801                      array('rows' => '4', 'columns' => '80'),
 802                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
 803              $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
 804              $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
 805          }
 806  
 807          if ($required) {
 808              $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
 809          }
 810      }
 811      /**
 812       * Convenience function: Adds an response editor
 813       *
 814       * @param int $count The count of the element to add
 815       * @param string $label, null means default
 816       * @param bool $required
 817       * @return void
 818       */
 819      protected final function add_response($count, $label = null, $required = false) {
 820          if ($label === null) {
 821              $label = get_string('response', 'lesson');
 822          }
 823          $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
 824                   array('rows' => '4', 'columns' => '80'),
 825                   array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
 826          $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
 827          $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
 828  
 829          if ($required) {
 830              $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
 831          }
 832      }
 833  
 834      /**
 835       * A function that gets called upon init of this object by the calling script.
 836       *
 837       * This can be used to process an immediate action if required. Currently it
 838       * is only used in special cases by non-standard page types.
 839       *
 840       * @return bool
 841       */
 842      public function construction_override($pageid, lesson $lesson) {
 843          return true;
 844      }
 845  }
 846  
 847  
 848  
 849  /**
 850   * Class representation of a lesson
 851   *
 852   * This class is used the interact with, and manage a lesson once instantiated.
 853   * If you need to fetch a lesson object you can do so by calling
 854   *
 855   * <code>
 856   * lesson::load($lessonid);
 857   * // or
 858   * $lessonrecord = $DB->get_record('lesson', $lessonid);
 859   * $lesson = new lesson($lessonrecord);
 860   * </code>
 861   *
 862   * The class itself extends lesson_base as all classes within the lesson module should
 863   *
 864   * These properties are from the database
 865   * @property int $id The id of this lesson
 866   * @property int $course The ID of the course this lesson belongs to
 867   * @property string $name The name of this lesson
 868   * @property int $practice Flag to toggle this as a practice lesson
 869   * @property int $modattempts Toggle to allow the user to go back and review answers
 870   * @property int $usepassword Toggle the use of a password for entry
 871   * @property string $password The password to require users to enter
 872   * @property int $dependency ID of another lesson this lesson is dependent on
 873   * @property string $conditions Conditions of the lesson dependency
 874   * @property int $grade The maximum grade a user can achieve (%)
 875   * @property int $custom Toggle custom scoring on or off
 876   * @property int $ongoing Toggle display of an ongoing score
 877   * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
 878   * @property int $maxanswers The max number of answers or branches
 879   * @property int $maxattempts The maximum number of attempts a user can record
 880   * @property int $review Toggle use or wrong answer review button
 881   * @property int $nextpagedefault Override the default next page
 882   * @property int $feedback Toggles display of default feedback
 883   * @property int $minquestions Sets a minimum value of pages seen when calculating grades
 884   * @property int $maxpages Maximum number of pages this lesson can contain
 885   * @property int $retake Flag to allow users to retake a lesson
 886   * @property int $activitylink Relate this lesson to another lesson
 887   * @property string $mediafile File to pop up to or webpage to display
 888   * @property int $mediaheight Sets the height of the media file popup
 889   * @property int $mediawidth Sets the width of the media file popup
 890   * @property int $mediaclose Toggle display of a media close button
 891   * @property int $slideshow Flag for whether branch pages should be shown as slideshows
 892   * @property int $width Width of slideshow
 893   * @property int $height Height of slideshow
 894   * @property string $bgcolor Background colour of slideshow
 895   * @property int $displayleft Display a left menu
 896   * @property int $displayleftif Sets the condition on which the left menu is displayed
 897   * @property int $progressbar Flag to toggle display of a lesson progress bar
 898   * @property int $highscores Flag to toggle collection of high scores
 899   * @property int $maxhighscores Number of high scores to limit to
 900   * @property int $available Timestamp of when this lesson becomes available
 901   * @property int $deadline Timestamp of when this lesson is no longer available
 902   * @property int $timemodified Timestamp when lesson was last modified
 903   *
 904   * These properties are calculated
 905   * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
 906   * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
 907   *
 908   * @copyright  2009 Sam Hemelryk
 909   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 910   */
 911  class lesson extends lesson_base {
 912  
 913      /**
 914       * The id of the first page (where prevpageid = 0) gets set and retrieved by
 915       * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
 916       * @var int
 917       */
 918      protected $firstpageid = null;
 919      /**
 920       * The id of the last page (where nextpageid = 0) gets set and retrieved by
 921       * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
 922       * @var int
 923       */
 924      protected $lastpageid = null;
 925      /**
 926       * An array used to cache the pages associated with this lesson after the first
 927       * time they have been loaded.
 928       * A note to developers: If you are going to be working with MORE than one or
 929       * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
 930       * in order to save excess database queries.
 931       * @var array An array of lesson_page objects
 932       */
 933      protected $pages = array();
 934      /**
 935       * Flag that gets set to true once all of the pages associated with the lesson
 936       * have been loaded.
 937       * @var bool
 938       */
 939      protected $loadedallpages = false;
 940  
 941      /**
 942       * Simply generates a lesson object given an array/object of properties
 943       * Overrides {@see lesson_base->create()}
 944       * @static
 945       * @param object|array $properties
 946       * @return lesson
 947       */
 948      public static function create($properties) {
 949          return new lesson($properties);
 950      }
 951  
 952      /**
 953       * Generates a lesson object from the database given its id
 954       * @static
 955       * @param int $lessonid
 956       * @return lesson
 957       */
 958      public static function load($lessonid) {
 959          global $DB;
 960  
 961          if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
 962              print_error('invalidcoursemodule');
 963          }
 964          return new lesson($lesson);
 965      }
 966  
 967      /**
 968       * Deletes this lesson from the database
 969       */
 970      public function delete() {
 971          global $CFG, $DB;
 972          require_once($CFG->libdir.'/gradelib.php');
 973          require_once($CFG->dirroot.'/calendar/lib.php');
 974  
 975          $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
 976          $context = context_module::instance($cm->id);
 977  
 978          $DB->delete_records("lesson", array("id"=>$this->properties->id));
 979          $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
 980          $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
 981          $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
 982          $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
 983          $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
 984          $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
 985          $DB->delete_records("lesson_high_scores", array("lessonid"=>$this->properties->id));
 986          if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
 987              foreach($events as $event) {
 988                  $event = calendar_event::load($event);
 989                  $event->delete();
 990              }
 991          }
 992  
 993          // Delete files associated with this module.
 994          $fs = get_file_storage();
 995          $fs->delete_area_files($context->id);
 996  
 997          grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
 998          return true;
 999      }
1000  
1001      /**
1002       * Fetches messages from the session that may have been set in previous page
1003       * actions.
1004       *
1005       * <code>
1006       * // Do not call this method directly instead use
1007       * $lesson->messages;
1008       * </code>
1009       *
1010       * @return array
1011       */
1012      protected function get_messages() {
1013          global $SESSION;
1014  
1015          $messages = array();
1016          if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1017              $messages = $SESSION->lesson_messages[$this->properties->id];
1018              unset($SESSION->lesson_messages[$this->properties->id]);
1019          }
1020  
1021          return $messages;
1022      }
1023  
1024      /**
1025       * Get all of the attempts for the current user.
1026       *
1027       * @param int $retries
1028       * @param bool $correct Optional: only fetch correct attempts
1029       * @param int $pageid Optional: only fetch attempts at the given page
1030       * @param int $userid Optional: defaults to the current user if not set
1031       * @return array|false
1032       */
1033      public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1034          global $USER, $DB;
1035          $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1036          if ($correct) {
1037              $params['correct'] = 1;
1038          }
1039          if ($pageid !== null) {
1040              $params['pageid'] = $pageid;
1041          }
1042          if ($userid === null) {
1043              $params['userid'] = $USER->id;
1044          }
1045          return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
1046      }
1047  
1048      /**
1049       * Returns the first page for the lesson or false if there isn't one.
1050       *
1051       * This method should be called via the magic method __get();
1052       * <code>
1053       * $firstpage = $lesson->firstpage;
1054       * </code>
1055       *
1056       * @return lesson_page|bool Returns the lesson_page specialised object or false
1057       */
1058      protected function get_firstpage() {
1059          $pages = $this->load_all_pages();
1060          if (count($pages) > 0) {
1061              foreach ($pages as $page) {
1062                  if ((int)$page->prevpageid === 0) {
1063                      return $page;
1064                  }
1065              }
1066          }
1067          return false;
1068      }
1069  
1070      /**
1071       * Returns the last page for the lesson or false if there isn't one.
1072       *
1073       * This method should be called via the magic method __get();
1074       * <code>
1075       * $lastpage = $lesson->lastpage;
1076       * </code>
1077       *
1078       * @return lesson_page|bool Returns the lesson_page specialised object or false
1079       */
1080      protected function get_lastpage() {
1081          $pages = $this->load_all_pages();
1082          if (count($pages) > 0) {
1083              foreach ($pages as $page) {
1084                  if ((int)$page->nextpageid === 0) {
1085                      return $page;
1086                  }
1087              }
1088          }
1089          return false;
1090      }
1091  
1092      /**
1093       * Returns the id of the first page of this lesson. (prevpageid = 0)
1094       * @return int
1095       */
1096      protected function get_firstpageid() {
1097          global $DB;
1098          if ($this->firstpageid == null) {
1099              if (!$this->loadedallpages) {
1100                  $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
1101                  if (!$firstpageid) {
1102                      print_error('cannotfindfirstpage', 'lesson');
1103                  }
1104                  $this->firstpageid = $firstpageid;
1105              } else {
1106                  $firstpage = $this->get_firstpage();
1107                  $this->firstpageid = $firstpage->id;
1108              }
1109          }
1110          return $this->firstpageid;
1111      }
1112  
1113      /**
1114       * Returns the id of the last page of this lesson. (nextpageid = 0)
1115       * @return int
1116       */
1117      public function get_lastpageid() {
1118          global $DB;
1119          if ($this->lastpageid == null) {
1120              if (!$this->loadedallpages) {
1121                  $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1122                  if (!$lastpageid) {
1123                      print_error('cannotfindlastpage', 'lesson');
1124                  }
1125                  $this->lastpageid = $lastpageid;
1126              } else {
1127                  $lastpageid = $this->get_lastpage();
1128                  $this->lastpageid = $lastpageid->id;
1129              }
1130          }
1131  
1132          return $this->lastpageid;
1133      }
1134  
1135       /**
1136       * Gets the next page id to display after the one that is provided.
1137       * @param int $nextpageid
1138       * @return bool
1139       */
1140      public function get_next_page($nextpageid) {
1141          global $USER, $DB;
1142          $allpages = $this->load_all_pages();
1143          if ($this->properties->nextpagedefault) {
1144              // in Flash Card mode...first get number of retakes
1145              $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
1146              shuffle($allpages);
1147              $found = false;
1148              if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
1149                  foreach ($allpages as $nextpage) {
1150                      if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
1151                          $found = true;
1152                          break;
1153                      }
1154                  }
1155              } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
1156                  foreach ($allpages as $nextpage) {
1157                      if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
1158                          $found = true;
1159                          break;
1160                      }
1161                  }
1162              }
1163              if ($found) {
1164                  if ($this->properties->maxpages) {
1165                      // check number of pages viewed (in the lesson)
1166                      if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
1167                          return LESSON_EOL;
1168                      }
1169                  }
1170                  return $nextpage->id;
1171              }
1172          }
1173          // In a normal lesson mode
1174          foreach ($allpages as $nextpage) {
1175              if ((int)$nextpage->id === (int)$nextpageid) {
1176                  return $nextpage->id;
1177              }
1178          }
1179          return LESSON_EOL;
1180      }
1181  
1182      /**
1183       * Sets a message against the session for this lesson that will displayed next
1184       * time the lesson processes messages
1185       *
1186       * @param string $message
1187       * @param string $class
1188       * @param string $align
1189       * @return bool
1190       */
1191      public function add_message($message, $class="notifyproblem", $align='center') {
1192          global $SESSION;
1193  
1194          if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
1195              $SESSION->lesson_messages = array();
1196              $SESSION->lesson_messages[$this->properties->id] = array();
1197          } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1198              $SESSION->lesson_messages[$this->properties->id] = array();
1199          }
1200  
1201          $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
1202  
1203          return true;
1204      }
1205  
1206      /**
1207       * Check if the lesson is accessible at the present time
1208       * @return bool True if the lesson is accessible, false otherwise
1209       */
1210      public function is_accessible() {
1211          $available = $this->properties->available;
1212          $deadline = $this->properties->deadline;
1213          return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
1214      }
1215  
1216      /**
1217       * Starts the lesson time for the current user
1218       * @return bool Returns true
1219       */
1220      public function start_timer() {
1221          global $USER, $DB;
1222  
1223          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
1224              false, MUST_EXIST);
1225  
1226          // Trigger lesson started event.
1227          $event = \mod_lesson\event\lesson_started::create(array(
1228              'objectid' => $this->properties()->id,
1229              'context' => context_module::instance($cm->id),
1230              'courseid' => $this->properties()->course
1231          ));
1232          $event->trigger();
1233  
1234          $USER->startlesson[$this->properties->id] = true;
1235          $startlesson = new stdClass;
1236          $startlesson->lessonid = $this->properties->id;
1237          $startlesson->userid = $USER->id;
1238          $startlesson->starttime = time();
1239          $startlesson->lessontime = time();
1240          $DB->insert_record('lesson_timer', $startlesson);
1241          if ($this->properties->timed) {
1242              $this->add_message(get_string('maxtimewarning', 'lesson', $this->properties->maxtime), 'center');
1243          }
1244          return true;
1245      }
1246  
1247      /**
1248       * Updates the timer to the current time and returns the new timer object
1249       * @param bool $restart If set to true the timer is restarted
1250       * @param bool $continue If set to true AND $restart=true then the timer
1251       *                        will continue from a previous attempt
1252       * @return stdClass The new timer
1253       */
1254      public function update_timer($restart=false, $continue=false) {
1255          global $USER, $DB;
1256          // clock code
1257          // get time information for this user
1258          if (!$timer = $DB->get_records('lesson_timer', array ("lessonid" => $this->properties->id, "userid" => $USER->id), 'starttime DESC', '*', 0, 1)) {
1259              print_error('cannotfindtimer', 'lesson');
1260          } else {
1261              $timer = current($timer); // this will get the latest start time record
1262          }
1263  
1264          if ($restart) {
1265              if ($continue) {
1266                  // continue a previous test, need to update the clock  (think this option is disabled atm)
1267                  $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
1268              } else {
1269                  // starting over, so reset the clock
1270                  $timer->starttime = time();
1271              }
1272          }
1273  
1274          $timer->lessontime = time();
1275          $DB->update_record('lesson_timer', $timer);
1276          return $timer;
1277      }
1278  
1279      /**
1280       * Updates the timer to the current time then stops it by unsetting the user var
1281       * @return bool Returns true
1282       */
1283      public function stop_timer() {
1284          global $USER, $DB;
1285          unset($USER->startlesson[$this->properties->id]);
1286  
1287          $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
1288              false, MUST_EXIST);
1289  
1290          // Trigger lesson ended event.
1291          $event = \mod_lesson\event\lesson_ended::create(array(
1292              'objectid' => $this->properties()->id,
1293              'context' => context_module::instance($cm->id),
1294              'courseid' => $this->properties()->course
1295          ));
1296          $event->trigger();
1297  
1298          return $this->update_timer(false, false);
1299      }
1300  
1301      /**
1302       * Checks to see if the lesson has pages
1303       */
1304      public function has_pages() {
1305          global $DB;
1306          $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
1307          return ($pagecount>0);
1308      }
1309  
1310      /**
1311       * Returns the link for the related activity
1312       * @return array|false
1313       */
1314      public function link_for_activitylink() {
1315          global $DB;
1316          $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
1317          if ($module) {
1318              $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
1319              if ($modname) {
1320                  $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
1321                  if ($instancename) {
1322                      return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php', array('id'=>$this->properties->activitylink)),
1323                          get_string('activitylinkname', 'lesson', $instancename),
1324                          array('class'=>'centerpadded lessonbutton standardbutton'));
1325                  }
1326              }
1327          }
1328          return '';
1329      }
1330  
1331      /**
1332       * Loads the requested page.
1333       *
1334       * This function will return the requested page id as either a specialised
1335       * lesson_page object OR as a generic lesson_page.
1336       * If the page has been loaded previously it will be returned from the pages
1337       * array, otherwise it will be loaded from the database first
1338       *
1339       * @param int $pageid
1340       * @return lesson_page A lesson_page object or an object that extends it
1341       */
1342      public function load_page($pageid) {
1343          if (!array_key_exists($pageid, $this->pages)) {
1344              $manager = lesson_page_type_manager::get($this);
1345              $this->pages[$pageid] = $manager->load_page($pageid, $this);
1346          }
1347          return $this->pages[$pageid];
1348      }
1349  
1350      /**
1351       * Loads ALL of the pages for this lesson
1352       *
1353       * @return array An array containing all pages from this lesson
1354       */
1355      public function load_all_pages() {
1356          if (!$this->loadedallpages) {
1357              $manager = lesson_page_type_manager::get($this);
1358              $this->pages = $manager->load_all_pages($this);
1359              $this->loadedallpages = true;
1360          }
1361          return $this->pages;
1362      }
1363  
1364      /**
1365       * Determines if a jumpto value is correct or not.
1366       *
1367       * returns true if jumpto page is (logically) after the pageid page or
1368       * if the jumpto value is a special value.  Returns false in all other cases.
1369       *
1370       * @param int $pageid Id of the page from which you are jumping from.
1371       * @param int $jumpto The jumpto number.
1372       * @return boolean True or false after a series of tests.
1373       **/
1374      public function jumpto_is_correct($pageid, $jumpto) {
1375          global $DB;
1376  
1377          // first test the special values
1378          if (!$jumpto) {
1379              // same page
1380              return false;
1381          } elseif ($jumpto == LESSON_NEXTPAGE) {
1382              return true;
1383          } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
1384              return true;
1385          } elseif ($jumpto == LESSON_RANDOMPAGE) {
1386              return true;
1387          } elseif ($jumpto == LESSON_CLUSTERJUMP) {
1388              return true;
1389          } elseif ($jumpto == LESSON_EOL) {
1390              return true;
1391          }
1392  
1393          $pages = $this->load_all_pages();
1394          $apageid = $pages[$pageid]->nextpageid;
1395          while ($apageid != 0) {
1396              if ($jumpto == $apageid) {
1397                  return true;
1398              }
1399              $apageid = $pages[$apageid]->nextpageid;
1400          }
1401          return false;
1402      }
1403  
1404      /**
1405       * Returns the time a user has remaining on this lesson
1406       * @param int $starttime Starttime timestamp
1407       * @return string
1408       */
1409      public function time_remaining($starttime) {
1410          $timeleft = $starttime + $this->maxtime * 60 - time();
1411          $hours = floor($timeleft/3600);
1412          $timeleft = $timeleft - ($hours * 3600);
1413          $minutes = floor($timeleft/60);
1414          $secs = $timeleft - ($minutes * 60);
1415  
1416          if ($minutes < 10) {
1417              $minutes = "0$minutes";
1418          }
1419          if ($secs < 10) {
1420              $secs = "0$secs";
1421          }
1422          $output   = array();
1423          $output[] = $hours;
1424          $output[] = $minutes;
1425          $output[] = $secs;
1426          $output = implode(':', $output);
1427          return $output;
1428      }
1429  
1430      /**
1431       * Interprets LESSON_CLUSTERJUMP jumpto value.
1432       *
1433       * This will select a page randomly
1434       * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
1435       * and the page selected will be a page that has not been viewed already
1436       * and if any pages are within a branch table or end of branch then only 1 page within
1437       * the branch table or end of branch will be randomly selected (sub clustering).
1438       *
1439       * @param int $pageid Id of the current page from which we are jumping from.
1440       * @param int $userid Id of the user.
1441       * @return int The id of the next page.
1442       **/
1443      public function cluster_jump($pageid, $userid=null) {
1444          global $DB, $USER;
1445  
1446          if ($userid===null) {
1447              $userid = $USER->id;
1448          }
1449          // get the number of retakes
1450          if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
1451              $retakes = 0;
1452          }
1453          // get all the lesson_attempts aka what the user has seen
1454          $seenpages = array();
1455          if ($attempts = $this->get_attempts($retakes)) {
1456              foreach ($attempts as $attempt) {
1457                  $seenpages[$attempt->pageid] = $attempt->pageid;
1458              }
1459  
1460          }
1461  
1462          // get the lesson pages
1463          $lessonpages = $this->load_all_pages();
1464          // find the start of the cluster
1465          while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
1466              if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
1467                  break;
1468              }
1469              $pageid = $lessonpages[$pageid]->prevpageid;
1470          }
1471  
1472          $clusterpages = array();
1473          $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
1474          $unseen = array();
1475          foreach ($clusterpages as $key=>$cluster) {
1476              if ($cluster->type !== lesson_page::TYPE_QUESTION) {
1477                  unset($clusterpages[$key]);
1478              } elseif ($cluster->is_unseen($seenpages)) {
1479                  $unseen[] = $cluster;
1480              }
1481          }
1482  
1483          if (count($unseen) > 0) {
1484              // it does not contain elements, then use exitjump, otherwise find out next page/branch
1485              $nextpage = $unseen[rand(0, count($unseen)-1)];
1486              if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
1487                  // if branch table, then pick a random page inside of it
1488                  $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
1489                  return $branchpages[rand(0, count($branchpages)-1)]->id;
1490              } else { // otherwise, return the page's id
1491                  return $nextpage->id;
1492              }
1493          } else {
1494              // seen all there is to see, leave the cluster
1495              if (end($clusterpages)->nextpageid == 0) {
1496                  return LESSON_EOL;
1497              } else {
1498                  $clusterendid = $pageid;
1499                  while ($clusterendid != 0) { // this condition should not be satisfied... should be a cluster page
1500                      if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_CLUSTER) {
1501                          break;
1502                      }
1503                      $clusterendid = $lessonpages[$clusterendid]->prevpageid;
1504                  }
1505                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
1506                  if ($exitjump == LESSON_NEXTPAGE) {
1507                      $exitjump = $lessonpages[$pageid]->nextpageid;
1508                  }
1509                  if ($exitjump == 0) {
1510                      return LESSON_EOL;
1511                  } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
1512                      return $exitjump;
1513                  } else {
1514                      if (!array_key_exists($exitjump, $lessonpages)) {
1515                          $found = false;
1516                          foreach ($lessonpages as $page) {
1517                              if ($page->id === $clusterendid) {
1518                                  $found = true;
1519                              } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1520                                  $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
1521                                  break;
1522                              }
1523                          }
1524                      }
1525                      if (!array_key_exists($exitjump, $lessonpages)) {
1526                          return LESSON_EOL;
1527                      }
1528                      return $exitjump;
1529                  }
1530              }
1531          }
1532      }
1533  
1534      /**
1535       * Finds all pages that appear to be a subtype of the provided pageid until
1536       * an end point specified within $ends is encountered or no more pages exist
1537       *
1538       * @param int $pageid
1539       * @param array $ends An array of LESSON_PAGE_* types that signify an end of
1540       *               the subtype
1541       * @return array An array of specialised lesson_page objects
1542       */
1543      public function get_sub_pages_of($pageid, array $ends) {
1544          $lessonpages = $this->load_all_pages();
1545          $pageid = $lessonpages[$pageid]->nextpageid;  // move to the first page after the branch table
1546          $pages = array();
1547  
1548          while (true) {
1549              if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
1550                  break;
1551              }
1552              $pages[] = $lessonpages[$pageid];
1553              $pageid = $lessonpages[$pageid]->nextpageid;
1554          }
1555  
1556          return $pages;
1557      }
1558  
1559      /**
1560       * Checks to see if the specified page[id] is a subpage of a type specified in
1561       * the $types array, until either there are no more pages of we find a type
1562       * corresponding to that of a type specified in $ends
1563       *
1564       * @param int $pageid The id of the page to check
1565       * @param array $types An array of types that would signify this page was a subpage
1566       * @param array $ends An array of types that mean this is not a subpage
1567       * @return bool
1568       */
1569      public function is_sub_page_of_type($pageid, array $types, array $ends) {
1570          $pages = $this->load_all_pages();
1571          $pageid = $pages[$pageid]->prevpageid; // move up one
1572  
1573          array_unshift($ends, 0);
1574          // go up the pages till branch table
1575          while (true) {
1576              if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
1577                  return false;
1578              } else if (in_array($pages[$pageid]->qtype, $types)) {
1579                  return true;
1580              }
1581              $pageid = $pages[$pageid]->prevpageid;
1582          }
1583      }
1584  }
1585  
1586  
1587  /**
1588   * Abstract class to provide a core functions to the all lesson classes
1589   *
1590   * This class should be abstracted by ALL classes with the lesson module to ensure
1591   * that all classes within this module can be interacted with in the same way.
1592   *
1593   * This class provides the user with a basic properties array that can be fetched
1594   * or set via magic methods, or alternatively by defining methods get_blah() or
1595   * set_blah() within the extending object.
1596   *
1597   * @copyright  2009 Sam Hemelryk
1598   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1599   */
1600  abstract class lesson_base {
1601  
1602      /**
1603       * An object containing properties
1604       * @var stdClass
1605       */
1606      protected $properties;
1607  
1608      /**
1609       * The constructor
1610       * @param stdClass $properties
1611       */
1612      public function __construct($properties) {
1613          $this->properties = (object)$properties;
1614      }
1615  
1616      /**
1617       * Magic property method
1618       *
1619       * Attempts to call a set_$key method if one exists otherwise falls back
1620       * to simply set the property
1621       *
1622       * @param string $key
1623       * @param mixed $value
1624       */
1625      public function __set($key, $value) {
1626          if (method_exists($this, 'set_'.$key)) {
1627              $this->{'set_'.$key}($value);
1628          }
1629          $this->properties->{$key} = $value;
1630      }
1631  
1632      /**
1633       * Magic get method
1634       *
1635       * Attempts to call a get_$key method to return the property and ralls over
1636       * to return the raw property
1637       *
1638       * @param str $key
1639       * @return mixed
1640       */
1641      public function __get($key) {
1642          if (method_exists($this, 'get_'.$key)) {
1643              return $this->{'get_'.$key}();
1644          }
1645          return $this->properties->{$key};
1646      }
1647  
1648      /**
1649       * Stupid PHP needs an isset magic method if you use the get magic method and
1650       * still want empty calls to work.... blah ~!
1651       *
1652       * @param string $key
1653       * @return bool
1654       */
1655      public function __isset($key) {
1656          if (method_exists($this, 'get_'.$key)) {
1657              $val = $this->{'get_'.$key}();
1658              return !empty($val);
1659          }
1660          return !empty($this->properties->{$key});
1661      }
1662  
1663      //NOTE: E_STRICT does not allow to change function signature!
1664  
1665      /**
1666       * If implemented should create a new instance, save it in the DB and return it
1667       */
1668      //public static function create() {}
1669      /**
1670       * If implemented should load an instance from the DB and return it
1671       */
1672      //public static function load() {}
1673      /**
1674       * Fetches all of the properties of the object
1675       * @return stdClass
1676       */
1677      public function properties() {
1678          return $this->properties;
1679      }
1680  }
1681  
1682  
1683  /**
1684   * Abstract class representation of a page associated with a lesson.
1685   *
1686   * This class should MUST be extended by all specialised page types defined in
1687   * mod/lesson/pagetypes/.
1688   * There are a handful of abstract methods that need to be defined as well as
1689   * severl methods that can optionally be defined in order to make the page type
1690   * operate in the desired way
1691   *
1692   * Database properties
1693   * @property int $id The id of this lesson page
1694   * @property int $lessonid The id of the lesson this page belongs to
1695   * @property int $prevpageid The id of the page before this one
1696   * @property int $nextpageid The id of the next page in the page sequence
1697   * @property int $qtype Identifies the page type of this page
1698   * @property int $qoption Used to record page type specific options
1699   * @property int $layout Used to record page specific layout selections
1700   * @property int $display Used to record page specific display selections
1701   * @property int $timecreated Timestamp for when the page was created
1702   * @property int $timemodified Timestamp for when the page was last modified
1703   * @property string $title The title of this page
1704   * @property string $contents The rich content shown to describe the page
1705   * @property int $contentsformat The format of the contents field
1706   *
1707   * Calculated properties
1708   * @property-read array $answers An array of answers for this page
1709   * @property-read bool $displayinmenublock Toggles display in the left menu block
1710   * @property-read array $jumps An array containing all the jumps this page uses
1711   * @property-read lesson $lesson The lesson this page belongs to
1712   * @property-read int $type The type of the page [question | structure]
1713   * @property-read typeid The unique identifier for the page type
1714   * @property-read typestring The string that describes this page type
1715   *
1716   * @abstract
1717   * @copyright  2009 Sam Hemelryk
1718   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1719   */
1720  abstract class lesson_page extends lesson_base {
1721  
1722      /**
1723       * A reference to the lesson this page belongs to
1724       * @var lesson
1725       */
1726      protected $lesson = null;
1727      /**
1728       * Contains the answers to this lesson_page once loaded
1729       * @var null|array
1730       */
1731      protected $answers = null;
1732      /**
1733       * This sets the type of the page, can be one of the constants defined below
1734       * @var int
1735       */
1736      protected $type = 0;
1737  
1738      /**
1739       * Constants used to identify the type of the page
1740       */
1741      const TYPE_QUESTION = 0;
1742      const TYPE_STRUCTURE = 1;
1743  
1744      /**
1745       * This method should return the integer used to identify the page type within
1746       * the database and throughout code. This maps back to the defines used in 1.x
1747       * @abstract
1748       * @return int
1749       */
1750      abstract protected function get_typeid();
1751      /**
1752       * This method should return the string that describes the pagetype
1753       * @abstract
1754       * @return string
1755       */
1756      abstract protected function get_typestring();
1757  
1758      /**
1759       * This method gets called to display the page to the user taking the lesson
1760       * @abstract
1761       * @param object $renderer
1762       * @param object $attempt
1763       * @return string
1764       */
1765      abstract public function display($renderer, $attempt);
1766  
1767      /**
1768       * Creates a new lesson_page within the database and returns the correct pagetype
1769       * object to use to interact with the new lesson
1770       *
1771       * @final
1772       * @static
1773       * @param object $properties
1774       * @param lesson $lesson
1775       * @return lesson_page Specialised object that extends lesson_page
1776       */
1777      final public static function create($properties, lesson $lesson, $context, $maxbytes) {
1778          global $DB;
1779          $newpage = new stdClass;
1780          $newpage->title = $properties->title;
1781          $newpage->contents = $properties->contents_editor['text'];
1782          $newpage->contentsformat = $properties->contents_editor['format'];
1783          $newpage->lessonid = $lesson->id;
1784          $newpage->timecreated = time();
1785          $newpage->qtype = $properties->qtype;
1786          $newpage->qoption = (isset($properties->qoption))?1:0;
1787          $newpage->layout = (isset($properties->layout))?1:0;
1788          $newpage->display = (isset($properties->display))?1:0;
1789          $newpage->prevpageid = 0; // this is a first page
1790          $newpage->nextpageid = 0; // this is the only page
1791  
1792          if ($properties->pageid) {
1793              $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
1794              if (!$prevpage) {
1795                  print_error('cannotfindpages', 'lesson');
1796              }
1797              $newpage->prevpageid = $prevpage->id;
1798              $newpage->nextpageid = $prevpage->nextpageid;
1799          } else {
1800              $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
1801              if ($nextpage) {
1802                  // This is the first page, there are existing pages put this at the start
1803                  $newpage->nextpageid = $nextpage->id;
1804              }
1805          }
1806  
1807          $newpage->id = $DB->insert_record("lesson_pages", $newpage);
1808  
1809          $editor = new stdClass;
1810          $editor->id = $newpage->id;
1811          $editor->contents_editor = $properties->contents_editor;
1812          $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id);
1813          $DB->update_record("lesson_pages", $editor);
1814  
1815          if ($newpage->prevpageid > 0) {
1816              $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
1817          }
1818          if ($newpage->nextpageid > 0) {
1819              $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
1820          }
1821  
1822          $page = lesson_page::load($newpage, $lesson);
1823          $page->create_answers($properties);
1824  
1825          $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
1826  
1827          return $page;
1828      }
1829  
1830      /**
1831       * This method loads a page object from the database and returns it as a
1832       * specialised object that extends lesson_page
1833       *
1834       * @final
1835       * @static
1836       * @param int $id
1837       * @param lesson $lesson
1838       * @return lesson_page Specialised lesson_page object
1839       */
1840      final public static function load($id, lesson $lesson) {
1841          global $DB;
1842  
1843          if (is_object($id) && !empty($id->qtype)) {
1844              $page = $id;
1845          } else {
1846              $page = $DB->get_record("lesson_pages", array("id" => $id));
1847              if (!$page) {
1848                  print_error('cannotfindpages', 'lesson');
1849              }
1850          }
1851          $manager = lesson_page_type_manager::get($lesson);
1852  
1853          $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
1854          if (!class_exists($class)) {
1855              $class = 'lesson_page';
1856          }
1857  
1858          return new $class($page, $lesson);
1859      }
1860  
1861      /**
1862       * Deletes a lesson_page from the database as well as any associated records.
1863       * @final
1864       * @return bool
1865       */
1866      final public function delete() {
1867          global $DB;
1868          // first delete all the associated records...
1869          $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
1870          // ...now delete the answers...
1871          $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
1872          // ..and the page itself
1873          $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
1874  
1875          // Delete files associated with this page.
1876          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
1877          $context = context_module::instance($cm->id);
1878          $fs = get_file_storage();
1879          $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id);
1880          $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $this->properties->id);
1881          $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $this->properties->id);
1882  
1883          // repair the hole in the linkage
1884          if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
1885              //This is the only page, no repair needed
1886          } elseif (!$this->properties->prevpageid) {
1887              // this is the first page...
1888              $page = $this->lesson->load_page($this->properties->nextpageid);
1889              $page->move(null, 0);
1890          } elseif (!$this->properties->nextpageid) {
1891              // this is the last page...
1892              $page = $this->lesson->load_page($this->properties->prevpageid);
1893              $page->move(0);
1894          } else {
1895              // page is in the middle...
1896              $prevpage = $this->lesson->load_page($this->properties->prevpageid);
1897              $nextpage = $this->lesson->load_page($this->properties->nextpageid);
1898  
1899              $prevpage->move($nextpage->id);
1900              $nextpage->move(null, $prevpage->id);
1901          }
1902          return true;
1903      }
1904  
1905      /**
1906       * Moves a page by updating its nextpageid and prevpageid values within
1907       * the database
1908       *
1909       * @final
1910       * @param int $nextpageid
1911       * @param int $prevpageid
1912       */
1913      final public function move($nextpageid=null, $prevpageid=null) {
1914          global $DB;
1915          if ($nextpageid === null) {
1916              $nextpageid = $this->properties->nextpageid;
1917          }
1918          if ($prevpageid === null) {
1919              $prevpageid = $this->properties->prevpageid;
1920          }
1921          $obj = new stdClass;
1922          $obj->id = $this->properties->id;
1923          $obj->prevpageid = $prevpageid;
1924          $obj->nextpageid = $nextpageid;
1925          $DB->update_record('lesson_pages', $obj);
1926      }
1927  
1928      /**
1929       * Returns the answers that are associated with this page in the database
1930       *
1931       * @final
1932       * @return array
1933       */
1934      final public function get_answers() {
1935          global $DB;
1936          if ($this->answers === null) {
1937              $this->answers = array();
1938              $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
1939              if (!$answers) {
1940                  // It is possible that a lesson upgraded from Moodle 1.9 still
1941                  // contains questions without any answers [MDL-25632].
1942                  // debugging(get_string('cannotfindanswer', 'lesson'));
1943                  return array();
1944              }
1945              foreach ($answers as $answer) {
1946                  $this->answers[count($this->answers)] = new lesson_page_answer($answer);
1947              }
1948          }
1949          return $this->answers;
1950      }
1951  
1952      /**
1953       * Returns the lesson this page is associated with
1954       * @final
1955       * @return lesson
1956       */
1957      final protected function get_lesson() {
1958          return $this->lesson;
1959      }
1960  
1961      /**
1962       * Returns the type of page this is. Not to be confused with page type
1963       * @final
1964       * @return int
1965       */
1966      final protected function get_type() {
1967          return $this->type;
1968      }
1969  
1970      /**
1971       * Records an attempt at this page
1972       *
1973       * @final
1974       * @global moodle_database $DB
1975       * @param stdClass $context
1976       * @return stdClass Returns the result of the attempt
1977       */
1978      final public function record_attempt($context) {
1979          global $DB, $USER, $OUTPUT;
1980  
1981          /**
1982           * This should be overridden by each page type to actually check the response
1983           * against what ever custom criteria they have defined
1984           */
1985          $result = $this->check_answer();
1986  
1987          $result->attemptsremaining  = 0;
1988          $result->maxattemptsreached = false;
1989  
1990          if ($result->noanswer) {
1991              $result->newpageid = $this->properties->id; // display same page again
1992              $result->feedback  = get_string('noanswer', 'lesson');
1993          } else {
1994              if (!has_capability('mod/lesson:manage', $context)) {
1995                  $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
1996                  // record student's attempt
1997                  $attempt = new stdClass;
1998                  $attempt->lessonid = $this->lesson->id;
1999                  $attempt->pageid = $this->properties->id;
2000                  $attempt->userid = $USER->id;
2001                  $attempt->answerid = $result->answerid;
2002                  $attempt->retry = $nretakes;
2003                  $attempt->correct = $result->correctanswer;
2004                  if($result->userresponse !== null) {
2005                      $attempt->useranswer = $result->userresponse;
2006                  }
2007  
2008                  $attempt->timeseen = time();
2009                  // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
2010                  $userisreviewing = false;
2011                  if (isset($USER->modattempts[$this->lesson->id])) {
2012                      $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
2013                      $userisreviewing = true;
2014                  }
2015  
2016                  // Only insert a record if we are not reviewing the lesson.
2017                  if (!$userisreviewing) {
2018                      if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) {
2019                          $DB->insert_record("lesson_attempts", $attempt);
2020                      }
2021                  }
2022                  // "number of attempts remaining" message if $this->lesson->maxattempts > 1
2023                  // displaying of message(s) is at the end of page for more ergonomic display
2024                  if (!$result->correctanswer && ($result->newpageid == 0)) {
2025                      // wrong answer and student is stuck on this page - check how many attempts
2026                      // the student has had at this page/question
2027                      $nattempts = $DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry" => $attempt->retry));
2028                      // retreive the number of attempts left counter for displaying at bottom of feedback page
2029                      if ($nattempts >= $this->lesson->maxattempts) {
2030                          if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2031                              $result->maxattemptsreached = true;
2032                          }
2033                          $result->newpageid = LESSON_NEXTPAGE;
2034                      } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2035                          $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
2036                      }
2037                  }
2038              }
2039              // TODO: merge this code with the jump code below.  Convert jumpto page into a proper page id
2040              if ($result->newpageid == 0) {
2041                  $result->newpageid = $this->properties->id;
2042              } elseif ($result->newpageid == LESSON_NEXTPAGE) {
2043                  $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
2044              }
2045  
2046              // Determine default feedback if necessary
2047              if (empty($result->response)) {
2048                  if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
2049                      // These conditions have been met:
2050                      //  1. The lesson manager has not supplied feedback to the student
2051                      //  2. Not displaying default feedback
2052                      //  3. The user did provide an answer
2053                      //  4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
2054  
2055                      $result->nodefaultresponse = true;  // This will cause a redirect below
2056                  } else if ($result->isessayquestion) {
2057                      $result->response = get_string('defaultessayresponse', 'lesson');
2058                  } else if ($result->correctanswer) {
2059                      $result->response = get_string('thatsthecorrectanswer', 'lesson');
2060                  } else {
2061                      $result->response = get_string('thatsthewronganswer', 'lesson');
2062                  }
2063              }
2064  
2065              if ($result->response) {
2066                  if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
2067                      $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2068                      $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
2069                      if ($qattempts == 1) {
2070                          $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback');
2071                      } else {
2072                          $result->feedback = $OUTPUT->BOX(get_string("secondpluswrong", "lesson"), 'feedback');
2073                      }
2074                  } else {
2075                      $class = 'response';
2076                      if ($result->correctanswer) {
2077                          $class .= ' correct'; //CSS over-ride this if they exist (!important)
2078                      } else if (!$result->isessayquestion) {
2079                          $class .= ' incorrect'; //CSS over-ride this if they exist (!important)
2080                      }
2081                      $options = new stdClass;
2082                      $options->noclean = true;
2083                      $options->para = true;
2084                      $options->overflowdiv = true;
2085                      $result->response = file_rewrite_pluginfile_urls($result->response, 'pluginfile.php', $context->id,
2086                              'mod_lesson', 'page_responses', $result->answerid);
2087  
2088                      $result->feedback = $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options), 'generalbox boxaligncenter');
2089                      $result->feedback .= '<div class="correctanswer generalbox"><em>'.get_string("youranswer", "lesson").'</em> : '.$result->studentanswer; // already in clean html
2090                      $result->feedback .= $OUTPUT->box($result->response, $class); // already conerted to HTML
2091                      $result->feedback .= '</div>';
2092                  }
2093              }
2094          }
2095  
2096          return $result;
2097      }
2098  
2099      /**
2100       * Returns the string for a jump name
2101       *
2102       * @final
2103       * @param int $jumpto Jump code or page ID
2104       * @return string
2105       **/
2106      final protected function get_jump_name($jumpto) {
2107          global $DB;
2108          static $jumpnames = array();
2109  
2110          if (!array_key_exists($jumpto, $jumpnames)) {
2111              if ($jumpto == LESSON_THISPAGE) {
2112                  $jumptitle = get_string('thispage', 'lesson');
2113              } elseif ($jumpto == LESSON_NEXTPAGE) {
2114                  $jumptitle = get_string('nextpage', 'lesson');
2115              } elseif ($jumpto == LESSON_EOL) {
2116                  $jumptitle = get_string('endoflesson', 'lesson');
2117              } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2118                  $jumptitle = get_string('unseenpageinbranch', 'lesson');
2119              } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
2120                  $jumptitle = get_string('previouspage', 'lesson');
2121              } elseif ($jumpto == LESSON_RANDOMPAGE) {
2122                  $jumptitle = get_string('randompageinbranch', 'lesson');
2123              } elseif ($jumpto == LESSON_RANDOMBRANCH) {
2124                  $jumptitle = get_string('randombranch', 'lesson');
2125              } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2126                  $jumptitle = get_string('clusterjump', 'lesson');
2127              } else {
2128                  if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
2129                      $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
2130                  }
2131              }
2132              $jumpnames[$jumpto] = format_string($jumptitle,true);
2133          }
2134  
2135          return $jumpnames[$jumpto];
2136      }
2137  
2138      /**
2139       * Constructor method
2140       * @param object $properties
2141       * @param lesson $lesson
2142       */
2143      public function __construct($properties, lesson $lesson) {
2144          parent::__construct($properties);
2145          $this->lesson = $lesson;
2146      }
2147  
2148      /**
2149       * Returns the score for the attempt
2150       * This may be overridden by page types that require manual grading
2151       * @param array $answers
2152       * @param object $attempt
2153       * @return int
2154       */
2155      public function earned_score($answers, $attempt) {
2156          return $answers[$attempt->answerid]->score;
2157      }
2158  
2159      /**
2160       * This is a callback method that can be override and gets called when ever a page
2161       * is viewed
2162       *
2163       * @param bool $canmanage True if the user has the manage cap
2164       * @return mixed
2165       */
2166      public function callback_on_view($canmanage) {
2167          return true;
2168      }
2169  
2170      /**
2171       * save editor answers files and update answer record
2172       *
2173       * @param object $context
2174       * @param int $maxbytes
2175       * @param object $answer
2176       * @param object $answereditor
2177       * @param object $responseeditor
2178       */
2179      public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') {
2180          global $DB;
2181          if (isset($answereditor['itemid'])) {
2182              $answer->answer = file_save_draft_area_files($answereditor['itemid'],
2183                      $context->id, 'mod_lesson', 'page_answers', $answer->id,
2184                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
2185                      $answer->answer, null);
2186              $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id));
2187          }
2188          if (isset($responseeditor['itemid'])) {
2189              $answer->response = file_save_draft_area_files($responseeditor['itemid'],
2190                      $context->id, 'mod_lesson', 'page_responses', $answer->id,
2191                      array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
2192                      $answer->response, null);
2193              $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id));
2194          }
2195      }
2196  
2197      /**
2198       * Rewrite urls in response and optionality answer of a question answer
2199       *
2200       * @param object $answer
2201       * @param bool $rewriteanswer must rewrite answer
2202       * @return object answer with rewritten urls
2203       */
2204      public static function rewrite_answers_urls($answer, $rewriteanswer = true) {
2205          global $PAGE;
2206  
2207          $context = context_module::instance($PAGE->cm->id);
2208          if ($rewriteanswer) {
2209              $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id,
2210                      'mod_lesson', 'page_answers', $answer->id);
2211          }
2212          $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id,
2213                  'mod_lesson', 'page_responses', $answer->id);
2214  
2215          return $answer;
2216      }
2217  
2218      /**
2219       * Updates a lesson page and its answers within the database
2220       *
2221       * @param object $properties
2222       * @return bool
2223       */
2224      public function update($properties, $context = null, $maxbytes = null) {
2225          global $DB, $PAGE;
2226          $answers  = $this->get_answers();
2227          $properties->id = $this->properties->id;
2228          $properties->lessonid = $this->lesson->id;
2229          if (empty($properties->qoption)) {
2230              $properties->qoption = '0';
2231          }
2232          if (empty($context)) {
2233              $context = $PAGE->context;
2234          }
2235          if ($maxbytes === null) {
2236              $maxbytes = get_user_max_upload_file_size($context);
2237          }
2238          $properties->timemodified = time();
2239          $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id);
2240          $DB->update_record("lesson_pages", $properties);
2241  
2242          for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2243              if (!array_key_exists($i, $this->answers)) {
2244                  $this->answers[$i] = new stdClass;
2245                  $this->answers[$i]->lessonid = $this->lesson->id;
2246                  $this->answers[$i]->pageid = $this->id;
2247                  $this->answers[$i]->timecreated = $this->timecreated;
2248              }
2249  
2250              if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
2251                  $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
2252                  $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
2253              }
2254              if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
2255                  $this->answers[$i]->response = $properties->response_editor[$i]['text'];
2256                  $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
2257              }
2258  
2259              // we don't need to check for isset here because properties called it's own isset method.
2260              if ($this->answers[$i]->answer != '') {
2261                  if (isset($properties->jumpto[$i])) {
2262                      $this->answers[$i]->jumpto = $properties->jumpto[$i];
2263                  }
2264                  if ($this->lesson->custom && isset($properties->score[$i])) {
2265                      $this->answers[$i]->score = $properties->score[$i];
2266                  }
2267                  if (!isset($this->answers[$i]->id)) {
2268                      $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
2269                  } else {
2270                      $DB->update_record("lesson_answers", $this->answers[$i]->properties());
2271                  }
2272  
2273                  // Save files in answers and responses.
2274                  $this->save_answers_files($context, $maxbytes, $this->answers[$i],
2275                          $properties->answer_editor[$i], $properties->response_editor[$i]);
2276  
2277              } else if (isset($this->answers[$i]->id)) {
2278                  $DB->delete_records('lesson_answers', array('id'=>$this->answers[$i]->id));
2279                  unset($this->answers[$i]);
2280              }
2281          }
2282          return true;
2283      }
2284  
2285      /**
2286       * Can be set to true if the page requires a static link to create a new instance
2287       * instead of simply being included in the dropdown
2288       * @param int $previd
2289       * @return bool
2290       */
2291      public function add_page_link($previd) {
2292          return false;
2293      }
2294  
2295      /**
2296       * Returns true if a page has been viewed before
2297       *
2298       * @param array|int $param Either an array of pages that have been seen or the
2299       *                   number of retakes a user has had
2300       * @return bool
2301       */
2302      public function is_unseen($param) {
2303          global $USER, $DB;
2304          if (is_array($param)) {
2305              $seenpages = $param;
2306              return (!array_key_exists($this->properties->id, $seenpages));
2307          } else {
2308              $nretakes = $param;
2309              if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
2310                  return true;
2311              }
2312          }
2313          return false;
2314      }
2315  
2316      /**
2317       * Checks to see if a page has been answered previously
2318       * @param int $nretakes
2319       * @return bool
2320       */
2321      public function is_unanswered($nretakes) {
2322          global $DB, $USER;
2323          if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
2324              return true;
2325          }
2326          return false;
2327      }
2328  
2329      /**
2330       * Creates answers within the database for this lesson_page. Usually only ever
2331       * called when creating a new page instance
2332       * @param object $properties
2333       * @return array
2334       */
2335      public function create_answers($properties) {
2336          global $DB, $PAGE;
2337          // now add the answers
2338          $newanswer = new stdClass;
2339          $newanswer->lessonid = $this->lesson->id;
2340          $newanswer->pageid = $this->properties->id;
2341          $newanswer->timecreated = $this->properties->timecreated;
2342  
2343          $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
2344          $context = context_module::instance($cm->id);
2345  
2346          $answers = array();
2347  
2348          for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2349              $answer = clone($newanswer);
2350  
2351              if (!empty($properties->answer_editor[$i]) && is_array($properties->answer_editor[$i])) {
2352                  $answer->answer = $properties->answer_editor[$i]['text'];
2353                  $answer->answerformat = $properties->answer_editor[$i]['format'];
2354              }
2355              if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
2356                  $answer->response = $properties->response_editor[$i]['text'];
2357                  $answer->responseformat = $properties->response_editor[$i]['format'];
2358              }
2359  
2360              if (isset($answer->answer) && $answer->answer != '') {
2361                  if (isset($properties->jumpto[$i])) {
2362                      $answer->jumpto = $properties->jumpto[$i];
2363                  }
2364                  if ($this->lesson->custom && isset($properties->score[$i])) {
2365                      $answer->score = $properties->score[$i];
2366                  }
2367                  $answer->id = $DB->insert_record("lesson_answers", $answer);
2368                  if (isset($properties->response_editor[$i])) {
2369                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
2370                              $properties->answer_editor[$i], $properties->response_editor[$i]);
2371                  } else {
2372                      $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
2373                              $properties->answer_editor[$i]);
2374                  }
2375                  $answers[$answer->id] = new lesson_page_answer($answer);
2376              } else {
2377                  break;
2378              }
2379          }
2380  
2381          $this->answers = $answers;
2382          return $answers;
2383      }
2384  
2385      /**
2386       * This method MUST be overridden by all question page types, or page types that
2387       * wish to score a page.
2388       *
2389       * The structure of result should always be the same so it is a good idea when
2390       * overriding this method on a page type to call
2391       * <code>
2392       * $result = parent::check_answer();
2393       * </code>
2394       * before modifying it as required.
2395       *
2396       * @return stdClass
2397       */
2398      public function check_answer() {
2399          $result = new stdClass;
2400          $result->answerid        = 0;
2401          $result->noanswer        = false;
2402          $result->correctanswer   = false;
2403          $result->isessayquestion = false;   // use this to turn off review button on essay questions
2404          $result->response        = '';
2405          $result->newpageid       = 0;       // stay on the page
2406          $result->studentanswer   = '';      // use this to store student's answer(s) in order to display it on feedback page
2407          $result->userresponse    = null;
2408          $result->feedback        = '';
2409          $result->nodefaultresponse  = false; // Flag for redirecting when default feedback is turned off
2410          return $result;
2411      }
2412  
2413      /**
2414       * True if the page uses a custom option
2415       *
2416       * Should be override and set to true if the page uses a custom option.
2417       *
2418       * @return bool
2419       */
2420      public function has_option() {
2421          return false;
2422      }
2423  
2424      /**
2425       * Returns the maximum number of answers for this page given the maximum number
2426       * of answers permitted by the lesson.
2427       *
2428       * @param int $default
2429       * @return int
2430       */
2431      public function max_answers($default) {
2432          return $default;
2433      }
2434  
2435      /**
2436       * Returns the properties of this lesson page as an object
2437       * @return stdClass;
2438       */
2439      public function properties() {
2440          $properties = clone($this->properties);
2441          if ($this->answers === null) {
2442              $this->get_answers();
2443          }
2444          if (count($this->answers)>0) {
2445              $count = 0;
2446              $qtype = $properties->qtype;
2447              foreach ($this->answers as $answer) {
2448                  $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat);
2449                  if ($qtype != LESSON_PAGE_MATCHING) {
2450                      $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat);
2451                  } else {
2452                      $properties->{'response_editor['.$count.']'} = $answer->response;
2453                  }
2454                  $properties->{'jumpto['.$count.']'} = $answer->jumpto;
2455                  $properties->{'score['.$count.']'} = $answer->score;
2456                  $count++;
2457              }
2458          }
2459          return $properties;
2460      }
2461  
2462      /**
2463       * Returns an array of options to display when choosing the jumpto for a page/answer
2464       * @static
2465       * @param int $pageid
2466       * @param lesson $lesson
2467       * @return array
2468       */
2469      public static function get_jumptooptions($pageid, lesson $lesson) {
2470          global $DB;
2471          $jump = array();
2472          $jump[0] = get_string("thispage", "lesson");
2473          $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
2474          $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
2475          $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
2476  
2477          if ($pageid == 0) {
2478              return $jump;
2479          }
2480  
2481          $pages = $lesson->load_all_pages();
2482          if ($pages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))) {
2483              $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
2484              $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
2485          }
2486          if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
2487              $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
2488          }
2489          if (!optional_param('firstpage', 0, PARAM_INT)) {
2490              $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
2491              while (true) {
2492                  if ($apageid) {
2493                      $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
2494                      $jump[$apageid] = strip_tags(format_string($title,true));
2495                      $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
2496                  } else {
2497                      // last page reached
2498                      break;
2499                  }
2500              }
2501          }
2502          return $jump;
2503      }
2504      /**
2505       * Returns the contents field for the page properly formatted and with plugin
2506       * file url's converted
2507       * @return string
2508       */
2509      public function get_contents() {
2510          global $PAGE;
2511          if (!empty($this->properties->contents)) {
2512              if (!isset($this->properties->contentsformat)) {
2513                  $this->properties->contentsformat = FORMAT_HTML;
2514              }
2515              $context = context_module::instance($PAGE->cm->id);
2516              $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson',
2517                                                       'page_contents', $this->properties->id);  // Must do this BEFORE format_text()!
2518              return format_text($contents, $this->properties->contentsformat,
2519                                 array('context' => $context, 'noclean' => true,
2520                                       'overflowdiv' => true));  // Page edit is marked with XSS, we want all content here.
2521          } else {
2522              return '';
2523          }
2524      }
2525  
2526      /**
2527       * Set to true if this page should display in the menu block
2528       * @return bool
2529       */
2530      protected function get_displayinmenublock() {
2531          return false;
2532      }
2533  
2534      /**
2535       * Get the string that describes the options of this page type
2536       * @return string
2537       */
2538      public function option_description_string() {
2539          return '';
2540      }
2541  
2542      /**
2543       * Updates a table with the answers for this page
2544       * @param html_table $table
2545       * @return html_table
2546       */
2547      public function display_answers(html_table $table) {
2548          $answers = $this->get_answers();
2549          $i = 1;
2550          foreach ($answers as $answer) {
2551              $cells = array();
2552              $cells[] = "<span class=\"label\">".get_string("jump", "lesson")." $i<span>: ";
2553              $cells[] = $this->get_jump_name($answer->jumpto);
2554              $table->data[] = new html_table_row($cells);
2555              if ($i === 1){
2556                  $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
2557              }
2558              $i++;
2559          }
2560          return $table;
2561      }
2562  
2563      /**
2564       * Determines if this page should be grayed out on the management/report screens
2565       * @return int 0 or 1
2566       */
2567      protected function get_grayout() {
2568          return 0;
2569      }
2570  
2571      /**
2572       * Adds stats for this page to the &pagestats object. This should be defined
2573       * for all page types that grade
2574       * @param array $pagestats
2575       * @param int $tries
2576       * @return bool
2577       */
2578      public function stats(array &$pagestats, $tries) {
2579          return true;
2580      }
2581  
2582      /**
2583       * Formats the answers of this page for a report
2584       *
2585       * @param object $answerpage
2586       * @param object $answerdata
2587       * @param object $useranswer
2588       * @param array $pagestats
2589       * @param int $i Count of first level answers
2590       * @param int $n Count of second level answers
2591       * @return object The answer page for this
2592       */
2593      public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
2594          $answers = $this->get_answers();
2595          $formattextdefoptions = new stdClass;
2596          $formattextdefoptions->para = false;  //I'll use it widely in this page
2597          foreach ($answers as $answer) {
2598              $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
2599              $answerdata->answers[] = array($data, "");
2600              $answerpage->answerdata = $answerdata;
2601          }
2602          return $answerpage;
2603      }
2604  
2605      /**
2606       * Gets an array of the jumps used by the answers of this page
2607       *
2608       * @return array
2609       */
2610      public function get_jumps() {
2611          global $DB;
2612          $jumps = array();
2613          $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
2614          if ($answers = $this->get_answers()) {
2615              foreach ($answers as $answer) {
2616                  $jumps[] = $this->get_jump_name($answer->jumpto);
2617              }
2618          } else {
2619              $jumps[] = $this->get_jump_name($this->properties->nextpageid);
2620          }
2621          return $jumps;
2622      }
2623      /**
2624       * Informs whether this page type require manual grading or not
2625       * @return bool
2626       */
2627      public function requires_manual_grading() {
2628          return false;
2629      }
2630  
2631      /**
2632       * A callback method that allows a page to override the next page a user will
2633       * see during when this page is being completed.
2634       * @return false|int
2635       */
2636      public function override_next_page() {
2637          return false;
2638      }
2639  
2640      /**
2641       * This method is used to determine if this page is a valid page
2642       *
2643       * @param array $validpages
2644       * @param array $pageviews
2645       * @return int The next page id to check
2646       */
2647      public function valid_page_and_view(&$validpages, &$pageviews) {
2648          $validpages[$this->properties->id] = 1;
2649          return $this->properties->nextpageid;
2650      }
2651  }
2652  
2653  
2654  
2655  /**
2656   * Class used to represent an answer to a page
2657   *
2658   * @property int $id The ID of this answer in the database
2659   * @property int $lessonid The ID of the lesson this answer belongs to
2660   * @property int $pageid The ID of the page this answer belongs to
2661   * @property int $jumpto Identifies where the user goes upon completing a page with this answer
2662   * @property int $grade The grade this answer is worth
2663   * @property int $score The score this answer will give
2664   * @property int $flags Used to store options for the answer
2665   * @property int $timecreated A timestamp of when the answer was created
2666   * @property int $timemodified A timestamp of when the answer was modified
2667   * @property string $answer The answer itself
2668   * @property string $response The response the user sees if selecting this answer
2669   *
2670   * @copyright  2009 Sam Hemelryk
2671   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2672   */
2673  class lesson_page_answer extends lesson_base {
2674  
2675      /**
2676       * Loads an page answer from the DB
2677       *
2678       * @param int $id
2679       * @return lesson_page_answer
2680       */
2681      public static function load($id) {
2682          global $DB;
2683          $answer = $DB->get_record("lesson_answers", array("id" => $id));
2684          return new lesson_page_answer($answer);
2685      }
2686  
2687      /**
2688       * Given an object of properties and a page created answer(s) and saves them
2689       * in the database.
2690       *
2691       * @param stdClass $properties
2692       * @param lesson_page $page
2693       * @return array
2694       */
2695      public static function create($properties, lesson_page $page) {
2696          return $page->create_answers($properties);
2697      }
2698  
2699  }
2700  
2701  /**
2702   * A management class for page types
2703   *
2704   * This class is responsible for managing the different pages. A manager object can
2705   * be retrieved by calling the following line of code:
2706   * <code>
2707   * $manager  = lesson_page_type_manager::get($lesson);
2708   * </code>
2709   * The first time the page type manager is retrieved the it includes all of the
2710   * different page types located in mod/lesson/pagetypes.
2711   *
2712   * @copyright  2009 Sam Hemelryk
2713   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2714   */
2715  class lesson_page_type_manager {
2716  
2717      /**
2718       * An array of different page type classes
2719       * @var array
2720       */
2721      protected $types = array();
2722  
2723      /**
2724       * Retrieves the lesson page type manager object
2725       *
2726       * If the object hasn't yet been created it is created here.
2727       *
2728       * @staticvar lesson_page_type_manager $pagetypemanager
2729       * @param lesson $lesson
2730       * @return lesson_page_type_manager
2731       */
2732      public static function get(lesson $lesson) {
2733          static $pagetypemanager;
2734          if (!($pagetypemanager instanceof lesson_page_type_manager)) {
2735              $pagetypemanager = new lesson_page_type_manager();
2736              $pagetypemanager->load_lesson_types($lesson);
2737          }
2738          return $pagetypemanager;
2739      }
2740  
2741      /**
2742       * Finds and loads all lesson page types in mod/lesson/pagetypes
2743       *
2744       * @param lesson $lesson
2745       */
2746      public function load_lesson_types(lesson $lesson) {
2747          global $CFG;
2748          $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
2749          $dir = dir($basedir);
2750          while (false !== ($entry = $dir->read())) {
2751              if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
2752                  continue;
2753              }
2754              require_once($basedir.$entry);
2755              $class = 'lesson_page_type_'.strtok($entry,'.');
2756              if (class_exists($class)) {
2757                  $pagetype = new $class(new stdClass, $lesson);
2758                  $this->types[$pagetype->typeid] = $pagetype;
2759              }
2760          }
2761  
2762      }
2763  
2764      /**
2765       * Returns an array of strings to describe the loaded page types
2766       *
2767       * @param int $type Can be used to return JUST the string for the requested type
2768       * @return array
2769       */
2770      public function get_page_type_strings($type=null, $special=true) {
2771          $types = array();
2772          foreach ($this->types as $pagetype) {
2773              if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
2774                  $types[$pagetype->typeid] = $pagetype->typestring;
2775              }
2776          }
2777          return $types;
2778      }
2779  
2780      /**
2781       * Returns the basic string used to identify a page type provided with an id
2782       *
2783       * This string can be used to instantiate or identify the page type class.
2784       * If the page type id is unknown then 'unknown' is returned
2785       *
2786       * @param int $id
2787       * @return string
2788       */
2789      public function get_page_type_idstring($id) {
2790          foreach ($this->types as $pagetype) {
2791              if ((int)$pagetype->typeid === (int)$id) {
2792                  return $pagetype->idstring;
2793              }
2794          }
2795          return 'unknown';
2796      }
2797  
2798      /**
2799       * Loads a page for the provided lesson given it's id
2800       *
2801       * This function loads a page from the lesson when given both the lesson it belongs
2802       * to as well as the page's id.
2803       * If the page doesn't exist an error is thrown
2804       *
2805       * @param int $pageid The id of the page to load
2806       * @param lesson $lesson The lesson the page belongs to
2807       * @return lesson_page A class that extends lesson_page
2808       */
2809      public function load_page($pageid, lesson $lesson) {
2810          global $DB;
2811          if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
2812              print_error('cannotfindpages', 'lesson');
2813          }
2814          $pagetype = get_class($this->types[$page->qtype]);
2815          $page = new $pagetype($page, $lesson);
2816          return $page;
2817      }
2818  
2819      /**
2820       * This function detects errors in the ordering between 2 pages and updates the page records.
2821       *
2822       * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list.
2823       * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list.
2824       */
2825      protected function check_page_order($page1, $page2) {
2826          global $DB;
2827          if (empty($page1)) {
2828              if ($page2->prevpageid != 0) {
2829                  debugging("***prevpageid of page " . $page2->id . " set to 0***");
2830                  $page2->prevpageid = 0;
2831                  $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id));
2832              }
2833          } else if (empty($page2)) {
2834              if ($page1->nextpageid != 0) {
2835                  debugging("***nextpageid of page " . $page1->id . " set to 0***");
2836                  $page1->nextpageid = 0;
2837                  $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id));
2838              }
2839          } else {
2840              if ($page1->nextpageid != $page2->id) {
2841                  debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***");
2842                  $page1->nextpageid = $page2->id;
2843                  $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id));
2844              }
2845              if ($page2->prevpageid != $page1->id) {
2846                  debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***");
2847                  $page2->prevpageid = $page1->id;
2848                  $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id));
2849              }
2850          }
2851      }
2852  
2853      /**
2854       * This function loads ALL pages that belong to the lesson.
2855       *
2856       * @param lesson $lesson
2857       * @return array An array of lesson_page_type_*
2858       */
2859      public function load_all_pages(lesson $lesson) {
2860          global $DB;
2861          if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
2862              return array(); // Records returned empty.
2863          }
2864          foreach ($pages as $key=>$page) {
2865              $pagetype = get_class($this->types[$page->qtype]);
2866              $pages[$key] = new $pagetype($page, $lesson);
2867          }
2868  
2869          $orderedpages = array();
2870          $lastpageid = 0;
2871          $morepages = true;
2872          while ($morepages) {
2873              $morepages = false;
2874              foreach ($pages as $page) {
2875                  if ((int)$page->prevpageid === (int)$lastpageid) {
2876                      // Check for errors in page ordering and fix them on the fly.
2877                      $prevpage = null;
2878                      if ($lastpageid !== 0) {
2879                          $prevpage = $orderedpages[$lastpageid];
2880                      }
2881                      $this->check_page_order($prevpage, $page);
2882                      $morepages = true;
2883                      $orderedpages[$page->id] = $page;
2884                      unset($pages[$page->id]);
2885                      $lastpageid = $page->id;
2886                      if ((int)$page->nextpageid===0) {
2887                          break 2;
2888                      } else {
2889                          break 1;
2890                      }
2891                  }
2892              }
2893          }
2894  
2895          // Add remaining pages and fix the nextpageid links for each page.
2896          foreach ($pages as $page) {
2897              // Check for errors in page ordering and fix them on the fly.
2898              $prevpage = null;
2899              if ($lastpageid !== 0) {
2900                  $prevpage = $orderedpages[$lastpageid];
2901              }
2902              $this->check_page_order($prevpage, $page);
2903              $orderedpages[$page->id] = $page;
2904              unset($pages[$page->id]);
2905              $lastpageid = $page->id;
2906          }
2907  
2908          if ($lastpageid !== 0) {
2909              $this->check_page_order($orderedpages[$lastpageid], null);
2910          }
2911  
2912          return $orderedpages;
2913      }
2914  
2915      /**
2916       * Fetches an mform that can be used to create/edit an page
2917       *
2918       * @param int $type The id for the page type
2919       * @param array $arguments Any arguments to pass to the mform
2920       * @return lesson_add_page_form_base
2921       */
2922      public function get_page_form($type, $arguments) {
2923          $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
2924          if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
2925              debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
2926              $class = 'lesson_add_page_form_selection';
2927          } else if ($class === 'lesson_add_page_form_unknown') {
2928              $class = 'lesson_add_page_form_selection';
2929          }
2930          return new $class(null, $arguments);
2931      }
2932  
2933      /**
2934       * Returns an array of links to use as add page links
2935       * @param int $previd The id of the previous page
2936       * @return array
2937       */
2938      public function get_add_page_type_links($previd) {
2939          global $OUTPUT;
2940  
2941          $links = array();
2942  
2943          foreach ($this->types as $key=>$type) {
2944              if ($link = $type->add_page_link($previd)) {
2945                  $links[$key] = $link;
2946              }
2947          }
2948  
2949          return $links;
2950      }
2951  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1