PageRenderTime 81ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/mod/lesson/locallib.php

https://bitbucket.org/moodle/moodle
PHP | 5351 lines | 3222 code | 524 blank | 1605 comment | 786 complexity | 4c6febf9ff6dd19337963db1e7dd0f45 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Local library file for Lesson. These are non-standard functions that are used
  18. * only by Lesson.
  19. *
  20. * @package mod_lesson
  21. * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
  23. **/
  24. /** Make sure this isn't being directly accessed */
  25. defined('MOODLE_INTERNAL') || die();
  26. /** Include the files that are required by this module */
  27. require_once($CFG->dirroot.'/course/moodleform_mod.php');
  28. require_once($CFG->dirroot . '/mod/lesson/lib.php');
  29. require_once($CFG->libdir . '/filelib.php');
  30. /** This page */
  31. define('LESSON_THISPAGE', 0);
  32. /** Next page -> any page not seen before */
  33. define("LESSON_UNSEENPAGE", 1);
  34. /** Next page -> any page not answered correctly */
  35. define("LESSON_UNANSWEREDPAGE", 2);
  36. /** Jump to Next Page */
  37. define("LESSON_NEXTPAGE", -1);
  38. /** End of Lesson */
  39. define("LESSON_EOL", -9);
  40. /** Jump to an unseen page within a branch and end of branch or end of lesson */
  41. define("LESSON_UNSEENBRANCHPAGE", -50);
  42. /** Jump to Previous Page */
  43. define("LESSON_PREVIOUSPAGE", -40);
  44. /** Jump to a random page within a branch and end of branch or end of lesson */
  45. define("LESSON_RANDOMPAGE", -60);
  46. /** Jump to a random Branch */
  47. define("LESSON_RANDOMBRANCH", -70);
  48. /** Cluster Jump */
  49. define("LESSON_CLUSTERJUMP", -80);
  50. /** Undefined */
  51. define("LESSON_UNDEFINED", -99);
  52. /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
  53. define("LESSON_MAX_EVENT_LENGTH", "432000");
  54. /** Answer format is HTML */
  55. define("LESSON_ANSWER_HTML", "HTML");
  56. /** Placeholder answer for all other answers. */
  57. define("LESSON_OTHER_ANSWERS", "@#wronganswer#@");
  58. //////////////////////////////////////////////////////////////////////////////////////
  59. /// Any other lesson functions go here. Each of them must have a name that
  60. /// starts with lesson_
  61. /**
  62. * Checks to see if a LESSON_CLUSTERJUMP or
  63. * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
  64. *
  65. * This function is only executed when a teacher is
  66. * checking the navigation for a lesson.
  67. *
  68. * @param stdClass $lesson Id of the lesson that is to be checked.
  69. * @return boolean True or false.
  70. **/
  71. function lesson_display_teacher_warning($lesson) {
  72. global $DB;
  73. // get all of the lesson answers
  74. $params = array ("lessonid" => $lesson->id);
  75. if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
  76. // no answers, then not using cluster or unseen
  77. return false;
  78. }
  79. // just check for the first one that fulfills the requirements
  80. foreach ($lessonanswers as $lessonanswer) {
  81. if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
  82. return true;
  83. }
  84. }
  85. // if no answers use either of the two jumps
  86. return false;
  87. }
  88. /**
  89. * Interprets the LESSON_UNSEENBRANCHPAGE jump.
  90. *
  91. * will return the pageid of a random unseen page that is within a branch
  92. *
  93. * @param lesson $lesson
  94. * @param int $userid Id of the user.
  95. * @param int $pageid Id of the page from which we are jumping.
  96. * @return int Id of the next page.
  97. **/
  98. function lesson_unseen_question_jump($lesson, $user, $pageid) {
  99. global $DB;
  100. // get the number of retakes
  101. if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
  102. $retakes = 0;
  103. }
  104. // get all the lesson_attempts aka what the user has seen
  105. if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
  106. foreach($viewedpages as $viewed) {
  107. $seenpages[] = $viewed->pageid;
  108. }
  109. } else {
  110. $seenpages = array();
  111. }
  112. // get the lesson pages
  113. $lessonpages = $lesson->load_all_pages();
  114. if ($pageid == LESSON_UNSEENBRANCHPAGE) { // this only happens when a student leaves in the middle of an unseen question within a branch series
  115. $pageid = $seenpages[0]; // just change the pageid to the last page viewed inside the branch table
  116. }
  117. // go up the pages till branch table
  118. while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
  119. if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
  120. break;
  121. }
  122. $pageid = $lessonpages[$pageid]->prevpageid;
  123. }
  124. $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
  125. // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
  126. $unseen = array();
  127. foreach($pagesinbranch as $page) {
  128. if (!in_array($page->id, $seenpages)) {
  129. $unseen[] = $page->id;
  130. }
  131. }
  132. if(count($unseen) == 0) {
  133. if(isset($pagesinbranch)) {
  134. $temp = end($pagesinbranch);
  135. $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
  136. } else {
  137. // there are no pages inside the branch, so return the next page
  138. $nextpage = $lessonpages[$pageid]->nextpageid;
  139. }
  140. if ($nextpage == 0) {
  141. return LESSON_EOL;
  142. } else {
  143. return $nextpage;
  144. }
  145. } else {
  146. return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
  147. }
  148. }
  149. /**
  150. * Handles the unseen branch table jump.
  151. *
  152. * @param lesson $lesson
  153. * @param int $userid User id.
  154. * @return int Will return the page id of a branch table or end of lesson
  155. **/
  156. function lesson_unseen_branch_jump($lesson, $userid) {
  157. global $DB;
  158. if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
  159. $retakes = 0;
  160. }
  161. if (!$seenbranches = $lesson->get_content_pages_viewed($retakes, $userid, 'timeseen DESC')) {
  162. print_error('cannotfindrecords', 'lesson');
  163. }
  164. // get the lesson pages
  165. $lessonpages = $lesson->load_all_pages();
  166. // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
  167. // which is the branch table that starts the unseenbranch function
  168. $seen = array();
  169. foreach ($seenbranches as $seenbranch) {
  170. if (!$seenbranch->flag) {
  171. $seen[$seenbranch->pageid] = $seenbranch->pageid;
  172. } else {
  173. $start = $seenbranch->pageid;
  174. break;
  175. }
  176. }
  177. // this function searches through the lesson pages to find all the branch tables
  178. // that follow the flagged branch table
  179. $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
  180. $branchtables = array();
  181. while ($pageid != 0) { // grab all of the branch table till eol
  182. if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
  183. $branchtables[] = $lessonpages[$pageid]->id;
  184. }
  185. $pageid = $lessonpages[$pageid]->nextpageid;
  186. }
  187. $unseen = array();
  188. foreach ($branchtables as $branchtable) {
  189. // load all of the unseen branch tables into unseen
  190. if (!array_key_exists($branchtable, $seen)) {
  191. $unseen[] = $branchtable;
  192. }
  193. }
  194. if (count($unseen) > 0) {
  195. return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
  196. } else {
  197. return LESSON_EOL; // has viewed all of the branch tables
  198. }
  199. }
  200. /**
  201. * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
  202. *
  203. * @param lesson $lesson
  204. * @param int $pageid The id of the page that we are jumping from (?)
  205. * @return int The pageid of a random page that is within a branch table
  206. **/
  207. function lesson_random_question_jump($lesson, $pageid) {
  208. global $DB;
  209. // get the lesson pages
  210. $params = array ("lessonid" => $lesson->id);
  211. if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
  212. print_error('cannotfindpages', 'lesson');
  213. }
  214. // go up the pages till branch table
  215. while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
  216. if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
  217. break;
  218. }
  219. $pageid = $lessonpages[$pageid]->prevpageid;
  220. }
  221. // get the pages within the branch
  222. $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
  223. if(count($pagesinbranch) == 0) {
  224. // there are no pages inside the branch, so return the next page
  225. return $lessonpages[$pageid]->nextpageid;
  226. } else {
  227. return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id; // returns a random page id for the next page
  228. }
  229. }
  230. /**
  231. * Calculates a user's grade for a lesson.
  232. *
  233. * @param object $lesson The lesson that the user is taking.
  234. * @param int $retries The attempt number.
  235. * @param int $userid Id of the user (optional, default current user).
  236. * @return object { nquestions => number of questions answered
  237. attempts => number of question attempts
  238. total => max points possible
  239. earned => points earned by student
  240. grade => calculated percentage grade
  241. nmanual => number of manually graded questions
  242. manualpoints => point value for manually graded questions }
  243. */
  244. function lesson_grade($lesson, $ntries, $userid = 0) {
  245. global $USER, $DB;
  246. if (empty($userid)) {
  247. $userid = $USER->id;
  248. }
  249. // Zero out everything
  250. $ncorrect = 0;
  251. $nviewed = 0;
  252. $score = 0;
  253. $nmanual = 0;
  254. $manualpoints = 0;
  255. $thegrade = 0;
  256. $nquestions = 0;
  257. $total = 0;
  258. $earned = 0;
  259. $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
  260. if ($useranswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND
  261. userid = :userid AND retry = :retry", $params, "timeseen")) {
  262. // group each try with its page
  263. $attemptset = array();
  264. foreach ($useranswers as $useranswer) {
  265. $attemptset[$useranswer->pageid][] = $useranswer;
  266. }
  267. if (!empty($lesson->maxattempts)) {
  268. // Drop all attempts that go beyond max attempts for the lesson.
  269. foreach ($attemptset as $key => $set) {
  270. $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
  271. }
  272. }
  273. // get only the pages and their answers that the user answered
  274. list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
  275. array_unshift($parameters, $lesson->id);
  276. $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
  277. $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
  278. // Number of pages answered
  279. $nquestions = count($pages);
  280. foreach ($attemptset as $attempts) {
  281. $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
  282. if ($lesson->custom) {
  283. $attempt = end($attempts);
  284. // If essay question, handle it, otherwise add to score
  285. if ($page->requires_manual_grading()) {
  286. $useranswerobj = unserialize($attempt->useranswer);
  287. if (isset($useranswerobj->score)) {
  288. $earned += $useranswerobj->score;
  289. }
  290. $nmanual++;
  291. $manualpoints += $answers[$attempt->answerid]->score;
  292. } else if (!empty($attempt->answerid)) {
  293. $earned += $page->earned_score($answers, $attempt);
  294. }
  295. } else {
  296. foreach ($attempts as $attempt) {
  297. $earned += $attempt->correct;
  298. }
  299. $attempt = end($attempts); // doesn't matter which one
  300. // If essay question, increase numbers
  301. if ($page->requires_manual_grading()) {
  302. $nmanual++;
  303. $manualpoints++;
  304. }
  305. }
  306. // Number of times answered
  307. $nviewed += count($attempts);
  308. }
  309. if ($lesson->custom) {
  310. $bestscores = array();
  311. // Find the highest possible score per page to get our total
  312. foreach ($answers as $answer) {
  313. if(!isset($bestscores[$answer->pageid])) {
  314. $bestscores[$answer->pageid] = $answer->score;
  315. } else if ($bestscores[$answer->pageid] < $answer->score) {
  316. $bestscores[$answer->pageid] = $answer->score;
  317. }
  318. }
  319. $total = array_sum($bestscores);
  320. } else {
  321. // Check to make sure the student has answered the minimum questions
  322. if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
  323. // Nope, increase number viewed by the amount of unanswered questions
  324. $total = $nviewed + ($lesson->minquestions - $nquestions);
  325. } else {
  326. $total = $nviewed;
  327. }
  328. }
  329. }
  330. if ($total) { // not zero
  331. $thegrade = round(100 * $earned / $total, 5);
  332. }
  333. // Build the grade information object
  334. $gradeinfo = new stdClass;
  335. $gradeinfo->nquestions = $nquestions;
  336. $gradeinfo->attempts = $nviewed;
  337. $gradeinfo->total = $total;
  338. $gradeinfo->earned = $earned;
  339. $gradeinfo->grade = $thegrade;
  340. $gradeinfo->nmanual = $nmanual;
  341. $gradeinfo->manualpoints = $manualpoints;
  342. return $gradeinfo;
  343. }
  344. /**
  345. * Determines if a user can view the left menu. The determining factor
  346. * is whether a user has a grade greater than or equal to the lesson setting
  347. * of displayleftif
  348. *
  349. * @param object $lesson Lesson object of the current lesson
  350. * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
  351. **/
  352. function lesson_displayleftif($lesson) {
  353. global $CFG, $USER, $DB;
  354. if (!empty($lesson->displayleftif)) {
  355. // get the current user's max grade for this lesson
  356. $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
  357. 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)) {
  358. if ($maxgrade->maxgrade < $lesson->displayleftif) {
  359. return 0; // turn off the displayleft
  360. }
  361. } else {
  362. return 0; // no grades
  363. }
  364. }
  365. // if we get to here, keep the original state of displayleft lesson setting
  366. return $lesson->displayleft;
  367. }
  368. /**
  369. *
  370. * @param $cm
  371. * @param $lesson
  372. * @param $page
  373. * @return unknown_type
  374. */
  375. function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
  376. $bc = lesson_menu_block_contents($cm->id, $lesson);
  377. if (!empty($bc)) {
  378. $regions = $page->blocks->get_regions();
  379. $firstregion = reset($regions);
  380. $page->blocks->add_fake_block($bc, $firstregion);
  381. }
  382. $bc = lesson_mediafile_block_contents($cm->id, $lesson);
  383. if (!empty($bc)) {
  384. $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
  385. }
  386. if (!empty($timer)) {
  387. $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
  388. if (!empty($bc)) {
  389. $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
  390. }
  391. }
  392. }
  393. /**
  394. * If there is a media file associated with this
  395. * lesson, return a block_contents that displays it.
  396. *
  397. * @param int $cmid Course Module ID for this lesson
  398. * @param object $lesson Full lesson record object
  399. * @return block_contents
  400. **/
  401. function lesson_mediafile_block_contents($cmid, $lesson) {
  402. global $OUTPUT;
  403. if (empty($lesson->mediafile)) {
  404. return null;
  405. }
  406. $options = array();
  407. $options['menubar'] = 0;
  408. $options['location'] = 0;
  409. $options['left'] = 5;
  410. $options['top'] = 5;
  411. $options['scrollbars'] = 1;
  412. $options['resizable'] = 1;
  413. $options['width'] = $lesson->mediawidth;
  414. $options['height'] = $lesson->mediaheight;
  415. $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
  416. $action = new popup_action('click', $link, 'lessonmediafile', $options);
  417. $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
  418. $bc = new block_contents();
  419. $bc->title = get_string('linkedmedia', 'lesson');
  420. $bc->attributes['class'] = 'mediafile block';
  421. $bc->content = $content;
  422. return $bc;
  423. }
  424. /**
  425. * If a timed lesson and not a teacher, then
  426. * return a block_contents containing the clock.
  427. *
  428. * @param int $cmid Course Module ID for this lesson
  429. * @param object $lesson Full lesson record object
  430. * @param object $timer Full timer record object
  431. * @return block_contents
  432. **/
  433. function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
  434. // Display for timed lessons and for students only
  435. $context = context_module::instance($cmid);
  436. if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) {
  437. return null;
  438. }
  439. $content = '<div id="lesson-timer">';
  440. $content .= $lesson->time_remaining($timer->starttime);
  441. $content .= '</div>';
  442. $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit);
  443. $page->requires->data_for_js('clocksettings', $clocksettings, true);
  444. $page->requires->strings_for_js(array('timeisup'), 'lesson');
  445. $page->requires->js('/mod/lesson/timer.js');
  446. $page->requires->js_init_call('show_clock');
  447. $bc = new block_contents();
  448. $bc->title = get_string('timeremaining', 'lesson');
  449. $bc->attributes['class'] = 'clock block';
  450. $bc->content = $content;
  451. return $bc;
  452. }
  453. /**
  454. * If left menu is turned on, then this will
  455. * print the menu in a block
  456. *
  457. * @param int $cmid Course Module ID for this lesson
  458. * @param lesson $lesson Full lesson record object
  459. * @return void
  460. **/
  461. function lesson_menu_block_contents($cmid, $lesson) {
  462. global $CFG, $DB;
  463. if (!$lesson->displayleft) {
  464. return null;
  465. }
  466. $pages = $lesson->load_all_pages();
  467. foreach ($pages as $page) {
  468. if ((int)$page->prevpageid === 0) {
  469. $pageid = $page->id;
  470. break;
  471. }
  472. }
  473. $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
  474. if (!$pageid || !$pages) {
  475. return null;
  476. }
  477. $content = '<a href="#maincontent" class="accesshide">' .
  478. get_string('skip', 'lesson') .
  479. "</a>\n<div class=\"menuwrapper\">\n<ul>\n";
  480. while ($pageid != 0) {
  481. $page = $pages[$pageid];
  482. // Only process branch tables with display turned on
  483. if ($page->displayinmenublock && $page->display) {
  484. if ($page->id == $currentpageid) {
  485. $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
  486. } else {
  487. $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";
  488. }
  489. }
  490. $pageid = $page->nextpageid;
  491. }
  492. $content .= "</ul>\n</div>\n";
  493. $bc = new block_contents();
  494. $bc->title = get_string('lessonmenu', 'lesson');
  495. $bc->attributes['class'] = 'menu block';
  496. $bc->content = $content;
  497. return $bc;
  498. }
  499. /**
  500. * Adds header buttons to the page for the lesson
  501. *
  502. * @param object $cm
  503. * @param object $context
  504. * @param bool $extraeditbuttons
  505. * @param int $lessonpageid
  506. */
  507. function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
  508. global $CFG, $PAGE, $OUTPUT;
  509. if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
  510. if ($lessonpageid === null) {
  511. print_error('invalidpageid', 'lesson');
  512. }
  513. if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
  514. $url = new moodle_url('/mod/lesson/editpage.php', array(
  515. 'id' => $cm->id,
  516. 'pageid' => $lessonpageid,
  517. 'edit' => 1,
  518. 'returnto' => $PAGE->url->out_as_local_url(false)
  519. ));
  520. $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
  521. }
  522. }
  523. }
  524. /**
  525. * This is a function used to detect media types and generate html code.
  526. *
  527. * @global object $CFG
  528. * @global object $PAGE
  529. * @param object $lesson
  530. * @param object $context
  531. * @return string $code the html code of media
  532. */
  533. function lesson_get_media_html($lesson, $context) {
  534. global $CFG, $PAGE, $OUTPUT;
  535. require_once("$CFG->libdir/resourcelib.php");
  536. // get the media file link
  537. if (strpos($lesson->mediafile, '://') !== false) {
  538. $url = new moodle_url($lesson->mediafile);
  539. } else {
  540. // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
  541. $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
  542. }
  543. $title = $lesson->mediafile;
  544. $clicktoopen = html_writer::link($url, get_string('download'));
  545. $mimetype = resourcelib_guess_url_mimetype($url);
  546. $extension = resourcelib_get_extension($url->out(false));
  547. $mediamanager = core_media_manager::instance($PAGE);
  548. $embedoptions = array(
  549. core_media_manager::OPTION_TRUSTED => true,
  550. core_media_manager::OPTION_BLOCK => true
  551. );
  552. // find the correct type and print it out
  553. if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image
  554. $code = resourcelib_embed_image($url, $title);
  555. } else if ($mediamanager->can_embed_url($url, $embedoptions)) {
  556. // Media (audio/video) file.
  557. $code = $mediamanager->embed_url($url, $title, 0, 0, $embedoptions);
  558. } else {
  559. // anything else - just try object tag enlarged as much as possible
  560. $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
  561. }
  562. return $code;
  563. }
  564. /**
  565. * Logic to happen when a/some group(s) has/have been deleted in a course.
  566. *
  567. * @param int $courseid The course ID.
  568. * @param int $groupid The group id if it is known
  569. * @return void
  570. */
  571. function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
  572. global $DB;
  573. $params = array('courseid' => $courseid);
  574. if ($groupid) {
  575. $params['groupid'] = $groupid;
  576. // We just update the group that was deleted.
  577. $sql = "SELECT o.id, o.lessonid, o.groupid
  578. FROM {lesson_overrides} o
  579. JOIN {lesson} lesson ON lesson.id = o.lessonid
  580. WHERE lesson.course = :courseid
  581. AND o.groupid = :groupid";
  582. } else {
  583. // No groupid, we update all orphaned group overrides for all lessons in course.
  584. $sql = "SELECT o.id, o.lessonid, o.groupid
  585. FROM {lesson_overrides} o
  586. JOIN {lesson} lesson ON lesson.id = o.lessonid
  587. LEFT JOIN {groups} grp ON grp.id = o.groupid
  588. WHERE lesson.course = :courseid
  589. AND o.groupid IS NOT NULL
  590. AND grp.id IS NULL";
  591. }
  592. $records = $DB->get_records_sql($sql, $params);
  593. if (!$records) {
  594. return; // Nothing to do.
  595. }
  596. $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
  597. $cache = cache::make('mod_lesson', 'overrides');
  598. foreach ($records as $record) {
  599. $cache->delete("{$record->lessonid}_g_{$record->groupid}");
  600. }
  601. }
  602. /**
  603. * Return the overview report table and data.
  604. *
  605. * @param lesson $lesson lesson instance
  606. * @param mixed $currentgroup false if not group used, 0 for all groups, group id (int) to filter by that groups
  607. * @return mixed false if there is no information otherwise html_table and stdClass with the table and data
  608. * @since Moodle 3.3
  609. */
  610. function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) {
  611. global $DB, $CFG, $OUTPUT;
  612. require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
  613. $context = $lesson->context;
  614. $cm = $lesson->cm;
  615. // Count the number of branch and question pages in this lesson.
  616. $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
  617. $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
  618. // Only load students if there attempts for this lesson.
  619. $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
  620. $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
  621. $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
  622. if ($attempts or $branches or $timer) {
  623. list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
  624. list($sort, $sortparams) = users_order_by_sql('u');
  625. // TODO Does not support custom user profile fields (MDL-70456).
  626. $userfieldsapi = \core_user\fields::for_identity($context, false)->with_userpic();
  627. $ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
  628. $extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
  629. $params['a1lessonid'] = $lesson->id;
  630. $params['b1lessonid'] = $lesson->id;
  631. $params['c1lessonid'] = $lesson->id;
  632. $sql = "SELECT DISTINCT $ufields
  633. FROM {user} u
  634. JOIN (
  635. SELECT userid, lessonid FROM {lesson_attempts} a1
  636. WHERE a1.lessonid = :a1lessonid
  637. UNION
  638. SELECT userid, lessonid FROM {lesson_branch} b1
  639. WHERE b1.lessonid = :b1lessonid
  640. UNION
  641. SELECT userid, lessonid FROM {lesson_timer} c1
  642. WHERE c1.lessonid = :c1lessonid
  643. ) a ON u.id = a.userid
  644. JOIN ($esql) ue ON ue.id = a.userid
  645. ORDER BY $sort";
  646. $students = $DB->get_recordset_sql($sql, $params);
  647. if (!$students->valid()) {
  648. $students->close();
  649. return array(false, false);
  650. }
  651. } else {
  652. return array(false, false);
  653. }
  654. if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
  655. $grades = array();
  656. }
  657. if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
  658. $times = array();
  659. }
  660. // Build an array for output.
  661. $studentdata = array();
  662. $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
  663. foreach ($attempts as $attempt) {
  664. // if the user is not in the array or if the retry number is not in the sub array, add the data for that try.
  665. if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
  666. // restore/setup defaults
  667. $n = 0;
  668. $timestart = 0;
  669. $timeend = 0;
  670. $usergrade = null;
  671. $eol = 0;
  672. // search for the grade record for this try. if not there, the nulls defined above will be used.
  673. foreach($grades as $grade) {
  674. // check to see if the grade matches the correct user
  675. if ($grade->userid == $attempt->userid) {
  676. // see if n is = to the retry
  677. if ($n == $attempt->retry) {
  678. // get grade info
  679. $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
  680. break;
  681. }
  682. $n++; // if not equal, then increment n
  683. }
  684. }
  685. $n = 0;
  686. // search for the time record for this try. if not there, the nulls defined above will be used.
  687. foreach($times as $time) {
  688. // check to see if the grade matches the correct user
  689. if ($time->userid == $attempt->userid) {
  690. // see if n is = to the retry
  691. if ($n == $attempt->retry) {
  692. // get grade info
  693. $timeend = $time->lessontime;
  694. $timestart = $time->starttime;
  695. $eol = $time->completed;
  696. break;
  697. }
  698. $n++; // if not equal, then increment n
  699. }
  700. }
  701. // build up the array.
  702. // this array represents each student and all of their tries at the lesson
  703. $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
  704. "timeend" => $timeend,
  705. "grade" => $usergrade,
  706. "end" => $eol,
  707. "try" => $attempt->retry,
  708. "userid" => $attempt->userid);
  709. }
  710. }
  711. $attempts->close();
  712. $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
  713. foreach ($branches as $branch) {
  714. // If the user is not in the array or if the retry number is not in the sub array, add the data for that try.
  715. if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
  716. // Restore/setup defaults.
  717. $n = 0;
  718. $timestart = 0;
  719. $timeend = 0;
  720. $usergrade = null;
  721. $eol = 0;
  722. // Search for the time record for this try. if not there, the nulls defined above will be used.
  723. foreach ($times as $time) {
  724. // Check to see if the grade matches the correct user.
  725. if ($time->userid == $branch->userid) {
  726. // See if n is = to the retry.
  727. if ($n == $branch->retry) {
  728. // Get grade info.
  729. $timeend = $time->lessontime;
  730. $timestart = $time->starttime;
  731. $eol = $time->completed;
  732. break;
  733. }
  734. $n++; // If not equal, then increment n.
  735. }
  736. }
  737. // Build up the array.
  738. // This array represents each student and all of their tries at the lesson.
  739. $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
  740. "timeend" => $timeend,
  741. "grade" => $usergrade,
  742. "end" => $eol,
  743. "try" => $branch->retry,
  744. "userid" => $branch->userid);
  745. }
  746. }
  747. $branches->close();
  748. // Need the same thing for timed entries that were not completed.
  749. foreach ($times as $time) {
  750. $endoflesson = $time->completed;
  751. // If the time start is the same with another record then we shouldn't be adding another item to this array.
  752. if (isset($studentdata[$time->userid])) {
  753. $foundmatch = false;
  754. $n = 0;
  755. foreach ($studentdata[$time->userid] as $key => $value) {
  756. if ($value['timestart'] == $time->starttime) {
  757. // Don't add this to the array.
  758. $foundmatch = true;
  759. break;
  760. }
  761. }
  762. $n = count($studentdata[$time->userid]) + 1;
  763. if (!$foundmatch) {
  764. // Add a record.
  765. $studentdata[$time->userid][] = array(
  766. "timestart" => $time->starttime,
  767. "timeend" => $time->lessontime,
  768. "grade" => null,
  769. "end" => $endoflesson,
  770. "try" => $n,
  771. "userid" => $time->userid
  772. );
  773. }
  774. } else {
  775. $studentdata[$time->userid][] = array(
  776. "timestart" => $time->starttime,
  777. "timeend" => $time->lessontime,
  778. "grade" => null,
  779. "end" => $endoflesson,
  780. "try" => 0,
  781. "userid" => $time->userid
  782. );
  783. }
  784. }
  785. // To store all the data to be returned by the function.
  786. $data = new stdClass();
  787. // Determine if lesson should have a score.
  788. if ($branchcount > 0 AND $questioncount == 0) {
  789. // This lesson only contains content pages and is not graded.
  790. $data->lessonscored = false;
  791. } else {
  792. // This lesson is graded.
  793. $data->lessonscored = true;
  794. }
  795. // set all the stats variables
  796. $data->numofattempts = 0;
  797. $data->avescore = 0;
  798. $data->avetime = 0;
  799. $data->highscore = null;
  800. $data->lowscore = null;
  801. $data->hightime = null;
  802. $data->lowtime = null;
  803. $data->students = array();
  804. $table = new html_table();
  805. $headers = [get_string('name')];
  806. foreach ($extrafields as $field) {
  807. $headers[] = \core_user\fields::get_display_name($field);
  808. }
  809. $caneditlesson = has_capability('mod/lesson:edit', $context);
  810. $attemptsheader = get_string('attempts', 'lesson');
  811. if ($caneditlesson) {
  812. $selectall = get_string('selectallattempts', 'lesson');
  813. $deselectall = get_string('deselectallattempts', 'lesson');
  814. // Build the select/deselect all control.
  815. $selectallid = 'selectall-attempts';
  816. $mastercheckbox = new \core\output\checkbox_toggleall('lesson-attempts', true, [
  817. 'id' => $selectallid,
  818. 'name' => $selectallid,
  819. 'value' => 1,
  820. 'label' => $selectall,
  821. 'selectall' => $selectall,
  822. 'deselectall' => $deselectall,
  823. 'labelclasses' => 'form-check-label'
  824. ]);
  825. $attemptsheader = $OUTPUT->render($mastercheckbox);
  826. }
  827. $headers [] = $attemptsheader;
  828. // Set up the table object.
  829. if ($data->lessonscored) {
  830. $headers [] = get_string('highscore', 'lesson');
  831. }
  832. $colcount = count($headers);
  833. $table->head = $headers;
  834. $table->align = [];
  835. $table->align = array_pad($table->align, $colcount, 'center');
  836. $table->align[$colcount - 1] = 'left';
  837. if ($data->lessonscored) {
  838. $table->align[$colcount - 2] = 'left';
  839. }
  840. $table->attributes['class'] = 'table table-striped';
  841. // print out the $studentdata array
  842. // going through each student that has attempted the lesson, so, each student should have something to be displayed
  843. foreach ($students as $student) {
  844. // check to see if the student has attempts to print out
  845. if (array_key_exists($student->id, $studentdata)) {
  846. // set/reset some variables
  847. $attempts = array();
  848. $dataforstudent = new stdClass;
  849. $dataforstudent->attempts = array();
  850. // gather the data for each user attempt
  851. $bestgrade = 0;
  852. // $tries holds all the tries/retries a student has done
  853. $tries = $studentdata[$student->id];
  854. $studentname = fullname($student, true);
  855. foreach ($tries as $try) {
  856. $dataforstudent->attempts[] = $try;
  857. // Start to build up the checkbox and link.
  858. $attempturlparams = [
  859. 'id' => $cm->id,
  860. 'action' => 'reportdetail',
  861. 'userid' => $try['userid'],
  862. 'try' => $try['try'],
  863. ];
  864. if ($try["grade"] !== null) { // if null then not done yet
  865. // this is what the link does when the user has completed the try
  866. $timetotake = $try["timeend"] - $try["timestart"];
  867. if ($try["grade"] > $bestgrade) {
  868. $bestgrade = $try["grade"];
  869. }
  870. $attemptdata = (object)[
  871. 'grade' => $try["grade"],
  872. 'timestart' => userdate($try["timestart"]),
  873. 'duration' => format_time($timetotake),
  874. ];
  875. $attemptlinkcontents = get_string('attemptinfowithgrade', 'lesson', $attemptdata);
  876. } else {
  877. if ($try["end"]) {
  878. // User finished the lesson but has no grade. (Happens when there are only content pages).
  879. $timetotake = $try["timeend"] - $try["timestart"];
  880. $attemptdata = (object)[
  881. 'timestart' => userdate($try["timestart"]),
  882. 'duration' => format_time($timetotake),
  883. ];
  884. $attemptlinkcontents = get_string('attemptinfonograde', 'lesson', $attemptdata);
  885. } else {
  886. // This is what the link does/looks like when the user has not completed the attempt.
  887. if ($try['timestart'] !== 0) {
  888. // Teacher previews do not track time spent.
  889. $attemptlinkcontents = get_string("notcompletedwithdate", "lesson", userdate($try["timestart"]));
  890. } else {
  891. $attemptlinkcontents = get_string("notcompleted", "lesson");
  892. }
  893. $timetotake = null;
  894. }
  895. }
  896. $attempturl = new moodle_url('/mod/lesson/report.php', $attempturlparams);
  897. $attemptlink = html_writer::link($attempturl, $attemptlinkcontents, ['class' => 'lesson-attempt-link']);
  898. if ($caneditlesson) {
  899. $attemptid = 'attempt-' . $try['userid'] . '-' . $try['try'];
  900. $attemptname = 'attempts[' . $try['userid'] . '][' . $try['try'] . ']';
  901. $checkbox = new \core\output\checkbox_toggleall('lesson-attempts', false, [
  902. 'id' => $attemptid,
  903. 'name' => $attemptname,
  904. 'label' => $attemptlink
  905. ]);
  906. $attemptlink = $OUTPUT->render($checkbox);
  907. }
  908. // build up the attempts array
  909. $attempts[] = $attemptlink;
  910. // Run these lines for the stats only if the user finnished the lesson.
  911. if ($try["end"]) {
  912. // User has completed the lesson.
  913. $data->numofattempts++;
  914. $data->avetime += $timetotake;
  915. if ($timetotake > $data->hightime || $data->hightime == null) {
  916. $data->hightime = $timetotake;
  917. }
  918. if ($timetotake < $data->lowtime || $data->lowtime == null) {
  919. $data->lowtime = $timetotake;
  920. }
  921. if ($try["grade"] !== null) {
  922. // The lesson was scored.
  923. $data->avescore += $try["grade"];
  924. if ($try["grade"] > $data->highscore || $data->highscore === null) {
  925. $data->highscore = $try["grade"];
  926. }
  927. if ($try["grade"] < $data->lowscore || $data->lowscore === null) {
  928. $data->lowscore = $try["grade"];
  929. }
  930. }
  931. }
  932. }
  933. // get line breaks in after each attempt
  934. $attempts = implode("<br />\n", $attempts);
  935. $row = [$studentname];
  936. foreach ($extrafields as $field) {
  937. $row[] = $student->$field;
  938. }
  939. $row[] = $attempts;
  940. if ($data->lessonscored) {
  941. // Add the grade if the lesson is graded.
  942. $row[] = $bestgrade."%";
  943. }
  944. $table->data[] = $row;
  945. // Add the student data.
  946. $dataforstudent->id = $student->id;
  947. $dataforstudent->fullname = $studentname;
  948. $dataforstudent->bestgrade = $bestgrade;
  949. $data->students[] = $dataforstudent;
  950. }
  951. }
  952. $students->close();
  953. if ($data->numofattempts > 0) {
  954. $data->avescore = $data->avescore / $data->numofattempts;
  955. }
  956. return array($table, $data);
  957. }
  958. /**
  959. * Return information about one user attempt (including answers)
  960. * @param lesson $lesson lesson instance
  961. * @param int $userid the user id
  962. * @param int $attempt the attempt number
  963. * @return array the user answers (array) and user data stats (object)
  964. * @since Moodle 3.3
  965. */
  966. function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
  967. global $DB;
  968. $context = $lesson->context;
  969. if (!empty($userid)) {
  970. // Apply overrides.
  971. $lesson->update_effective_access($userid);
  972. }
  973. $pageid = 0;
  974. $lessonpages = $lesson->load_all_pages();
  975. foreach ($lessonpages as $lessonpage) {
  976. if ($lessonpage->prevpageid == 0) {
  977. $pageid = $lessonpage->id;
  978. }
  979. }
  980. // now gather the stats into an object
  981. $firstpageid = $pageid;
  982. $pagestats = array();
  983. while ($pageid != 0) { // EOL
  984. $page = $lessonpages[$pageid];
  985. $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
  986. if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
  987. // get them ready for processing
  988. $orderedanswers = array();
  989. foreach ($allanswers as $singleanswer) {
  990. // ordering them like this, will help to find the single attempt record that we want to keep.
  991. $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
  992. }
  993. // this is foreach user and for each try for that user, keep one attempt record
  994. foreach ($orderedanswers as $orderedanswer) {
  995. foreach($orderedanswer as $tries) {
  996. $page->stats($pagestats, $tries);
  997. }
  998. }
  999. } else {
  1000. // no one answered yet...
  1001. }
  1002. //unset($orderedanswers); initialized above now
  1003. $pageid = $page->nextpageid;
  1004. }
  1005. $manager = lesson_page_type_manager::get($lesson);
  1006. $qtypes = $manager->get_page_type_strings();
  1007. $answerpages = array();
  1008. $answerpage = "";
  1009. $pageid = $firstpageid;
  1010. // cycle through all the pages
  1011. // foreach page, add to the $answerpages[] array all the data that is needed
  1012. // from the question, the users attempt, and the statistics
  1013. // grayout pages that the user did not answer and Branch, end of branch, cluster
  1014. // and end of cluster pages
  1015. while ($pageid != 0) { // EOL
  1016. $page = $lessonpages[$pageid];
  1017. $answerpage = new stdClass;
  1018. // Keep the original page object.
  1019. $answerpage->page = $page;
  1020. $data ='';
  1021. $answerdata = new stdClass;
  1022. // Set some defaults for the answer data.
  1023. $answerdata->score = null;
  1024. $answerdata->response = null;
  1025. $answerdata->responseformat = FORMAT_PLAIN;
  1026. $answerpage->title = format_string($page->title);
  1027. $options = new stdClass;
  1028. $options->noclean = true;
  1029. $options->overflowdiv = true;
  1030. $options->context = $context;
  1031. $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
  1032. $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
  1033. $answerpage->grayout = $page->grayout;
  1034. $answerpage->context = $context;
  1035. if (empty($userid)) {
  1036. // there is no userid, so set these vars and display stats.
  1037. $answerpage->grayout = 0;
  1038. $useranswer = null;
  1039. } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
  1040. // get the user's answer for this page
  1041. // need to find the right one
  1042. $i = 0;
  1043. foreach ($useranswers as $userattempt) {
  1044. $useranswer = $userattempt;
  1045. $i++;
  1046. if ($lesson->maxattempts == $i) {
  1047. break; // reached maxattempts, break out
  1048. }
  1049. }
  1050. } else {
  1051. // user did not answer this page, gray it out and set some nulls
  1052. $answerpage->grayout = 1;
  1053. $useranswer = null;
  1054. }
  1055. $i = 0;
  1056. $n = 0;
  1057. $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
  1058. $pageid = $page->nextpageid;
  1059. }
  1060. $userstats = new stdClass;
  1061. if (!empty($userid)) {
  1062. $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
  1063. $alreadycompleted = true;
  1064. if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
  1065. $userstats->grade = -1;
  1066. $userstats->completed = -1;
  1067. $alreadycompleted = false;
  1068. } else {
  1069. $userstats->grade = current($grades);
  1070. $userstats->completed = $userstats->grade->completed;
  1071. $userstats->grade = round($userstats->grade->grade, 2);
  1072. }
  1073. if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
  1074. $userstats->timetotake = -1;
  1075. $alreadycompleted = false;
  1076. } else {
  1077. $userstats->timetotake = current($times);
  1078. $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
  1079. }
  1080. if ($alreadycompleted) {
  1081. $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
  1082. }
  1083. }
  1084. return array($answerpages, $userstats);
  1085. }
  1086. /**
  1087. * Return user's deadline for all lessons in a course, hereby taking into account group and user overrides.
  1088. *
  1089. * @param int $courseid the course id.
  1090. * @return object An object with of all lessonsids and close unixdates in this course,
  1091. * taking into account the most lenient overrides, if existing and 0 if no close date is set.
  1092. */
  1093. function lesson_get_user_deadline($courseid) {
  1094. global $DB, $USER;
  1095. // For teacher and manager/admins return lesson's deadline.
  1096. if (has_capability('moodle/course:update', context_course::instance($courseid))) {
  1097. $sql = "SELECT lesson.id, lesson.deadline AS userdeadline
  1098. FROM {lesson} lesson
  1099. WHERE lesson.course = :courseid";
  1100. $results = $DB->get_records_sql($sql, array('courseid' => $courseid));
  1101. return $results;
  1102. }
  1103. $sql = "SELECT a.id,
  1104. COALESCE(v.userclose, v.groupclose, a.deadline, 0) AS userdeadline
  1105. FROM (
  1106. SELECT lesson.id as lessonid,
  1107. MAX(leo.deadline) AS userclose, MAX(qgo.deadline) AS groupclose
  1108. FROM {lesson} lesson
  1109. LEFT JOIN {lesson_overrides} leo on lesson.id = leo.lessonid AND leo.userid = :userid
  1110. LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid
  1111. LEFT JOIN {lesson_overrides} qgo on lesson.id = qgo.lessonid AND qgo.groupid = gm.groupid
  1112. WHERE lesson.course = :courseid
  1113. GROUP BY lesson.id
  1114. ) v
  1115. JOIN {lesson} a ON a.id = v.lessonid";
  1116. $results = $DB->get_records_sql($sql, array('userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid));
  1117. return $results;
  1118. }
  1119. /**
  1120. * Abstract class that page type's MUST inherit from.
  1121. *
  1122. * This is the abstract class that ALL add page type forms must extend.
  1123. * You will notice that all but two of the methods this class contains are final.
  1124. * Essentially the only thing that extending classes can do is extend custom_definition.
  1125. * OR if it has a special requirement on creation it can extend construction_override
  1126. *
  1127. * @abstract
  1128. * @copyright 2009 Sam Hemelryk
  1129. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1130. */
  1131. abstract class lesson_add_page_form_base extends moodleform {
  1132. /**
  1133. * This is the classic define that is used to identify this pagetype.
  1134. * Will be one of LESSON_*
  1135. * @var int
  1136. */
  1137. public $qtype;
  1138. /**
  1139. * The simple string that describes the page type e.g. truefalse, multichoice
  1140. * @var string
  1141. */
  1142. public $qtypestring;
  1143. /**
  1144. * An array of options used in the htmleditor
  1145. * @var array
  1146. */
  1147. protected $editoroptions = array();
  1148. /**
  1149. * True if this is a standard page of false if it does something special.
  1150. * Questions are standard pages, branch tables are not
  1151. * @var bool
  1152. */
  1153. protected $standard = true;
  1154. /**
  1155. * Answer format supported by question type.
  1156. */
  1157. protected $answerformat = '';
  1158. /**
  1159. * Response format supported by question type.
  1160. */
  1161. protected $responseformat = '';
  1162. /**
  1163. * Each page type can and should override this to add any custom elements to
  1164. * the basic form that they want
  1165. */
  1166. public function custom_definition() {}
  1167. /**
  1168. * Returns answer format used by question type.
  1169. */
  1170. public function get_answer_format() {
  1171. return $this->answerformat;
  1172. }
  1173. /**
  1174. * Returns response format used by question type.
  1175. */
  1176. public function get_response_format() {
  1177. return $this->responseformat;
  1178. }
  1179. /**
  1180. * Used to determine if this is a standard page or a special page
  1181. * @return bool
  1182. */
  1183. public final function is_standard() {
  1184. return (bool)$this->standard;
  1185. }
  1186. /**
  1187. * Add the required basic elements to the form.
  1188. *
  1189. * This method adds the basic elements to the form including title and contents
  1190. * and then calls custom_definition();
  1191. */
  1192. public final function definition() {
  1193. global $CFG;
  1194. $mform = $this->_form;
  1195. $editoroptions = $this->_customdata['editoroptions'];
  1196. if ($this->qtypestring != 'selectaqtype') {
  1197. if ($this->_customdata['edit']) {
  1198. $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
  1199. } else {
  1200. $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
  1201. }
  1202. }
  1203. if (!empty($this->_customdata['returnto'])) {
  1204. $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
  1205. $mform->setType('returnto', PARAM_LOCALURL);
  1206. }
  1207. $mform->addElement('hidden', 'id');
  1208. $mform->setType('id', PARAM_INT);
  1209. $mform->addElement('hidden', 'pageid');
  1210. $mform->setType('pageid', PARAM_INT);
  1211. if ($this->standard === true) {
  1212. $mform->addElement('hidden', 'qtype');
  1213. $mform->setType('qtype', PARAM_INT);
  1214. $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
  1215. $mform->addRule('title', get_string('required'), 'required', null, 'client');
  1216. if (!empty($CFG->formatstringstriptags)) {
  1217. $mform->setType('title', PARAM_TEXT);
  1218. } else {
  1219. $mform->setType('title', PARAM_CLEANHTML);
  1220. }
  1221. $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
  1222. $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
  1223. $mform->setType('contents_editor', PARAM_RAW);
  1224. $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
  1225. }
  1226. $this->custom_definition();
  1227. if ($this->_customdata['edit'] === true) {
  1228. $mform->addElement('hidden', 'edit', 1);
  1229. $mform->setType('edit', PARAM_BOOL);
  1230. $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
  1231. } else if ($this->qtype === 'questiontype') {
  1232. $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
  1233. } else {
  1234. $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
  1235. }
  1236. }
  1237. /**
  1238. * Convenience function: Adds a jumpto select element
  1239. *
  1240. * @param string $name
  1241. * @param string|null $label
  1242. * @param int $selected The page to select by default
  1243. */
  1244. protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
  1245. $title = get_string("jump", "lesson");
  1246. if ($label === null) {
  1247. $label = $title;
  1248. }
  1249. if (is_int($name)) {
  1250. $name = "jumpto[$name]";
  1251. }
  1252. $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
  1253. $this->_form->setDefault($name, $selected);
  1254. $this->_form->addHelpButton($name, 'jumps', 'lesson');
  1255. }
  1256. /**
  1257. * Convenience function: Adds a score input element
  1258. *
  1259. * @param string $name
  1260. * @param string|null $label
  1261. * @param mixed $value The default value
  1262. */
  1263. protected final function add_score($name, $label=null, $value=null) {
  1264. if ($label === null) {
  1265. $label = get_string("score", "lesson");
  1266. }
  1267. if (is_int($name)) {
  1268. $name = "score[$name]";
  1269. }
  1270. $this->_form->addElement('text', $name, $label, array('size'=>5));
  1271. $this->_form->setType($name, PARAM_INT);
  1272. if ($value !== null) {
  1273. $this->_form->setDefault($name, $value);
  1274. }
  1275. $this->_form->addHelpButton($name, 'score', 'lesson');
  1276. // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
  1277. if (!$this->_customdata['lesson']->custom) {
  1278. $this->_form->freeze($name);
  1279. }
  1280. }
  1281. /**
  1282. * Convenience function: Adds an answer editor
  1283. *
  1284. * @param int $count The count of the element to add
  1285. * @param string $label, null means default
  1286. * @param bool $required
  1287. * @param string $format
  1288. * @param array $help Add help text via the addHelpButton. Must be an array which contains the string identifier and
  1289. * component as it's elements
  1290. * @return void
  1291. */
  1292. protected final function add_answer($count, $label = null, $required = false, $format= '', array $help = []) {
  1293. if ($label === null) {
  1294. $label = get_string('answer', 'lesson');
  1295. }
  1296. if ($format == LESSON_ANSWER_HTML) {
  1297. $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
  1298. array('rows' => '4', 'columns' => '80'),
  1299. array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
  1300. $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
  1301. $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
  1302. } else {
  1303. $this->_form->addElement('text', 'answer_editor['.$count.']', $label,
  1304. array('size' => '50', 'maxlength' => '200'));
  1305. $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
  1306. }
  1307. if ($required) {
  1308. $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
  1309. }
  1310. if ($help) {
  1311. $this->_form->addHelpButton("answer_editor[$count]", $help['identifier'], $help['component']);
  1312. }
  1313. }
  1314. /**
  1315. * Convenience function: Adds an response editor
  1316. *
  1317. * @param int $count The count of the element to add
  1318. * @param string $label, null means default
  1319. * @param bool $required
  1320. * @return void
  1321. */
  1322. protected final function add_response($count, $label = null, $required = false) {
  1323. if ($label === null) {
  1324. $label = get_string('response', 'lesson');
  1325. }
  1326. $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
  1327. array('rows' => '4', 'columns' => '80'),
  1328. array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
  1329. $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
  1330. $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
  1331. if ($required) {
  1332. $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
  1333. }
  1334. }
  1335. /**
  1336. * A function that gets called upon init of this object by the calling script.
  1337. *
  1338. * This can be used to process an immediate action if required. Currently it
  1339. * is only used in special cases by non-standard page types.
  1340. *
  1341. * @return bool
  1342. */
  1343. public function construction_override($pageid, lesson $lesson) {
  1344. return true;
  1345. }
  1346. }
  1347. /**
  1348. * Class representation of a lesson
  1349. *
  1350. * This class is used the interact with, and manage a lesson once instantiated.
  1351. * If you need to fetch a lesson object you can do so by calling
  1352. *
  1353. * <code>
  1354. * lesson::load($lessonid);
  1355. * // or
  1356. * $lessonrecord = $DB->get_record('lesson', $lessonid);
  1357. * $lesson = new lesson($lessonrecord);
  1358. * </code>
  1359. *
  1360. * The class itself extends lesson_base as all classes within the lesson module should
  1361. *
  1362. * These properties are from the database
  1363. * @property int $id The id of this lesson
  1364. * @property int $course The ID of the course this lesson belongs to
  1365. * @property string $name The name of this lesson
  1366. * @property int $practice Flag to toggle this as a practice lesson
  1367. * @property int $modattempts Toggle to allow the user to go back and review answers
  1368. * @property int $usepassword Toggle the use of a password for entry
  1369. * @property string $password The password to require users to enter
  1370. * @property int $dependency ID of another lesson this lesson is dependent on
  1371. * @property string $conditions Conditions of the lesson dependency
  1372. * @property int $grade The maximum grade a user can achieve (%)
  1373. * @property int $custom Toggle custom scoring on or off
  1374. * @property int $ongoing Toggle display of an ongoing score
  1375. * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
  1376. * @property int $maxanswers The max number of answers or branches
  1377. * @property int $maxattempts The maximum number of attempts a user can record
  1378. * @property int $review Toggle use or wrong answer review button
  1379. * @property int $nextpagedefault Override the default next page
  1380. * @property int $feedback Toggles display of default feedback
  1381. * @property int $minquestions Sets a minimum value of pages seen when calculating grades
  1382. * @property int $maxpages Maximum number of pages this lesson can contain
  1383. * @property int $retake Flag to allow users to retake a lesson
  1384. * @property int $activitylink Relate this lesson to another lesson
  1385. * @property string $mediafile File to pop up to or webpage to display
  1386. * @property int $mediaheight Sets the height of the media file popup
  1387. * @property int $mediawidth Sets the width of the media file popup
  1388. * @property int $mediaclose Toggle display of a media close button
  1389. * @property int $slideshow Flag for whether branch pages should be shown as slideshows
  1390. * @property int $width Width of slideshow
  1391. * @property int $height Height of slideshow
  1392. * @property string $bgcolor Background colour of slideshow
  1393. * @property int $displayleft Display a left menu
  1394. * @property int $displayleftif Sets the condition on which the left menu is displayed
  1395. * @property int $progressbar Flag to toggle display of a lesson progress bar
  1396. * @property int $available Timestamp of when this lesson becomes available
  1397. * @property int $deadline Timestamp of when this lesson is no longer available
  1398. * @property int $timemodified Timestamp when lesson was last modified
  1399. * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app
  1400. *
  1401. * These properties are calculated
  1402. * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
  1403. * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
  1404. *
  1405. * @copyright 2009 Sam Hemelryk
  1406. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  1407. */
  1408. class lesson extends lesson_base {
  1409. /**
  1410. * The id of the first page (where prevpageid = 0) gets set and retrieved by
  1411. * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
  1412. * @var int
  1413. */
  1414. protected $firstpageid = null;
  1415. /**
  1416. * The id of the last page (where nextpageid = 0) gets set and retrieved by
  1417. * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
  1418. * @var int
  1419. */
  1420. protected $lastpageid = null;
  1421. /**
  1422. * An array used to cache the pages associated with this lesson after the first
  1423. * time they have been loaded.
  1424. * A note to developers: If you are going to be working with MORE than one or
  1425. * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
  1426. * in order to save excess database queries.
  1427. * @var array An array of lesson_page objects
  1428. */
  1429. protected $pages = array();
  1430. /**
  1431. * Flag that gets set to true once all of the pages associated with the lesson
  1432. * have been loaded.
  1433. * @var bool
  1434. */
  1435. protected $loadedallpages = false;
  1436. /**
  1437. * Course module object gets set and retrieved by directly calling <code>$lesson->cm;</code>
  1438. * @see get_cm()
  1439. * @var stdClass
  1440. */
  1441. protected $cm = null;
  1442. /**
  1443. * Course object gets set and retrieved by directly calling <code>$lesson->courserecord;</code>
  1444. * @see get_courserecord()
  1445. * @var stdClass
  1446. */
  1447. protected $courserecord = null;
  1448. /**
  1449. * Context object gets set and retrieved by directly calling <code>$lesson->context;</code>
  1450. * @see get_context()
  1451. * @var stdClass
  1452. */
  1453. protected $context = null;
  1454. /**
  1455. * Constructor method
  1456. *
  1457. * @param object $properties
  1458. * @param stdClass $cm course module object
  1459. * @param stdClass $course course object
  1460. * @since Moodle 3.3
  1461. */
  1462. public function __construct($properties, $cm = null, $course = null) {
  1463. parent::__construct($properties);
  1464. $this->cm = $cm;
  1465. $this->courserecord = $course;
  1466. }
  1467. /**
  1468. * Simply generates a lesson object given an array/object of properties
  1469. * Overrides {@see lesson_base->create()}
  1470. * @static
  1471. * @param object|array $properties
  1472. * @return lesson
  1473. */
  1474. public static function create($properties) {
  1475. return new lesson($properties);
  1476. }
  1477. /**
  1478. * Generates a lesson object from the database given its id
  1479. * @static
  1480. * @param int $lessonid
  1481. * @return lesson
  1482. */
  1483. public static function load($lessonid) {
  1484. global $DB;
  1485. if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
  1486. print_error('invalidcoursemodule');
  1487. }
  1488. return new lesson($lesson);
  1489. }
  1490. /**
  1491. * Deletes this lesson from the database
  1492. */
  1493. public function delete() {
  1494. global $CFG, $DB;
  1495. require_once($CFG->libdir.'/gradelib.php');
  1496. require_once($CFG->dirroot.'/calendar/lib.php');
  1497. $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
  1498. $context = context_module::instance($cm->id);
  1499. $this->delete_all_overrides();
  1500. grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
  1501. // We must delete the module record after we delete the grade item.
  1502. $DB->delete_records("lesson", array("id"=>$this->properties->id));
  1503. $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
  1504. $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
  1505. $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
  1506. $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
  1507. $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
  1508. $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
  1509. if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
  1510. $coursecontext = context_course::instance($cm->course);
  1511. foreach($events as $event) {
  1512. $event->context = $coursecontext;
  1513. $event = calendar_event::load($event);
  1514. $event->delete();
  1515. }
  1516. }
  1517. // Delete files associated with this module.
  1518. $fs = get_file_storage();
  1519. $fs->delete_area_files($context->id);
  1520. return true;
  1521. }
  1522. /**
  1523. * Deletes a lesson override from the database and clears any corresponding calendar events
  1524. *
  1525. * @param int $overrideid The id of the override being deleted
  1526. * @return bool true on success
  1527. */
  1528. public function delete_override($overrideid) {
  1529. global $CFG, $DB;
  1530. require_once($CFG->dirroot . '/calendar/lib.php');
  1531. $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
  1532. $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST);
  1533. // Delete the events.
  1534. $conds = array('modulename' => 'lesson',
  1535. 'instance' => $this->properties->id);
  1536. if (isset($override->userid)) {
  1537. $conds['userid'] = $override->userid;
  1538. $cachekey = "{$cm->instance}_u_{$override->userid}";
  1539. } else {
  1540. $conds['groupid'] = $override->groupid;
  1541. $cachekey = "{$cm->instance}_g_{$override->groupid}";
  1542. }
  1543. $events = $DB->get_records('event', $conds);
  1544. foreach ($events as $event) {
  1545. $eventold = calendar_event::load($event);
  1546. $eventold->delete();
  1547. }
  1548. $DB->delete_records('lesson_overrides', array('id' => $overrideid));
  1549. cache::make('mod_lesson', 'overrides')->delete($cachekey);
  1550. // Set the common parameters for one of the events we will be triggering.
  1551. $params = array(
  1552. 'objectid' => $override->id,
  1553. 'context' => context_module::instance($cm->id),
  1554. 'other' => array(
  1555. 'lessonid' => $override->lessonid
  1556. )
  1557. );
  1558. // Determine which override deleted event to fire.
  1559. if (!empty($override->userid)) {
  1560. $params['relateduserid'] = $override->userid;
  1561. $event = \mod_lesson\event\user_override_deleted::create($params);
  1562. } else {
  1563. $params['other']['groupid'] = $override->groupid;
  1564. $event = \mod_lesson\event\group_override_deleted::create($params);
  1565. }
  1566. // Trigger the override deleted event.
  1567. $event->add_record_snapshot('lesson_overrides', $override);
  1568. $event->trigger();
  1569. return true;
  1570. }
  1571. /**
  1572. * Deletes all lesson overrides from the database and clears any corresponding calendar events
  1573. */
  1574. public function delete_all_overrides() {
  1575. global $DB;
  1576. $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id');
  1577. foreach ($overrides as $override) {
  1578. $this->delete_override($override->id);
  1579. }
  1580. }
  1581. /**
  1582. * Checks user enrollment in the current course.
  1583. *
  1584. * @param int $userid
  1585. * @return null|stdClass user record
  1586. */
  1587. public function is_participant($userid) {
  1588. return is_enrolled($this->get_context(), $userid, 'mod/lesson:view', $this->show_only_active_users());
  1589. }
  1590. /**
  1591. * Check is only active users in course should be shown.
  1592. *
  1593. * @return bool true if only active users should be shown.
  1594. */
  1595. public function show_only_active_users() {
  1596. return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
  1597. }
  1598. /**
  1599. * Updates the lesson properties with override information for a user.
  1600. *
  1601. * Algorithm: For each lesson setting, if there is a matching user-specific override,
  1602. * then use that otherwise, if there are group-specific overrides, return the most
  1603. * lenient combination of them. If neither applies, leave the quiz setting unchanged.
  1604. *
  1605. * Special case: if there is more than one password that applies to the user, then
  1606. * lesson->extrapasswords will contain an array of strings giving the remaining
  1607. * passwords.
  1608. *
  1609. * @param int $userid The userid.
  1610. */
  1611. public function update_effective_access($userid) {
  1612. global $DB;
  1613. // Check for user override.
  1614. $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid));
  1615. if (!$override) {
  1616. $override = new stdClass();
  1617. $override->available = null;
  1618. $override->deadline = null;
  1619. $override->timelimit = null;
  1620. $override->review = null;
  1621. $override->maxattempts = null;
  1622. $override->retake = null;
  1623. $override->password = null;
  1624. }
  1625. // Check for group overrides.
  1626. $groupings = groups_get_user_groups($this->properties->course, $userid);
  1627. if (!empty($groupings[0])) {
  1628. // Select all overrides that apply to the User's groups.
  1629. list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
  1630. $sql = "SELECT * FROM {lesson_overrides}
  1631. WHERE groupid $extra AND lessonid = ?";
  1632. $params[] = $this->properties->id;
  1633. $records = $DB->get_records_sql($sql, $params);
  1634. // Combine the overrides.
  1635. $availables = array();
  1636. $deadlines = array();
  1637. $timelimits = array();
  1638. $reviews = array();
  1639. $attempts = array();
  1640. $retakes = array();
  1641. $passwords = array();
  1642. foreach ($records as $gpoverride) {
  1643. if (isset($gpoverride->available)) {
  1644. $availables[] = $gpoverride->available;
  1645. }
  1646. if (isset($gpoverride->deadline)) {
  1647. $deadlines[] = $gpoverride->deadline;
  1648. }
  1649. if (isset($gpoverride->timelimit)) {
  1650. $timelimits[] = $gpoverride->timelimit;
  1651. }
  1652. if (isset($gpoverride->review)) {
  1653. $reviews[] = $gpoverride->review;
  1654. }
  1655. if (isset($gpoverride->maxattempts)) {
  1656. $attempts[] = $gpoverride->maxattempts;
  1657. }
  1658. if (isset($gpoverride->retake)) {
  1659. $retakes[] = $gpoverride->retake;
  1660. }
  1661. if (isset($gpoverride->password)) {
  1662. $passwords[] = $gpoverride->password;
  1663. }
  1664. }
  1665. // If there is a user override for a setting, ignore the group override.
  1666. if (is_null($override->available) && count($availables)) {
  1667. $override->available = min($availables);
  1668. }
  1669. if (is_null($override->deadline) && count($deadlines)) {
  1670. if (in_array(0, $deadlines)) {
  1671. $override->deadline = 0;
  1672. } else {
  1673. $override->deadline = max($deadlines);
  1674. }
  1675. }
  1676. if (is_null($override->timelimit) && count($timelimits)) {
  1677. if (in_array(0, $timelimits)) {
  1678. $override->timelimit = 0;
  1679. } else {
  1680. $override->timelimit = max($timelimits);
  1681. }
  1682. }
  1683. if (is_null($override->review) && count($reviews)) {
  1684. $override->review = max($reviews);
  1685. }
  1686. if (is_null($override->maxattempts) && count($attempts)) {
  1687. $override->maxattempts = max($attempts);
  1688. }
  1689. if (is_null($override->retake) && count($retakes)) {
  1690. $override->retake = max($retakes);
  1691. }
  1692. if (is_null($override->password) && count($passwords)) {
  1693. $override->password = array_shift($passwords);
  1694. if (count($passwords)) {
  1695. $override->extrapasswords = $passwords;
  1696. }
  1697. }
  1698. }
  1699. // Merge with lesson defaults.
  1700. $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake');
  1701. foreach ($keys as $key) {
  1702. if (isset($override->{$key})) {
  1703. $this->properties->{$key} = $override->{$key};
  1704. }
  1705. }
  1706. // Special handling of lesson usepassword and password.
  1707. if (isset($override->password)) {
  1708. if ($override->password == '') {
  1709. $this->properties->usepassword = 0;
  1710. } else {
  1711. $this->properties->usepassword = 1;
  1712. $this->properties->password = $override->password;
  1713. if (isset($override->extrapasswords)) {
  1714. $this->properties->extrapasswords = $override->extrapasswords;
  1715. }
  1716. }
  1717. }
  1718. }
  1719. /**
  1720. * Fetches messages from the session that may have been set in previous page
  1721. * actions.
  1722. *
  1723. * <code>
  1724. * // Do not call this method directly instead use
  1725. * $lesson->messages;
  1726. * </code>
  1727. *
  1728. * @return array
  1729. */
  1730. protected function get_messages() {
  1731. global $SESSION;
  1732. $messages = array();
  1733. if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
  1734. $messages = $SESSION->lesson_messages[$this->properties->id];
  1735. unset($SESSION->lesson_messages[$this->properties->id]);
  1736. }
  1737. return $messages;
  1738. }
  1739. /**
  1740. * Get all of the attempts for the current user.
  1741. *
  1742. * @param int $retries
  1743. * @param bool $correct Optional: only fetch correct attempts
  1744. * @param int $pageid Optional: only fetch attempts at the given page
  1745. * @param int $userid Optional: defaults to the current user if not set
  1746. * @return array|false
  1747. */
  1748. public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
  1749. global $USER, $DB;
  1750. $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
  1751. if ($correct) {
  1752. $params['correct'] = 1;
  1753. }
  1754. if ($pageid !== null) {
  1755. $params['pageid'] = $pageid;
  1756. }
  1757. if ($userid === null) {
  1758. $params['userid'] = $USER->id;
  1759. }
  1760. return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
  1761. }
  1762. /**
  1763. * Get a list of content pages (formerly known as branch tables) viewed in the lesson for the given user during an attempt.
  1764. *
  1765. * @param int $lessonattempt the lesson attempt number (also known as retries)
  1766. * @param int $userid the user id to retrieve the data from
  1767. * @param string $sort an order to sort the results in (a valid SQL ORDER BY parameter)
  1768. * @param string $fields a comma separated list of fields to return
  1769. * @return array of pages
  1770. * @since Moodle 3.3
  1771. */
  1772. public function get_content_pages_viewed($lessonattempt, $userid = null, $sort = '', $fields = '*') {
  1773. global $USER, $DB;
  1774. if ($userid === null) {
  1775. $userid = $USER->id;
  1776. }
  1777. $conditions = array("lessonid" => $this->properties->id, "userid" => $userid, "retry" => $lessonattempt);
  1778. return $DB->get_records('lesson_branch', $conditions, $sort, $fields);
  1779. }
  1780. /**
  1781. * Returns the first page for the lesson or false if there isn't one.
  1782. *
  1783. * This method should be called via the magic method __get();
  1784. * <code>
  1785. * $firstpage = $lesson->firstpage;
  1786. * </code>
  1787. *
  1788. * @return lesson_page|bool Returns the lesson_page specialised object or false
  1789. */
  1790. protected function get_firstpage() {
  1791. $pages = $this->load_all_pages();
  1792. if (count($pages) > 0) {
  1793. foreach ($pages as $page) {
  1794. if ((int)$page->prevpageid === 0) {
  1795. return $page;
  1796. }
  1797. }
  1798. }
  1799. return false;
  1800. }
  1801. /**
  1802. * Returns the last page for the lesson or false if there isn't one.
  1803. *
  1804. * This method should be called via the magic method __get();
  1805. * <code>
  1806. * $lastpage = $lesson->lastpage;
  1807. * </code>
  1808. *
  1809. * @return lesson_page|bool Returns the lesson_page specialised object or false
  1810. */
  1811. protected function get_lastpage() {
  1812. $pages = $this->load_all_pages();
  1813. if (count($pages) > 0) {
  1814. foreach ($pages as $page) {
  1815. if ((int)$page->nextpageid === 0) {
  1816. return $page;
  1817. }
  1818. }
  1819. }
  1820. return false;
  1821. }
  1822. /**
  1823. * Returns the id of the first page of this lesson. (prevpageid = 0)
  1824. * @return int
  1825. */
  1826. protected function get_firstpageid() {
  1827. global $DB;
  1828. if ($this->firstpageid == null) {
  1829. if (!$this->loadedallpages) {
  1830. $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
  1831. if (!$firstpageid) {
  1832. print_error('cannotfindfirstpage', 'lesson');
  1833. }
  1834. $this->firstpageid = $firstpageid;
  1835. } else {
  1836. $firstpage = $this->get_firstpage();
  1837. $this->firstpageid = $firstpage->id;
  1838. }
  1839. }
  1840. return $this->firstpageid;
  1841. }
  1842. /**
  1843. * Returns the id of the last page of this lesson. (nextpageid = 0)
  1844. * @return int
  1845. */
  1846. public function get_lastpageid() {
  1847. global $DB;
  1848. if ($this->lastpageid == null) {
  1849. if (!$this->loadedallpages) {
  1850. $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
  1851. if (!$lastpageid) {
  1852. print_error('cannotfindlastpage', 'lesson');
  1853. }
  1854. $this->lastpageid = $lastpageid;
  1855. } else {
  1856. $lastpageid = $this->get_lastpage();
  1857. $this->lastpageid = $lastpageid->id;
  1858. }
  1859. }
  1860. return $this->lastpageid;
  1861. }
  1862. /**
  1863. * Gets the next page id to display after the one that is provided.
  1864. * @param int $nextpageid
  1865. * @return bool
  1866. */
  1867. public function get_next_page($nextpageid) {
  1868. global $USER, $DB;
  1869. $allpages = $this->load_all_pages();
  1870. if ($this->properties->nextpagedefault) {
  1871. // in Flash Card mode...first get number of retakes
  1872. $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  1873. shuffle($allpages);
  1874. $found = false;
  1875. if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
  1876. foreach ($allpages as $nextpage) {
  1877. if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
  1878. $found = true;
  1879. break;
  1880. }
  1881. }
  1882. } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
  1883. foreach ($allpages as $nextpage) {
  1884. if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
  1885. $found = true;
  1886. break;
  1887. }
  1888. }
  1889. }
  1890. if ($found) {
  1891. if ($this->properties->maxpages) {
  1892. // check number of pages viewed (in the lesson)
  1893. if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
  1894. return LESSON_EOL;
  1895. }
  1896. }
  1897. return $nextpage->id;
  1898. }
  1899. }
  1900. // In a normal lesson mode
  1901. foreach ($allpages as $nextpage) {
  1902. if ((int)$nextpage->id === (int)$nextpageid) {
  1903. return $nextpage->id;
  1904. }
  1905. }
  1906. return LESSON_EOL;
  1907. }
  1908. /**
  1909. * Sets a message against the session for this lesson that will displayed next
  1910. * time the lesson processes messages
  1911. *
  1912. * @param string $message
  1913. * @param string $class
  1914. * @param string $align
  1915. * @return bool
  1916. */
  1917. public function add_message($message, $class="notifyproblem", $align='center') {
  1918. global $SESSION;
  1919. if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
  1920. $SESSION->lesson_messages = array();
  1921. $SESSION->lesson_messages[$this->properties->id] = array();
  1922. } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
  1923. $SESSION->lesson_messages[$this->properties->id] = array();
  1924. }
  1925. $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
  1926. return true;
  1927. }
  1928. /**
  1929. * Check if the lesson is accessible at the present time
  1930. * @return bool True if the lesson is accessible, false otherwise
  1931. */
  1932. public function is_accessible() {
  1933. $available = $this->properties->available;
  1934. $deadline = $this->properties->deadline;
  1935. return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
  1936. }
  1937. /**
  1938. * Starts the lesson time for the current user
  1939. * @return bool Returns true
  1940. */
  1941. public function start_timer() {
  1942. global $USER, $DB;
  1943. $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
  1944. false, MUST_EXIST);
  1945. // Trigger lesson started event.
  1946. $event = \mod_lesson\event\lesson_started::create(array(
  1947. 'objectid' => $this->properties()->id,
  1948. 'context' => context_module::instance($cm->id),
  1949. 'courseid' => $this->properties()->course
  1950. ));
  1951. $event->trigger();
  1952. $USER->startlesson[$this->properties->id] = true;
  1953. $timenow = time();
  1954. $startlesson = new stdClass;
  1955. $startlesson->lessonid = $this->properties->id;
  1956. $startlesson->userid = $USER->id;
  1957. $startlesson->starttime = $timenow;
  1958. $startlesson->lessontime = $timenow;
  1959. if (WS_SERVER) {
  1960. $startlesson->timemodifiedoffline = $timenow;
  1961. }
  1962. $DB->insert_record('lesson_timer', $startlesson);
  1963. if ($this->properties->timelimit) {
  1964. $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
  1965. }
  1966. return true;
  1967. }
  1968. /**
  1969. * Updates the timer to the current time and returns the new timer object
  1970. * @param bool $restart If set to true the timer is restarted
  1971. * @param bool $continue If set to true AND $restart=true then the timer
  1972. * will continue from a previous attempt
  1973. * @return stdClass The new timer
  1974. */
  1975. public function update_timer($restart=false, $continue=false, $endreached =false) {
  1976. global $USER, $DB;
  1977. $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
  1978. // clock code
  1979. // get time information for this user
  1980. if (!$timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
  1981. $this->start_timer();
  1982. $timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1);
  1983. }
  1984. $timer = current($timer); // This will get the latest start time record.
  1985. if ($restart) {
  1986. if ($continue) {
  1987. // continue a previous test, need to update the clock (think this option is disabled atm)
  1988. $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
  1989. // Trigger lesson resumed event.
  1990. $event = \mod_lesson\event\lesson_resumed::create(array(
  1991. 'objectid' => $this->properties->id,
  1992. 'context' => context_module::instance($cm->id),
  1993. 'courseid' => $this->properties->course
  1994. ));
  1995. $event->trigger();
  1996. } else {
  1997. // starting over, so reset the clock
  1998. $timer->starttime = time();
  1999. // Trigger lesson restarted event.
  2000. $event = \mod_lesson\event\lesson_restarted::create(array(
  2001. 'objectid' => $this->properties->id,
  2002. 'context' => context_module::instance($cm->id),
  2003. 'courseid' => $this->properties->course
  2004. ));
  2005. $event->trigger();
  2006. }
  2007. }
  2008. $timenow = time();
  2009. $timer->lessontime = $timenow;
  2010. if (WS_SERVER) {
  2011. $timer->timemodifiedoffline = $timenow;
  2012. }
  2013. $timer->completed = $endreached;
  2014. $DB->update_record('lesson_timer', $timer);
  2015. // Update completion state.
  2016. $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
  2017. false, MUST_EXIST);
  2018. $course = get_course($cm->course);
  2019. $completion = new completion_info($course);
  2020. if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) {
  2021. $completion->update_state($cm, COMPLETION_COMPLETE);
  2022. }
  2023. return $timer;
  2024. }
  2025. /**
  2026. * Updates the timer to the current time then stops it by unsetting the user var
  2027. * @return bool Returns true
  2028. */
  2029. public function stop_timer() {
  2030. global $USER, $DB;
  2031. unset($USER->startlesson[$this->properties->id]);
  2032. $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
  2033. false, MUST_EXIST);
  2034. // Trigger lesson ended event.
  2035. $event = \mod_lesson\event\lesson_ended::create(array(
  2036. 'objectid' => $this->properties()->id,
  2037. 'context' => context_module::instance($cm->id),
  2038. 'courseid' => $this->properties()->course
  2039. ));
  2040. $event->trigger();
  2041. return $this->update_timer(false, false, true);
  2042. }
  2043. /**
  2044. * Checks to see if the lesson has pages
  2045. */
  2046. public function has_pages() {
  2047. global $DB;
  2048. $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
  2049. return ($pagecount>0);
  2050. }
  2051. /**
  2052. * Returns the link for the related activity
  2053. * @return string
  2054. */
  2055. public function link_for_activitylink() {
  2056. global $DB;
  2057. $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
  2058. if ($module) {
  2059. $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
  2060. if ($modname) {
  2061. $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
  2062. if ($instancename) {
  2063. return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php',
  2064. array('id' => $this->properties->activitylink)), get_string('activitylinkname',
  2065. 'lesson', $instancename), array('class' => 'centerpadded lessonbutton standardbutton pr-3'));
  2066. }
  2067. }
  2068. }
  2069. return '';
  2070. }
  2071. /**
  2072. * Loads the requested page.
  2073. *
  2074. * This function will return the requested page id as either a specialised
  2075. * lesson_page object OR as a generic lesson_page.
  2076. * If the page has been loaded previously it will be returned from the pages
  2077. * array, otherwise it will be loaded from the database first
  2078. *
  2079. * @param int $pageid
  2080. * @return lesson_page A lesson_page object or an object that extends it
  2081. */
  2082. public function load_page($pageid) {
  2083. if (!array_key_exists($pageid, $this->pages)) {
  2084. $manager = lesson_page_type_manager::get($this);
  2085. $this->pages[$pageid] = $manager->load_page($pageid, $this);
  2086. }
  2087. return $this->pages[$pageid];
  2088. }
  2089. /**
  2090. * Loads ALL of the pages for this lesson
  2091. *
  2092. * @return array An array containing all pages from this lesson
  2093. */
  2094. public function load_all_pages() {
  2095. if (!$this->loadedallpages) {
  2096. $manager = lesson_page_type_manager::get($this);
  2097. $this->pages = $manager->load_all_pages($this);
  2098. $this->loadedallpages = true;
  2099. }
  2100. return $this->pages;
  2101. }
  2102. /**
  2103. * Duplicate the lesson page.
  2104. *
  2105. * @param int $pageid Page ID of the page to duplicate.
  2106. * @return void.
  2107. */
  2108. public function duplicate_page($pageid) {
  2109. global $PAGE;
  2110. $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
  2111. $context = context_module::instance($cm->id);
  2112. // Load the page.
  2113. $page = $this->load_page($pageid);
  2114. $properties = $page->properties();
  2115. // The create method checks to see if these properties are set and if not sets them to zero, hence the unsetting here.
  2116. if (!$properties->qoption) {
  2117. unset($properties->qoption);
  2118. }
  2119. if (!$properties->layout) {
  2120. unset($properties->layout);
  2121. }
  2122. if (!$properties->display) {
  2123. unset($properties->display);
  2124. }
  2125. $properties->pageid = $pageid;
  2126. // Add text and format into the format required to create a new page.
  2127. $properties->contents_editor = array(
  2128. 'text' => $properties->contents,
  2129. 'format' => $properties->contentsformat
  2130. );
  2131. $answers = $page->get_answers();
  2132. // Answers need to be added to $properties.
  2133. $i = 0;
  2134. $answerids = array();
  2135. foreach ($answers as $answer) {
  2136. // Needs to be rearranged to work with the create function.
  2137. $properties->answer_editor[$i] = array(
  2138. 'text' => $answer->answer,
  2139. 'format' => $answer->answerformat
  2140. );
  2141. $properties->response_editor[$i] = array(
  2142. 'text' => $answer->response,
  2143. 'format' => $answer->responseformat
  2144. );
  2145. $answerids[] = $answer->id;
  2146. $properties->jumpto[$i] = $answer->jumpto;
  2147. $properties->score[$i] = $answer->score;
  2148. $i++;
  2149. }
  2150. // Create the duplicate page.
  2151. $newlessonpage = lesson_page::create($properties, $this, $context, $PAGE->course->maxbytes);
  2152. $newanswers = $newlessonpage->get_answers();
  2153. // Copy over the file areas as well.
  2154. $this->copy_page_files('page_contents', $pageid, $newlessonpage->id, $context->id);
  2155. $j = 0;
  2156. foreach ($newanswers as $answer) {
  2157. if (isset($answer->answer) && strpos($answer->answer, '@@PLUGINFILE@@') !== false) {
  2158. $this->copy_page_files('page_answers', $answerids[$j], $answer->id, $context->id);
  2159. }
  2160. if (isset($answer->response) && !is_array($answer->response) && strpos($answer->response, '@@PLUGINFILE@@') !== false) {
  2161. $this->copy_page_files('page_responses', $answerids[$j], $answer->id, $context->id);
  2162. }
  2163. $j++;
  2164. }
  2165. }
  2166. /**
  2167. * Copy the files from one page to another.
  2168. *
  2169. * @param string $filearea Area that the files are stored.
  2170. * @param int $itemid Item ID.
  2171. * @param int $newitemid The item ID for the new page.
  2172. * @param int $contextid Context ID for this page.
  2173. * @return void.
  2174. */
  2175. protected function copy_page_files($filearea, $itemid, $newitemid, $contextid) {
  2176. $fs = get_file_storage();
  2177. $files = $fs->get_area_files($contextid, 'mod_lesson', $filearea, $itemid);
  2178. foreach ($files as $file) {
  2179. $fieldupdates = array('itemid' => $newitemid);
  2180. $fs->create_file_from_storedfile($fieldupdates, $file);
  2181. }
  2182. }
  2183. /**
  2184. * Determines if a jumpto value is correct or not.
  2185. *
  2186. * returns true if jumpto page is (logically) after the pageid page or
  2187. * if the jumpto value is a special value. Returns false in all other cases.
  2188. *
  2189. * @param int $pageid Id of the page from which you are jumping from.
  2190. * @param int $jumpto The jumpto number.
  2191. * @return boolean True or false after a series of tests.
  2192. **/
  2193. public function jumpto_is_correct($pageid, $jumpto) {
  2194. global $DB;
  2195. // first test the special values
  2196. if (!$jumpto) {
  2197. // same page
  2198. return false;
  2199. } elseif ($jumpto == LESSON_NEXTPAGE) {
  2200. return true;
  2201. } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
  2202. return true;
  2203. } elseif ($jumpto == LESSON_RANDOMPAGE) {
  2204. return true;
  2205. } elseif ($jumpto == LESSON_CLUSTERJUMP) {
  2206. return true;
  2207. } elseif ($jumpto == LESSON_EOL) {
  2208. return true;
  2209. }
  2210. $pages = $this->load_all_pages();
  2211. $apageid = $pages[$pageid]->nextpageid;
  2212. while ($apageid != 0) {
  2213. if ($jumpto == $apageid) {
  2214. return true;
  2215. }
  2216. $apageid = $pages[$apageid]->nextpageid;
  2217. }
  2218. return false;
  2219. }
  2220. /**
  2221. * Returns the time a user has remaining on this lesson
  2222. * @param int $starttime Starttime timestamp
  2223. * @return string
  2224. */
  2225. public function time_remaining($starttime) {
  2226. $timeleft = $starttime + $this->properties->timelimit - time();
  2227. $hours = floor($timeleft/3600);
  2228. $timeleft = $timeleft - ($hours * 3600);
  2229. $minutes = floor($timeleft/60);
  2230. $secs = $timeleft - ($minutes * 60);
  2231. if ($minutes < 10) {
  2232. $minutes = "0$minutes";
  2233. }
  2234. if ($secs < 10) {
  2235. $secs = "0$secs";
  2236. }
  2237. $output = array();
  2238. $output[] = $hours;
  2239. $output[] = $minutes;
  2240. $output[] = $secs;
  2241. $output = implode(':', $output);
  2242. return $output;
  2243. }
  2244. /**
  2245. * Interprets LESSON_CLUSTERJUMP jumpto value.
  2246. *
  2247. * This will select a page randomly
  2248. * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
  2249. * and the page selected will be a page that has not been viewed already
  2250. * and if any pages are within a branch table or end of branch then only 1 page within
  2251. * the branch table or end of branch will be randomly selected (sub clustering).
  2252. *
  2253. * @param int $pageid Id of the current page from which we are jumping from.
  2254. * @param int $userid Id of the user.
  2255. * @return int The id of the next page.
  2256. **/
  2257. public function cluster_jump($pageid, $userid=null) {
  2258. global $DB, $USER;
  2259. if ($userid===null) {
  2260. $userid = $USER->id;
  2261. }
  2262. // get the number of retakes
  2263. if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
  2264. $retakes = 0;
  2265. }
  2266. // get all the lesson_attempts aka what the user has seen
  2267. $seenpages = array();
  2268. if ($attempts = $this->get_attempts($retakes)) {
  2269. foreach ($attempts as $attempt) {
  2270. $seenpages[$attempt->pageid] = $attempt->pageid;
  2271. }
  2272. }
  2273. // get the lesson pages
  2274. $lessonpages = $this->load_all_pages();
  2275. // find the start of the cluster
  2276. while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
  2277. if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
  2278. break;
  2279. }
  2280. $pageid = $lessonpages[$pageid]->prevpageid;
  2281. }
  2282. $clusterpages = array();
  2283. $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
  2284. $unseen = array();
  2285. foreach ($clusterpages as $key=>$cluster) {
  2286. // Remove the page if it is in a branch table or is an endofbranch.
  2287. if ($this->is_sub_page_of_type($cluster->id,
  2288. array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))
  2289. || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) {
  2290. unset($clusterpages[$key]);
  2291. } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) {
  2292. // If branchtable, check to see if any pages inside have been viewed.
  2293. $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
  2294. $flag = true;
  2295. foreach ($branchpages as $branchpage) {
  2296. if (array_key_exists($branchpage->id, $seenpages)) { // Check if any of the pages have been viewed.
  2297. $flag = false;
  2298. }
  2299. }
  2300. if ($flag && count($branchpages) > 0) {
  2301. // Add branch table.
  2302. $unseen[] = $cluster;
  2303. }
  2304. } elseif ($cluster->is_unseen($seenpages)) {
  2305. $unseen[] = $cluster;
  2306. }
  2307. }
  2308. if (count($unseen) > 0) {
  2309. // it does not contain elements, then use exitjump, otherwise find out next page/branch
  2310. $nextpage = $unseen[rand(0, count($unseen)-1)];
  2311. if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
  2312. // if branch table, then pick a random page inside of it
  2313. $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
  2314. return $branchpages[rand(0, count($branchpages)-1)]->id;
  2315. } else { // otherwise, return the page's id
  2316. return $nextpage->id;
  2317. }
  2318. } else {
  2319. // seen all there is to see, leave the cluster
  2320. if (end($clusterpages)->nextpageid == 0) {
  2321. return LESSON_EOL;
  2322. } else {
  2323. $clusterendid = $pageid;
  2324. while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page.
  2325. if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) {
  2326. break;
  2327. }
  2328. $clusterendid = $lessonpages[$clusterendid]->nextpageid;
  2329. }
  2330. $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
  2331. if ($exitjump == LESSON_NEXTPAGE) {
  2332. $exitjump = $lessonpages[$clusterendid]->nextpageid;
  2333. }
  2334. if ($exitjump == 0) {
  2335. return LESSON_EOL;
  2336. } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
  2337. return $exitjump;
  2338. } else {
  2339. if (!array_key_exists($exitjump, $lessonpages)) {
  2340. $found = false;
  2341. foreach ($lessonpages as $page) {
  2342. if ($page->id === $clusterendid) {
  2343. $found = true;
  2344. } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
  2345. $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
  2346. if ($exitjump == LESSON_NEXTPAGE) {
  2347. $exitjump = $lessonpages[$page->id]->nextpageid;
  2348. }
  2349. break;
  2350. }
  2351. }
  2352. }
  2353. if (!array_key_exists($exitjump, $lessonpages)) {
  2354. return LESSON_EOL;
  2355. }
  2356. // Check to see that the return type is not a cluster.
  2357. if ($lessonpages[$exitjump]->qtype == LESSON_PAGE_CLUSTER) {
  2358. // If the exitjump is a cluster then go through this function again and try to find an unseen question.
  2359. $exitjump = $this->cluster_jump($exitjump, $userid);
  2360. }
  2361. return $exitjump;
  2362. }
  2363. }
  2364. }
  2365. }
  2366. /**
  2367. * Finds all pages that appear to be a subtype of the provided pageid until
  2368. * an end point specified within $ends is encountered or no more pages exist
  2369. *
  2370. * @param int $pageid
  2371. * @param array $ends An array of LESSON_PAGE_* types that signify an end of
  2372. * the subtype
  2373. * @return array An array of specialised lesson_page objects
  2374. */
  2375. public function get_sub_pages_of($pageid, array $ends) {
  2376. $lessonpages = $this->load_all_pages();
  2377. $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table
  2378. $pages = array();
  2379. while (true) {
  2380. if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
  2381. break;
  2382. }
  2383. $pages[] = $lessonpages[$pageid];
  2384. $pageid = $lessonpages[$pageid]->nextpageid;
  2385. }
  2386. return $pages;
  2387. }
  2388. /**
  2389. * Checks to see if the specified page[id] is a subpage of a type specified in
  2390. * the $types array, until either there are no more pages of we find a type
  2391. * corresponding to that of a type specified in $ends
  2392. *
  2393. * @param int $pageid The id of the page to check
  2394. * @param array $types An array of types that would signify this page was a subpage
  2395. * @param array $ends An array of types that mean this is not a subpage
  2396. * @return bool
  2397. */
  2398. public function is_sub_page_of_type($pageid, array $types, array $ends) {
  2399. $pages = $this->load_all_pages();
  2400. $pageid = $pages[$pageid]->prevpageid; // move up one
  2401. array_unshift($ends, 0);
  2402. // go up the pages till branch table
  2403. while (true) {
  2404. if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
  2405. return false;
  2406. } else if (in_array($pages[$pageid]->qtype, $types)) {
  2407. return true;
  2408. }
  2409. $pageid = $pages[$pageid]->prevpageid;
  2410. }
  2411. }
  2412. /**
  2413. * Move a page resorting all other pages.
  2414. *
  2415. * @param int $pageid
  2416. * @param int $after
  2417. * @return void
  2418. */
  2419. public function resort_pages($pageid, $after) {
  2420. global $CFG;
  2421. $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
  2422. $context = context_module::instance($cm->id);
  2423. $pages = $this->load_all_pages();
  2424. if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) {
  2425. print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id");
  2426. }
  2427. $pagetomove = clone($pages[$pageid]);
  2428. unset($pages[$pageid]);
  2429. $pageids = array();
  2430. if ($after === 0) {
  2431. $pageids['p0'] = $pageid;
  2432. }
  2433. foreach ($pages as $page) {
  2434. $pageids[] = $page->id;
  2435. if ($page->id == $after) {
  2436. $pageids[] = $pageid;
  2437. }
  2438. }
  2439. $pageidsref = $pageids;
  2440. reset($pageidsref);
  2441. $prev = 0;
  2442. $next = next($pageidsref);
  2443. foreach ($pageids as $pid) {
  2444. if ($pid === $pageid) {
  2445. $page = $pagetomove;
  2446. } else {
  2447. $page = $pages[$pid];
  2448. }
  2449. if ($page->prevpageid != $prev || $page->nextpageid != $next) {
  2450. $page->move($next, $prev);
  2451. if ($pid === $pageid) {
  2452. // We will trigger an event.
  2453. $pageupdated = array('next' => $next, 'prev' => $prev);
  2454. }
  2455. }
  2456. $prev = $page->id;
  2457. $next = next($pageidsref);
  2458. if (!$next) {
  2459. $next = 0;
  2460. }
  2461. }
  2462. // Trigger an event: page moved.
  2463. if (!empty($pageupdated)) {
  2464. $eventparams = array(
  2465. 'context' => $context,
  2466. 'objectid' => $pageid,
  2467. 'other' => array(
  2468. 'pagetype' => $page->get_typestring(),
  2469. 'prevpageid' => $pageupdated['prev'],
  2470. 'nextpageid' => $pageupdated['next']
  2471. )
  2472. );
  2473. $event = \mod_lesson\event\page_moved::create($eventparams);
  2474. $event->trigger();
  2475. }
  2476. }
  2477. /**
  2478. * Return the lesson context object.
  2479. *
  2480. * @return stdClass context
  2481. * @since Moodle 3.3
  2482. */
  2483. public function get_context() {
  2484. if ($this->context == null) {
  2485. $this->context = context_module::instance($this->get_cm()->id);
  2486. }
  2487. return $this->context;
  2488. }
  2489. /**
  2490. * Set the lesson course module object.
  2491. *
  2492. * @param stdClass $cm course module objct
  2493. * @since Moodle 3.3
  2494. */
  2495. private function set_cm($cm) {
  2496. $this->cm = $cm;
  2497. }
  2498. /**
  2499. * Return the lesson course module object.
  2500. *
  2501. * @return stdClass course module
  2502. * @since Moodle 3.3
  2503. */
  2504. public function get_cm() {
  2505. if ($this->cm == null) {
  2506. $this->cm = get_coursemodule_from_instance('lesson', $this->properties->id);
  2507. }
  2508. return $this->cm;
  2509. }
  2510. /**
  2511. * Set the lesson course object.
  2512. *
  2513. * @param stdClass $course course objct
  2514. * @since Moodle 3.3
  2515. */
  2516. private function set_courserecord($course) {
  2517. $this->courserecord = $course;
  2518. }
  2519. /**
  2520. * Return the lesson course object.
  2521. *
  2522. * @return stdClass course
  2523. * @since Moodle 3.3
  2524. */
  2525. public function get_courserecord() {
  2526. global $DB;
  2527. if ($this->courserecord == null) {
  2528. $this->courserecord = $DB->get_record('course', array('id' => $this->properties->course));
  2529. }
  2530. return $this->courserecord;
  2531. }
  2532. /**
  2533. * Check if the user can manage the lesson activity.
  2534. *
  2535. * @return bool true if the user can manage the lesson
  2536. * @since Moodle 3.3
  2537. */
  2538. public function can_manage() {
  2539. return has_capability('mod/lesson:manage', $this->get_context());
  2540. }
  2541. /**
  2542. * Check if time restriction is applied.
  2543. *
  2544. * @return mixed false if there aren't restrictions or an object with the restriction information
  2545. * @since Moodle 3.3
  2546. */
  2547. public function get_time_restriction_status() {
  2548. if ($this->can_manage()) {
  2549. return false;
  2550. }
  2551. if (!$this->is_accessible()) {
  2552. if ($this->properties->deadline != 0 && time() > $this->properties->deadline) {
  2553. $status = ['reason' => 'lessonclosed', 'time' => $this->properties->deadline];
  2554. } else {
  2555. $status = ['reason' => 'lessonopen', 'time' => $this->properties->available];
  2556. }
  2557. return (object) $status;
  2558. }
  2559. return false;
  2560. }
  2561. /**
  2562. * Check if password restriction is applied.
  2563. *
  2564. * @param string $userpassword the user password to check (if the restriction is set)
  2565. * @return mixed false if there aren't restrictions or an object with the restriction information
  2566. * @since Moodle 3.3
  2567. */
  2568. public function get_password_restriction_status($userpassword) {
  2569. global $USER;
  2570. if ($this->can_manage()) {
  2571. return false;
  2572. }
  2573. if ($this->properties->usepassword && empty($USER->lessonloggedin[$this->id])) {
  2574. $correctpass = false;
  2575. if (!empty($userpassword) &&
  2576. (($this->properties->password == md5(trim($userpassword))) || ($this->properties->password == trim($userpassword)))) {
  2577. // With or without md5 for backward compatibility (MDL-11090).
  2578. $correctpass = true;
  2579. $USER->lessonloggedin[$this->id] = true;
  2580. } else if (isset($this->properties->extrapasswords)) {
  2581. // Group overrides may have additional passwords.
  2582. foreach ($this->properties->extrapasswords as $password) {
  2583. if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) {
  2584. $correctpass = true;
  2585. $USER->lessonloggedin[$this->id] = true;
  2586. }
  2587. }
  2588. }
  2589. return !$correctpass;
  2590. }
  2591. return false;
  2592. }
  2593. /**
  2594. * Check if dependencies restrictions are applied.
  2595. *
  2596. * @return mixed false if there aren't restrictions or an object with the restriction information
  2597. * @since Moodle 3.3
  2598. */
  2599. public function get_dependencies_restriction_status() {
  2600. global $DB, $USER;
  2601. if ($this->can_manage()) {
  2602. return false;
  2603. }
  2604. if ($dependentlesson = $DB->get_record('lesson', array('id' => $this->properties->dependency))) {
  2605. // Lesson exists, so we can proceed.
  2606. $conditions = unserialize($this->properties->conditions);
  2607. // Assume false for all.
  2608. $errors = array();
  2609. // Check for the timespent condition.
  2610. if ($conditions->timespent) {
  2611. $timespent = false;
  2612. if ($attempttimes = $DB->get_records('lesson_timer', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
  2613. // Go through all the times and test to see if any of them satisfy the condition.
  2614. foreach ($attempttimes as $attempttime) {
  2615. $duration = $attempttime->lessontime - $attempttime->starttime;
  2616. if ($conditions->timespent < $duration / 60) {
  2617. $timespent = true;
  2618. }
  2619. }
  2620. }
  2621. if (!$timespent) {
  2622. $errors[] = get_string('timespenterror', 'lesson', $conditions->timespent);
  2623. }
  2624. }
  2625. // Check for the gradebetterthan condition.
  2626. if ($conditions->gradebetterthan) {
  2627. $gradebetterthan = false;
  2628. if ($studentgrades = $DB->get_records('lesson_grades', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
  2629. // Go through all the grades and test to see if any of them satisfy the condition.
  2630. foreach ($studentgrades as $studentgrade) {
  2631. if ($studentgrade->grade >= $conditions->gradebetterthan) {
  2632. $gradebetterthan = true;
  2633. }
  2634. }
  2635. }
  2636. if (!$gradebetterthan) {
  2637. $errors[] = get_string('gradebetterthanerror', 'lesson', $conditions->gradebetterthan);
  2638. }
  2639. }
  2640. // Check for the completed condition.
  2641. if ($conditions->completed) {
  2642. if (!$DB->count_records('lesson_grades', array('userid' => $USER->id, 'lessonid' => $dependentlesson->id))) {
  2643. $errors[] = get_string('completederror', 'lesson');
  2644. }
  2645. }
  2646. if (!empty($errors)) {
  2647. return (object) ['errors' => $errors, 'dependentlesson' => $dependentlesson];
  2648. }
  2649. }
  2650. return false;
  2651. }
  2652. /**
  2653. * Check if the lesson is in review mode. (The user already finished it and retakes are not allowed).
  2654. *
  2655. * @return bool true if is in review mode
  2656. * @since Moodle 3.3
  2657. */
  2658. public function is_in_review_mode() {
  2659. global $DB, $USER;
  2660. $userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  2661. if ($userhasgrade && !$this->properties->retake) {
  2662. return true;
  2663. }
  2664. return false;
  2665. }
  2666. /**
  2667. * Return the last page the current user saw.
  2668. *
  2669. * @param int $retriescount the number of retries for the lesson (the last retry number).
  2670. * @return mixed false if the user didn't see the lesson or the last page id
  2671. */
  2672. public function get_last_page_seen($retriescount) {
  2673. global $DB, $USER;
  2674. $lastpageseen = false;
  2675. $allattempts = $this->get_attempts($retriescount);
  2676. if (!empty($allattempts)) {
  2677. $attempt = end($allattempts);
  2678. $attemptpage = $this->load_page($attempt->pageid);
  2679. $jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
  2680. // Convert the jumpto to a proper page id.
  2681. if ($jumpto == 0) {
  2682. // Check if a question has been incorrectly answered AND no more attempts at it are left.
  2683. $nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
  2684. if (count($nattempts) >= $this->properties->maxattempts) {
  2685. $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
  2686. } else {
  2687. $lastpageseen = $attempt->pageid;
  2688. }
  2689. } else if ($jumpto == LESSON_NEXTPAGE) {
  2690. $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
  2691. } else if ($jumpto == LESSON_CLUSTERJUMP) {
  2692. $lastpageseen = $this->cluster_jump($attempt->pageid);
  2693. } else {
  2694. $lastpageseen = $jumpto;
  2695. }
  2696. }
  2697. if ($branchtables = $this->get_content_pages_viewed($retriescount, $USER->id, 'timeseen DESC')) {
  2698. // In here, user has viewed a branch table.
  2699. $lastbranchtable = current($branchtables);
  2700. if (count($allattempts) > 0) {
  2701. if ($lastbranchtable->timeseen > $attempt->timeseen) {
  2702. // This branch table was viewed more recently than the question page.
  2703. if (!empty($lastbranchtable->nextpageid)) {
  2704. $lastpageseen = $lastbranchtable->nextpageid;
  2705. } else {
  2706. // Next page ID did not exist prior to MDL-34006.
  2707. $lastpageseen = $lastbranchtable->pageid;
  2708. }
  2709. }
  2710. } else {
  2711. // Has not answered any questions but has viewed a branch table.
  2712. if (!empty($lastbranchtable->nextpageid)) {
  2713. $lastpageseen = $lastbranchtable->nextpageid;
  2714. } else {
  2715. // Next page ID did not exist prior to MDL-34006.
  2716. $lastpageseen = $lastbranchtable->pageid;
  2717. }
  2718. }
  2719. }
  2720. return $lastpageseen;
  2721. }
  2722. /**
  2723. * Return the number of retries in a lesson for a given user.
  2724. *
  2725. * @param int $userid the user id
  2726. * @return int the retries count
  2727. * @since Moodle 3.3
  2728. */
  2729. public function count_user_retries($userid) {
  2730. global $DB;
  2731. return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid));
  2732. }
  2733. /**
  2734. * Check if a user left a timed session.
  2735. *
  2736. * @param int $retriescount the number of retries for the lesson (the last retry number).
  2737. * @return true if the user left the timed session
  2738. * @since Moodle 3.3
  2739. */
  2740. public function left_during_timed_session($retriescount) {
  2741. global $DB, $USER;
  2742. $conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount);
  2743. return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0;
  2744. }
  2745. /**
  2746. * Trigger module viewed event and set the module viewed for completion.
  2747. *
  2748. * @since Moodle 3.3
  2749. */
  2750. public function set_module_viewed() {
  2751. global $CFG;
  2752. require_once($CFG->libdir . '/completionlib.php');
  2753. // Trigger module viewed event.
  2754. $event = \mod_lesson\event\course_module_viewed::create(array(
  2755. 'objectid' => $this->properties->id,
  2756. 'context' => $this->get_context()
  2757. ));
  2758. $event->add_record_snapshot('course_modules', $this->get_cm());
  2759. $event->add_record_snapshot('course', $this->get_courserecord());
  2760. $event->trigger();
  2761. // Mark as viewed.
  2762. $completion = new completion_info($this->get_courserecord());
  2763. $completion->set_module_viewed($this->get_cm());
  2764. }
  2765. /**
  2766. * Return the timers in the current lesson for the given user.
  2767. *
  2768. * @param int $userid the user id
  2769. * @param string $sort an order to sort the results in (optional, a valid SQL ORDER BY parameter).
  2770. * @param string $fields a comma separated list of fields to return
  2771. * @param int $limitfrom return a subset of records, starting at this point (optional).
  2772. * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
  2773. * @return array list of timers for the given user in the lesson
  2774. * @since Moodle 3.3
  2775. */
  2776. public function get_user_timers($userid = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
  2777. global $DB, $USER;
  2778. if ($userid === null) {
  2779. $userid = $USER->id;
  2780. }
  2781. $params = array('lessonid' => $this->properties->id, 'userid' => $userid);
  2782. return $DB->get_records('lesson_timer', $params, $sort, $fields, $limitfrom, $limitnum);
  2783. }
  2784. /**
  2785. * Check if the user is out of time in a timed lesson.
  2786. *
  2787. * @param stdClass $timer timer object
  2788. * @return bool True if the user is on time, false is the user ran out of time
  2789. * @since Moodle 3.3
  2790. */
  2791. public function check_time($timer) {
  2792. if ($this->properties->timelimit) {
  2793. $timeleft = $timer->starttime + $this->properties->timelimit - time();
  2794. if ($timeleft <= 0) {
  2795. // Out of time.
  2796. $this->add_message(get_string('eolstudentoutoftime', 'lesson'));
  2797. return false;
  2798. } else if ($timeleft < 60) {
  2799. // One minute warning.
  2800. $this->add_message(get_string('studentoneminwarning', 'lesson'));
  2801. }
  2802. }
  2803. return true;
  2804. }
  2805. /**
  2806. * Add different informative messages to the given page.
  2807. *
  2808. * @param lesson_page $page page object
  2809. * @param reviewmode $bool whether we are in review mode or not
  2810. * @since Moodle 3.3
  2811. */
  2812. public function add_messages_on_page_view(lesson_page $page, $reviewmode) {
  2813. global $DB, $USER;
  2814. if (!$this->can_manage()) {
  2815. if ($page->qtype == LESSON_PAGE_BRANCHTABLE && $this->properties->minquestions) {
  2816. // Tell student how many questions they have seen, how many are required and their grade.
  2817. $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  2818. $gradeinfo = lesson_grade($this, $ntries);
  2819. if ($gradeinfo->attempts) {
  2820. if ($gradeinfo->nquestions < $this->properties->minquestions) {
  2821. $a = new stdClass;
  2822. $a->nquestions = $gradeinfo->nquestions;
  2823. $a->minquestions = $this->properties->minquestions;
  2824. $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
  2825. }
  2826. if (!$reviewmode && $this->properties->ongoing) {
  2827. $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
  2828. if ($this->properties->grade != GRADE_TYPE_NONE) {
  2829. $a = new stdClass;
  2830. $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
  2831. $a->total = $this->properties->grade;
  2832. $this->add_message(get_string('yourcurrentgradeisoutof', 'lesson', $a), 'notify');
  2833. }
  2834. }
  2835. }
  2836. }
  2837. } else {
  2838. if ($this->properties->timelimit) {
  2839. $this->add_message(get_string('teachertimerwarning', 'lesson'));
  2840. }
  2841. if (lesson_display_teacher_warning($this)) {
  2842. // This is the warning msg for teachers to inform them that cluster
  2843. // and unseen does not work while logged in as a teacher.
  2844. $warningvars = new stdClass();
  2845. $warningvars->cluster = get_string('clusterjump', 'lesson');
  2846. $warningvars->unseen = get_string('unseenpageinbranch', 'lesson');
  2847. $this->add_message(get_string('teacherjumpwarning', 'lesson', $warningvars));
  2848. }
  2849. }
  2850. }
  2851. /**
  2852. * Get the ongoing score message for the user (depending on the user permission and lesson settings).
  2853. *
  2854. * @return str the ongoing score message
  2855. * @since Moodle 3.3
  2856. */
  2857. public function get_ongoing_score_message() {
  2858. global $USER, $DB;
  2859. $context = $this->get_context();
  2860. if (has_capability('mod/lesson:manage', $context)) {
  2861. return get_string('teacherongoingwarning', 'lesson');
  2862. } else {
  2863. $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  2864. if (isset($USER->modattempts[$this->properties->id])) {
  2865. $ntries--;
  2866. }
  2867. $gradeinfo = lesson_grade($this, $ntries);
  2868. $a = new stdClass;
  2869. if ($this->properties->custom) {
  2870. $a->score = $gradeinfo->earned;
  2871. $a->currenthigh = $gradeinfo->total;
  2872. return get_string("ongoingcustom", "lesson", $a);
  2873. } else {
  2874. $a->correct = $gradeinfo->earned;
  2875. $a->viewed = $gradeinfo->attempts;
  2876. return get_string("ongoingnormal", "lesson", $a);
  2877. }
  2878. }
  2879. }
  2880. /**
  2881. * Calculate the progress of the current user in the lesson.
  2882. *
  2883. * @return int the progress (scale 0-100)
  2884. * @since Moodle 3.3
  2885. */
  2886. public function calculate_progress() {
  2887. global $USER, $DB;
  2888. // Check if the user is reviewing the attempt.
  2889. if (isset($USER->modattempts[$this->properties->id])) {
  2890. return 100;
  2891. }
  2892. // All of the lesson pages.
  2893. $pages = $this->load_all_pages();
  2894. foreach ($pages as $page) {
  2895. if ($page->prevpageid == 0) {
  2896. $pageid = $page->id; // Find the first page id.
  2897. break;
  2898. }
  2899. }
  2900. // Current attempt number.
  2901. if (!$ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id))) {
  2902. $ntries = 0; // May not be necessary.
  2903. }
  2904. $viewedpageids = array();
  2905. if ($attempts = $this->get_attempts($ntries, false)) {
  2906. foreach ($attempts as $attempt) {
  2907. $viewedpageids[$attempt->pageid] = $attempt;
  2908. }
  2909. }
  2910. $viewedbranches = array();
  2911. // Collect all of the branch tables viewed.
  2912. if ($branches = $this->get_content_pages_viewed($ntries, $USER->id, 'timeseen ASC', 'id, pageid')) {
  2913. foreach ($branches as $branch) {
  2914. $viewedbranches[$branch->pageid] = $branch;
  2915. }
  2916. $viewedpageids = array_merge($viewedpageids, $viewedbranches);
  2917. }
  2918. // Filter out the following pages:
  2919. // - End of Cluster
  2920. // - End of Branch
  2921. // - Pages found inside of Clusters
  2922. // Do not filter out Cluster Page(s) because we count a cluster as one.
  2923. // By keeping the cluster page, we get our 1.
  2924. $validpages = array();
  2925. while ($pageid != 0) {
  2926. $pageid = $pages[$pageid]->valid_page_and_view($validpages, $viewedpageids);
  2927. }
  2928. // Progress calculation as a percent.
  2929. $progress = round(count($viewedpageids) / count($validpages), 2) * 100;
  2930. return (int) $progress;
  2931. }
  2932. /**
  2933. * Calculate the correct page and prepare contents for a given page id (could be a page jump id).
  2934. *
  2935. * @param int $pageid the given page id
  2936. * @param mod_lesson_renderer $lessonoutput the lesson output rendered
  2937. * @param bool $reviewmode whether we are in review mode or not
  2938. * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect.
  2939. * @return array the page object and contents
  2940. * @throws moodle_exception
  2941. * @since Moodle 3.3
  2942. */
  2943. public function prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, $redirect = true) {
  2944. global $USER, $CFG;
  2945. $page = $this->load_page($pageid);
  2946. // Check if the page is of a special type and if so take any nessecary action.
  2947. $newpageid = $page->callback_on_view($this->can_manage(), $redirect);
  2948. // Avoid redirections returning the jump to special page id.
  2949. if (!$redirect && is_numeric($newpageid) && $newpageid < 0) {
  2950. return array($newpageid, null, null);
  2951. }
  2952. if (is_numeric($newpageid)) {
  2953. $page = $this->load_page($newpageid);
  2954. }
  2955. // Add different informative messages to the given page.
  2956. $this->add_messages_on_page_view($page, $reviewmode);
  2957. if (is_array($page->answers) && count($page->answers) > 0) {
  2958. // This is for modattempts option. Find the users previous answer to this page,
  2959. // and then display it below in answer processing.
  2960. if (isset($USER->modattempts[$this->properties->id])) {
  2961. $retries = $this->count_user_retries($USER->id);
  2962. if (!$attempts = $this->get_attempts($retries - 1, false, $page->id)) {
  2963. throw new moodle_exception('cannotfindpreattempt', 'lesson');
  2964. }
  2965. $attempt = end($attempts);
  2966. $USER->modattempts[$this->properties->id] = $attempt;
  2967. } else {
  2968. $attempt = false;
  2969. }
  2970. $lessoncontent = $lessonoutput->display_page($this, $page, $attempt);
  2971. } else {
  2972. require_once($CFG->dirroot . '/mod/lesson/view_form.php');
  2973. $data = new stdClass;
  2974. $data->id = $this->get_cm()->id;
  2975. $data->pageid = $page->id;
  2976. $data->newpageid = $this->get_next_page($page->nextpageid);
  2977. $customdata = array(
  2978. 'title' => $page->title,
  2979. 'contents' => $page->get_contents()
  2980. );
  2981. $mform = new lesson_page_without_answers($CFG->wwwroot.'/mod/lesson/continue.php', $customdata);
  2982. $mform->set_data($data);
  2983. ob_start();
  2984. $mform->display();
  2985. $lessoncontent = ob_get_contents();
  2986. ob_end_clean();
  2987. }
  2988. return array($page->id, $page, $lessoncontent);
  2989. }
  2990. /**
  2991. * This returns a real page id to jump to (or LESSON_EOL) after processing page responses.
  2992. *
  2993. * @param lesson_page $page lesson page
  2994. * @param int $newpageid the new page id
  2995. * @return int the real page to jump to (or end of lesson)
  2996. * @since Moodle 3.3
  2997. */
  2998. public function calculate_new_page_on_jump(lesson_page $page, $newpageid) {
  2999. global $USER, $DB;
  3000. $canmanage = $this->can_manage();
  3001. if (isset($USER->modattempts[$this->properties->id])) {
  3002. // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time.
  3003. if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) {
  3004. // Remember, this session variable holds the pageid of the last page that the user saw.
  3005. $newpageid = LESSON_EOL;
  3006. } else {
  3007. $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  3008. $nretakes--; // Make sure we are looking at the right try.
  3009. $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid");
  3010. $found = false;
  3011. $temppageid = 0;
  3012. // Make sure that the newpageid always defaults to something valid.
  3013. $newpageid = LESSON_EOL;
  3014. foreach ($attempts as $attempt) {
  3015. if ($found && $temppageid != $attempt->pageid) {
  3016. // Now try to find the next page, make sure next few attempts do no belong to current page.
  3017. $newpageid = $attempt->pageid;
  3018. break;
  3019. }
  3020. if ($attempt->pageid == $page->id) {
  3021. $found = true; // If found current page.
  3022. $temppageid = $attempt->pageid;
  3023. }
  3024. }
  3025. }
  3026. } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) {
  3027. // Going to check to see if the page that the user is going to view next, is a cluster page.
  3028. // If so, dont display, go into the cluster.
  3029. // The $newpageid > 0 is used to filter out all of the negative code jumps.
  3030. $newpage = $this->load_page($newpageid);
  3031. if ($overridenewpageid = $newpage->override_next_page($newpageid)) {
  3032. $newpageid = $overridenewpageid;
  3033. }
  3034. } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) {
  3035. if ($canmanage) {
  3036. if ($page->nextpageid == 0) {
  3037. $newpageid = LESSON_EOL;
  3038. } else {
  3039. $newpageid = $page->nextpageid;
  3040. }
  3041. } else {
  3042. $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id);
  3043. }
  3044. } else if ($newpageid == LESSON_PREVIOUSPAGE) {
  3045. $newpageid = $page->prevpageid;
  3046. } else if ($newpageid == LESSON_RANDOMPAGE) {
  3047. $newpageid = lesson_random_question_jump($this, $page->id);
  3048. } else if ($newpageid == LESSON_CLUSTERJUMP) {
  3049. if ($canmanage) {
  3050. if ($page->nextpageid == 0) { // If teacher, go to next page.
  3051. $newpageid = LESSON_EOL;
  3052. } else {
  3053. $newpageid = $page->nextpageid;
  3054. }
  3055. } else {
  3056. $newpageid = $this->cluster_jump($page->id);
  3057. }
  3058. } else if ($newpageid == 0) {
  3059. $newpageid = $page->id;
  3060. } else if ($newpageid == LESSON_NEXTPAGE) {
  3061. $newpageid = $this->get_next_page($page->nextpageid);
  3062. }
  3063. return $newpageid;
  3064. }
  3065. /**
  3066. * Process page responses.
  3067. *
  3068. * @param lesson_page $page page object
  3069. * @since Moodle 3.3
  3070. */
  3071. public function process_page_responses(lesson_page $page) {
  3072. $context = $this->get_context();
  3073. // Check the page has answers [MDL-25632].
  3074. if (count($page->answers) > 0) {
  3075. $result = $page->record_attempt($context);
  3076. } else {
  3077. // The page has no answers so we will just progress to the next page in the
  3078. // sequence (as set by newpageid).
  3079. $result = new stdClass;
  3080. $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT);
  3081. $result->nodefaultresponse = true;
  3082. $result->inmediatejump = false;
  3083. }
  3084. if ($result->inmediatejump) {
  3085. return $result;
  3086. }
  3087. $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid);
  3088. return $result;
  3089. }
  3090. /**
  3091. * Add different informative messages to the given page.
  3092. *
  3093. * @param lesson_page $page page object
  3094. * @param stdClass $result the page processing result object
  3095. * @param bool $reviewmode whether we are in review mode or not
  3096. * @since Moodle 3.3
  3097. */
  3098. public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) {
  3099. if ($this->can_manage()) {
  3100. // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
  3101. if (lesson_display_teacher_warning($this)) {
  3102. $warningvars = new stdClass();
  3103. $warningvars->cluster = get_string("clusterjump", "lesson");
  3104. $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
  3105. $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
  3106. }
  3107. // Inform teacher that s/he will not see the timer.
  3108. if ($this->properties->timelimit) {
  3109. $this->add_message(get_string("teachertimerwarning", "lesson"));
  3110. }
  3111. }
  3112. // Report attempts remaining.
  3113. if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) {
  3114. $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
  3115. }
  3116. }
  3117. /**
  3118. * Process and return all the information for the end of lesson page.
  3119. *
  3120. * @param string $outoftime used to check to see if the student ran out of time
  3121. * @return stdclass an object with all the page data ready for rendering
  3122. * @since Moodle 3.3
  3123. */
  3124. public function process_eol_page($outoftime) {
  3125. global $DB, $USER;
  3126. $course = $this->get_courserecord();
  3127. $cm = $this->get_cm();
  3128. $canmanage = $this->can_manage();
  3129. // Init all the possible fields and values.
  3130. $data = (object) array(
  3131. 'gradelesson' => true,
  3132. 'notenoughtimespent' => false,
  3133. 'numberofpagesviewed' => false,
  3134. 'youshouldview' => false,
  3135. 'numberofcorrectanswers' => false,
  3136. 'displayscorewithessays' => false,
  3137. 'displayscorewithoutessays' => false,
  3138. 'yourcurrentgradeisoutof' => false,
  3139. 'eolstudentoutoftimenoanswers' => false,
  3140. 'welldone' => false,
  3141. 'progressbar' => false,
  3142. 'displayofgrade' => false,
  3143. 'reviewlesson' => false,
  3144. 'modattemptsnoteacher' => false,
  3145. 'activitylink' => false,
  3146. 'progresscompleted' => false,
  3147. );
  3148. $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
  3149. if (isset($USER->modattempts[$this->properties->id])) {
  3150. $ntries--; // Need to look at the old attempts :).
  3151. }
  3152. $gradeinfo = lesson_grade($this, $ntries);
  3153. $data->gradeinfo = $gradeinfo;
  3154. if ($this->properties->custom && !$canmanage) {
  3155. // Before we calculate the custom score make sure they answered the minimum
  3156. // number of questions. We only need to do this for custom scoring as we can
  3157. // not get the miniumum score the user should achieve. If we are not using
  3158. // custom scoring (so all questions are valued as 1) then we simply check if
  3159. // they answered more than the minimum questions, if not, we mark it out of the
  3160. // number specified in the minimum questions setting - which is done in lesson_grade().
  3161. // Get the number of answers given.
  3162. if ($gradeinfo->nquestions < $this->properties->minquestions) {
  3163. $data->gradelesson = false;
  3164. $a = new stdClass;
  3165. $a->nquestions = $gradeinfo->nquestions;
  3166. $a->minquestions = $this->properties->minquestions;
  3167. $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
  3168. }
  3169. }
  3170. if (!$canmanage) {
  3171. if ($data->gradelesson) {
  3172. // Store this now before any modifications to pages viewed.
  3173. $progresscompleted = $this->calculate_progress();
  3174. // Update the clock / get time information for this user.
  3175. $this->stop_timer();
  3176. // Update completion state.
  3177. $completion = new completion_info($course);
  3178. if ($completion->is_enabled($cm) && $this->properties->completionendreached) {
  3179. $completion->update_state($cm, COMPLETION_COMPLETE);
  3180. }
  3181. if ($this->properties->completiontimespent > 0) {
  3182. $duration = $DB->get_field_sql(
  3183. "SELECT SUM(lessontime - starttime)
  3184. FROM {lesson_timer}
  3185. WHERE lessonid = :lessonid
  3186. AND userid = :userid",
  3187. array('userid' => $USER->id, 'lessonid' => $this->properties->id));
  3188. if (!$duration) {
  3189. $duration = 0;
  3190. }
  3191. // If student has not spend enough time in the lesson, display a message.
  3192. if ($duration < $this->properties->completiontimespent) {
  3193. $a = new stdClass;
  3194. $a->timespentraw = $duration;
  3195. $a->timespent = format_time($duration);
  3196. $a->timerequiredraw = $this->properties->completiontimespent;
  3197. $a->timerequired = format_time($this->properties->completiontimespent);
  3198. $data->notenoughtimespent = $a;
  3199. }
  3200. }
  3201. if ($gradeinfo->attempts) {
  3202. if (!$this->properties->custom) {
  3203. $data->numberofpagesviewed = $gradeinfo->nquestions;
  3204. if ($this->properties->minquestions) {
  3205. if ($gradeinfo->nquestions < $this->properties->minquestions) {
  3206. $data->youshouldview = $this->properties->minquestions;
  3207. }
  3208. }
  3209. $data->numberofcorrectanswers = $gradeinfo->earned;
  3210. }
  3211. $a = new stdClass;
  3212. $a->score = $gradeinfo->earned;
  3213. $a->grade = $gradeinfo->total;
  3214. if ($gradeinfo->nmanual) {
  3215. $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
  3216. $a->essayquestions = $gradeinfo->nmanual;
  3217. $data->displayscorewithessays = $a;
  3218. } else {
  3219. $data->displayscorewithoutessays = $a;
  3220. }
  3221. if ($this->properties->grade != GRADE_TYPE_NONE) {
  3222. $a = new stdClass;
  3223. $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
  3224. $a->total = $this->properties->grade;
  3225. $data->yourcurrentgradeisoutof = $a;
  3226. }
  3227. $grade = new stdClass();
  3228. $grade->lessonid = $this->properties->id;
  3229. $grade->userid = $USER->id;
  3230. $grade->grade = $gradeinfo->grade;
  3231. $grade->completed = time();
  3232. if (isset($USER->modattempts[$this->properties->id])) { // If reviewing, make sure update old grade record.
  3233. if (!$grades = $DB->get_records("lesson_grades",
  3234. array("lessonid" => $this->properties->id, "userid" => $USER->id), "completed DESC", '*', 0, 1)) {
  3235. throw new moodle_exception('cannotfindgrade', 'lesson');
  3236. }
  3237. $oldgrade = array_shift($grades);
  3238. $grade->id = $oldgrade->id;
  3239. $DB->update_record("lesson_grades", $grade);
  3240. } else {
  3241. $newgradeid = $DB->insert_record("lesson_grades", $grade);
  3242. }
  3243. } else {
  3244. if ($this->properties->timelimit) {
  3245. if ($outoftime == 'normal') {
  3246. $grade = new stdClass();
  3247. $grade->lessonid = $this->properties->id;
  3248. $grade->userid = $USER->id;
  3249. $grade->grade = 0;
  3250. $grade->completed = time();
  3251. $newgradeid = $DB->insert_record("lesson_grades", $grade);
  3252. $data->eolstudentoutoftimenoanswers = true;
  3253. }
  3254. } else {
  3255. $data->welldone = true;
  3256. }
  3257. }
  3258. // Update central gradebook.
  3259. lesson_update_grades($this, $USER->id);
  3260. $data->progresscompleted = $progresscompleted;
  3261. }
  3262. } else {
  3263. // Display for teacher.
  3264. if ($this->properties->grade != GRADE_TYPE_NONE) {
  3265. $data->displayofgrade = true;
  3266. }
  3267. }
  3268. if ($this->properties->modattempts && !$canmanage) {
  3269. // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time
  3270. // look at the attempt records to find the first QUESTION page that the user answered, then use that page id
  3271. // to pass to view again. This is slick cause it wont call the empty($pageid) code
  3272. // $ntries is decremented above.
  3273. if (!$attempts = $this->get_attempts($ntries)) {
  3274. $attempts = array();
  3275. $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id));
  3276. } else {
  3277. $firstattempt = current($attempts);
  3278. $pageid = $firstattempt->pageid;
  3279. // If the student wishes to review, need to know the last question page that the student answered.
  3280. // This will help to make sure that the student can leave the lesson via pushing the continue button.
  3281. $lastattempt = end($attempts);
  3282. $USER->modattempts[$this->properties->id] = $lastattempt->pageid;
  3283. $url = new moodle_url('/mod/lesson/view.php', array('id' => $cm->id, 'pageid' => $pageid));
  3284. }
  3285. $data->reviewlesson = $url->out(false);
  3286. } else if ($this->properties->modattempts && $canmanage) {
  3287. $data->modattemptsnoteacher = true;
  3288. }
  3289. if ($this->properties->activitylink) {
  3290. $data->activitylink = $this->link_for_activitylink();
  3291. }
  3292. return $data;
  3293. }
  3294. /**
  3295. * Returns the last "legal" attempt from the list of student attempts.
  3296. *
  3297. * @param array $attempts The list of student attempts.
  3298. * @return stdClass The updated fom data.
  3299. */
  3300. public function get_last_attempt(array $attempts): stdClass {
  3301. // If there are more tries than the max that is allowed, grab the last "legal" attempt.
  3302. if (!empty($this->maxattempts) && (count($attempts) > $this->maxattempts)) {
  3303. $lastattempt = $attempts[$this->maxattempts - 1];
  3304. } else {
  3305. // Grab the last attempt since there's no limit to the max attempts or the user has made fewer attempts than the max.
  3306. $lastattempt = end($attempts);
  3307. }
  3308. return $lastattempt;
  3309. }
  3310. }
  3311. /**
  3312. * Abstract class to provide a core functions to the all lesson classes
  3313. *
  3314. * This class should be abstracted by ALL classes with the lesson module to ensure
  3315. * that all classes within this module can be interacted with in the same way.
  3316. *
  3317. * This class provides the user with a basic properties array that can be fetched
  3318. * or set via magic methods, or alternatively by defining methods get_blah() or
  3319. * set_blah() within the extending object.
  3320. *
  3321. * @copyright 2009 Sam Hemelryk
  3322. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  3323. */
  3324. abstract class lesson_base {
  3325. /**
  3326. * An object containing properties
  3327. * @var stdClass
  3328. */
  3329. protected $properties;
  3330. /**
  3331. * The constructor
  3332. * @param stdClass $properties
  3333. */
  3334. public function __construct($properties) {
  3335. $this->properties = (object)$properties;
  3336. }
  3337. /**
  3338. * Magic property method
  3339. *
  3340. * Attempts to call a set_$key method if one exists otherwise falls back
  3341. * to simply set the property
  3342. *
  3343. * @param string $key
  3344. * @param mixed $value
  3345. */
  3346. public function __set($key, $value) {
  3347. if (method_exists($this, 'set_'.$key)) {
  3348. $this->{'set_'.$key}($value);
  3349. }
  3350. $this->properties->{$key} = $value;
  3351. }
  3352. /**
  3353. * Magic get method
  3354. *
  3355. * Attempts to call a get_$key method to return the property and ralls over
  3356. * to return the raw property
  3357. *
  3358. * @param str $key
  3359. * @return mixed
  3360. */
  3361. public function __get($key) {
  3362. if (method_exists($this, 'get_'.$key)) {
  3363. return $this->{'get_'.$key}();
  3364. }
  3365. return $this->properties->{$key};
  3366. }
  3367. /**
  3368. * Stupid PHP needs an isset magic method if you use the get magic method and
  3369. * still want empty calls to work.... blah ~!
  3370. *
  3371. * @param string $key
  3372. * @return bool
  3373. */
  3374. public function __isset($key) {
  3375. if (method_exists($this, 'get_'.$key)) {
  3376. $val = $this->{'get_'.$key}();
  3377. return !empty($val);
  3378. }
  3379. return !empty($this->properties->{$key});
  3380. }
  3381. //NOTE: E_STRICT does not allow to change function signature!
  3382. /**
  3383. * If implemented should create a new instance, save it in the DB and return it
  3384. */
  3385. //public static function create() {}
  3386. /**
  3387. * If implemented should load an instance from the DB and return it
  3388. */
  3389. //public static function load() {}
  3390. /**
  3391. * Fetches all of the properties of the object
  3392. * @return stdClass
  3393. */
  3394. public function properties() {
  3395. return $this->properties;
  3396. }
  3397. }
  3398. /**
  3399. * Abstract class representation of a page associated with a lesson.
  3400. *
  3401. * This class should MUST be extended by all specialised page types defined in
  3402. * mod/lesson/pagetypes/.
  3403. * There are a handful of abstract methods that need to be defined as well as
  3404. * severl methods that can optionally be defined in order to make the page type
  3405. * operate in the desired way
  3406. *
  3407. * Database properties
  3408. * @property int $id The id of this lesson page
  3409. * @property int $lessonid The id of the lesson this page belongs to
  3410. * @property int $prevpageid The id of the page before this one
  3411. * @property int $nextpageid The id of the next page in the page sequence
  3412. * @property int $qtype Identifies the page type of this page
  3413. * @property int $qoption Used to record page type specific options
  3414. * @property int $layout Used to record page specific layout selections
  3415. * @property int $display Used to record page specific display selections
  3416. * @property int $timecreated Timestamp for when the page was created
  3417. * @property int $timemodified Timestamp for when the page was last modified
  3418. * @property string $title The title of this page
  3419. * @property string $contents The rich content shown to describe the page
  3420. * @property int $contentsformat The format of the contents field
  3421. *
  3422. * Calculated properties
  3423. * @property-read array $answers An array of answers for this page
  3424. * @property-read bool $displayinmenublock Toggles display in the left menu block
  3425. * @property-read array $jumps An array containing all the jumps this page uses
  3426. * @property-read lesson $lesson The lesson this page belongs to
  3427. * @property-read int $type The type of the page [question | structure]
  3428. * @property-read typeid The unique identifier for the page type
  3429. * @property-read typestring The string that describes this page type
  3430. *
  3431. * @abstract
  3432. * @copyright 2009 Sam Hemelryk
  3433. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  3434. */
  3435. abstract class lesson_page extends lesson_base {
  3436. /**
  3437. * A reference to the lesson this page belongs to
  3438. * @var lesson
  3439. */
  3440. protected $lesson = null;
  3441. /**
  3442. * Contains the answers to this lesson_page once loaded
  3443. * @var null|array
  3444. */
  3445. protected $answers = null;
  3446. /**
  3447. * This sets the type of the page, can be one of the constants defined below
  3448. * @var int
  3449. */
  3450. protected $type = 0;
  3451. /**
  3452. * Constants used to identify the type of the page
  3453. */
  3454. const TYPE_QUESTION = 0;
  3455. const TYPE_STRUCTURE = 1;
  3456. /**
  3457. * Constant used as a delimiter when parsing multianswer questions
  3458. */
  3459. const MULTIANSWER_DELIMITER = '@^#|';
  3460. /**
  3461. * This method should return the integer used to identify the page type within
  3462. * the database and throughout code. This maps back to the defines used in 1.x
  3463. * @abstract
  3464. * @return int
  3465. */
  3466. abstract protected function get_typeid();
  3467. /**
  3468. * This method should return the string that describes the pagetype
  3469. * @abstract
  3470. * @return string
  3471. */
  3472. abstract protected function get_typestring();
  3473. /**
  3474. * This method gets called to display the page to the user taking the lesson
  3475. * @abstract
  3476. * @param object $renderer
  3477. * @param object $attempt
  3478. * @return string
  3479. */
  3480. abstract public function display($renderer, $attempt);
  3481. /**
  3482. * Creates a new lesson_page within the database and returns the correct pagetype
  3483. * object to use to interact with the new lesson
  3484. *
  3485. * @final
  3486. * @static
  3487. * @param object $properties
  3488. * @param lesson $lesson
  3489. * @return lesson_page Specialised object that extends lesson_page
  3490. */
  3491. final public static function create($properties, lesson $lesson, $context, $maxbytes) {
  3492. global $DB;
  3493. $newpage = new stdClass;
  3494. $newpage->title = $properties->title;
  3495. $newpage->contents = $properties->contents_editor['text'];
  3496. $newpage->contentsformat = $properties->contents_editor['format'];
  3497. $newpage->lessonid = $lesson->id;
  3498. $newpage->timecreated = time();
  3499. $newpage->qtype = $properties->qtype;
  3500. $newpage->qoption = (isset($properties->qoption))?1:0;
  3501. $newpage->layout = (isset($properties->layout))?1:0;
  3502. $newpage->display = (isset($properties->display))?1:0;
  3503. $newpage->prevpageid = 0; // this is a first page
  3504. $newpage->nextpageid = 0; // this is the only page
  3505. if ($properties->pageid) {
  3506. $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
  3507. if (!$prevpage) {
  3508. print_error('cannotfindpages', 'lesson');
  3509. }
  3510. $newpage->prevpageid = $prevpage->id;
  3511. $newpage->nextpageid = $prevpage->nextpageid;
  3512. } else {
  3513. $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
  3514. if ($nextpage) {
  3515. // This is the first page, there are existing pages put this at the start
  3516. $newpage->nextpageid = $nextpage->id;
  3517. }
  3518. }
  3519. $newpage->id = $DB->insert_record("lesson_pages", $newpage);
  3520. $editor = new stdClass;
  3521. $editor->id = $newpage->id;
  3522. $editor->contents_editor = $properties->contents_editor;
  3523. $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id);
  3524. $DB->update_record("lesson_pages", $editor);
  3525. if ($newpage->prevpageid > 0) {
  3526. $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
  3527. }
  3528. if ($newpage->nextpageid > 0) {
  3529. $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
  3530. }
  3531. $page = lesson_page::load($newpage, $lesson);
  3532. $page->create_answers($properties);
  3533. // Trigger an event: page created.
  3534. $eventparams = array(
  3535. 'context' => $context,
  3536. 'objectid' => $newpage->id,
  3537. 'other' => array(
  3538. 'pagetype' => $page->get_typestring()
  3539. )
  3540. );
  3541. $event = \mod_lesson\event\page_created::create($eventparams);
  3542. $snapshot = clone($newpage);
  3543. $snapshot->timemodified = 0;
  3544. $event->add_record_snapshot('lesson_pages', $snapshot);
  3545. $event->trigger();
  3546. $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
  3547. return $page;
  3548. }
  3549. /**
  3550. * This method loads a page object from the database and returns it as a
  3551. * specialised object that extends lesson_page
  3552. *
  3553. * @final
  3554. * @static
  3555. * @param int $id
  3556. * @param lesson $lesson
  3557. * @return lesson_page Specialised lesson_page object
  3558. */
  3559. final public static function load($id, lesson $lesson) {
  3560. global $DB;
  3561. if (is_object($id) && !empty($id->qtype)) {
  3562. $page = $id;
  3563. } else {
  3564. $page = $DB->get_record("lesson_pages", array("id" => $id));
  3565. if (!$page) {
  3566. print_error('cannotfindpages', 'lesson');
  3567. }
  3568. }
  3569. $manager = lesson_page_type_manager::get($lesson);
  3570. $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
  3571. if (!class_exists($class)) {
  3572. $class = 'lesson_page';
  3573. }
  3574. return new $class($page, $lesson);
  3575. }
  3576. /**
  3577. * Deletes a lesson_page from the database as well as any associated records.
  3578. * @final
  3579. * @return bool
  3580. */
  3581. final public function delete() {
  3582. global $DB;
  3583. $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
  3584. $context = context_module::instance($cm->id);
  3585. // Delete files associated with attempts.
  3586. $fs = get_file_storage();
  3587. if ($attempts = $DB->get_records('lesson_attempts', array("pageid" => $this->properties->id))) {
  3588. foreach ($attempts as $attempt) {
  3589. $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $attempt->id);
  3590. $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers', $attempt->id);
  3591. }
  3592. }
  3593. // Then delete all the associated records...
  3594. $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
  3595. $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id));
  3596. // Delete files related to answers and responses.
  3597. if ($answers = $DB->get_records("lesson_answers", array("pageid" => $this->properties->id))) {
  3598. foreach ($answers as $answer) {
  3599. $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $answer->id);
  3600. $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $answer->id);
  3601. }
  3602. }
  3603. // ...now delete the answers...
  3604. $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
  3605. // ..and the page itself
  3606. $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
  3607. // Trigger an event: page deleted.
  3608. $eventparams = array(
  3609. 'context' => $context,
  3610. 'objectid' => $this->properties->id,
  3611. 'other' => array(
  3612. 'pagetype' => $this->get_typestring()
  3613. )
  3614. );
  3615. $event = \mod_lesson\event\page_deleted::create($eventparams);
  3616. $event->add_record_snapshot('lesson_pages', $this->properties);
  3617. $event->trigger();
  3618. // Delete files associated with this page.
  3619. $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id);
  3620. // repair the hole in the linkage
  3621. if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
  3622. //This is the only page, no repair needed
  3623. } elseif (!$this->properties->prevpageid) {
  3624. // this is the first page...
  3625. $page = $this->lesson->load_page($this->properties->nextpageid);
  3626. $page->move(null, 0);
  3627. } elseif (!$this->properties->nextpageid) {
  3628. // this is the last page...
  3629. $page = $this->lesson->load_page($this->properties->prevpageid);
  3630. $page->move(0);
  3631. } else {
  3632. // page is in the middle...
  3633. $prevpage = $this->lesson->load_page($this->properties->prevpageid);
  3634. $nextpage = $this->lesson->load_page($this->properties->nextpageid);
  3635. $prevpage->move($nextpage->id);
  3636. $nextpage->move(null, $prevpage->id);
  3637. }
  3638. return true;
  3639. }
  3640. /**
  3641. * Moves a page by updating its nextpageid and prevpageid values within
  3642. * the database
  3643. *
  3644. * @final
  3645. * @param int $nextpageid
  3646. * @param int $prevpageid
  3647. */
  3648. final public function move($nextpageid=null, $prevpageid=null) {
  3649. global $DB;
  3650. if ($nextpageid === null) {
  3651. $nextpageid = $this->properties->nextpageid;
  3652. }
  3653. if ($prevpageid === null) {
  3654. $prevpageid = $this->properties->prevpageid;
  3655. }
  3656. $obj = new stdClass;
  3657. $obj->id = $this->properties->id;
  3658. $obj->prevpageid = $prevpageid;
  3659. $obj->nextpageid = $nextpageid;
  3660. $DB->update_record('lesson_pages', $obj);
  3661. }
  3662. /**
  3663. * Returns the answers that are associated with this page in the database
  3664. *
  3665. * @final
  3666. * @return array
  3667. */
  3668. final public function get_answers() {
  3669. global $DB;
  3670. if ($this->answers === null) {
  3671. $this->answers = array();
  3672. $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
  3673. if (!$answers) {
  3674. // It is possible that a lesson upgraded from Moodle 1.9 still
  3675. // contains questions without any answers [MDL-25632].
  3676. // debugging(get_string('cannotfindanswer', 'lesson'));
  3677. return array();
  3678. }
  3679. foreach ($answers as $answer) {
  3680. $this->answers[count($this->answers)] = new lesson_page_answer($answer);
  3681. }
  3682. }
  3683. return $this->answers;
  3684. }
  3685. /**
  3686. * Returns the lesson this page is associated with
  3687. * @final
  3688. * @return lesson
  3689. */
  3690. final protected function get_lesson() {
  3691. return $this->lesson;
  3692. }
  3693. /**
  3694. * Returns the type of page this is. Not to be confused with page type
  3695. * @final
  3696. * @return int
  3697. */
  3698. final protected function get_type() {
  3699. return $this->type;
  3700. }
  3701. /**
  3702. * Records an attempt at this page
  3703. *
  3704. * @final
  3705. * @global moodle_database $DB
  3706. * @param stdClass $context
  3707. * @return stdClass Returns the result of the attempt
  3708. */
  3709. final public function record_attempt($context) {
  3710. global $DB, $USER, $OUTPUT, $PAGE;
  3711. /**
  3712. * This should be overridden by each page type to actually check the response
  3713. * against what ever custom criteria they have defined
  3714. */
  3715. $result = $this->check_answer();
  3716. // Processes inmediate jumps.
  3717. if ($result->inmediatejump) {
  3718. return $result;
  3719. }
  3720. $result->attemptsremaining = 0;
  3721. $result->maxattemptsreached = false;
  3722. if ($result->noanswer) {
  3723. $result->newpageid = $this->properties->id; // display same page again
  3724. $result->feedback = get_string('noanswer', 'lesson');
  3725. } else {
  3726. if (!has_capability('mod/lesson:manage', $context)) {
  3727. $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
  3728. // Get the number of attempts that have been made on this question for this student and retake,
  3729. $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id,
  3730. 'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes));
  3731. // Check if they have reached (or exceeded) the maximum number of attempts allowed.
  3732. if (!empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) {
  3733. $result->maxattemptsreached = true;
  3734. $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson');
  3735. $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid);
  3736. return $result;
  3737. }
  3738. // record student's attempt
  3739. $attempt = new stdClass;
  3740. $attempt->lessonid = $this->lesson->id;
  3741. $attempt->pageid = $this->properties->id;
  3742. $attempt->userid = $USER->id;
  3743. $attempt->answerid = $result->answerid;
  3744. $attempt->retry = $nretakes;
  3745. $attempt->correct = $result->correctanswer;
  3746. if($result->userresponse !== null) {
  3747. $attempt->useranswer = $result->userresponse;
  3748. }
  3749. $attempt->timeseen = time();
  3750. // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
  3751. $userisreviewing = false;
  3752. if (isset($USER->modattempts[$this->lesson->id])) {
  3753. $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
  3754. $userisreviewing = true;
  3755. }
  3756. // Only insert a record if we are not reviewing the lesson.
  3757. if (!$userisreviewing) {
  3758. if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) {
  3759. $attempt->id = $DB->insert_record("lesson_attempts", $attempt);
  3760. list($updatedattempt, $updatedresult) = $this->on_after_write_attempt($attempt, $result);
  3761. if ($updatedattempt) {
  3762. $attempt = $updatedattempt;
  3763. $result = $updatedresult;
  3764. $DB->update_record("lesson_attempts", $attempt);
  3765. }
  3766. // Trigger an event: question answered.
  3767. $eventparams = array(
  3768. 'context' => context_module::instance($PAGE->cm->id),
  3769. 'objectid' => $this->properties->id,
  3770. 'other' => array(
  3771. 'pagetype' => $this->get_typestring()
  3772. )
  3773. );
  3774. $event = \mod_lesson\event\question_answered::create($eventparams);
  3775. $event->add_record_snapshot('lesson_attempts', $attempt);
  3776. $event->trigger();
  3777. // Increase the number of attempts made.
  3778. $nattempts++;
  3779. }
  3780. } else {
  3781. // When reviewing the lesson, the existing attemptid is also needed for the filearea options.
  3782. $params = [
  3783. 'lessonid' => $attempt->lessonid,
  3784. 'pageid' => $attempt->pageid,
  3785. 'userid' => $attempt->userid,
  3786. 'answerid' => $attempt->answerid,
  3787. 'retry' => $attempt->retry
  3788. ];
  3789. $attempt->id = $DB->get_field('lesson_attempts', 'id', $params);
  3790. }
  3791. // "number of attempts remaining" message if $this->lesson->maxattempts > 1
  3792. // displaying of message(s) is at the end of page for more ergonomic display
  3793. if (!$result->correctanswer && ($result->newpageid == 0)) {
  3794. // Retrieve the number of attempts left counter for displaying at bottom of feedback page.
  3795. if (!empty($this->lesson->maxattempts) && $nattempts >= $this->lesson->maxattempts) {
  3796. if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
  3797. $result->maxattemptsreached = true;
  3798. }
  3799. $result->newpageid = LESSON_NEXTPAGE;
  3800. } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
  3801. $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
  3802. }
  3803. }
  3804. }
  3805. // Determine default feedback if necessary
  3806. if (empty($result->response)) {
  3807. if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
  3808. // These conditions have been met:
  3809. // 1. The lesson manager has not supplied feedback to the student
  3810. // 2. Not displaying default feedback
  3811. // 3. The user did provide an answer
  3812. // 4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
  3813. $result->nodefaultresponse = true; // This will cause a redirect below
  3814. } else if ($result->isessayquestion) {
  3815. $result->response = get_string('defaultessayresponse', 'lesson');
  3816. } else if ($result->correctanswer) {
  3817. $result->response = get_string('thatsthecorrectanswer', 'lesson');
  3818. } else {
  3819. $result->response = get_string('thatsthewronganswer', 'lesson');
  3820. }
  3821. }
  3822. if ($result->response) {
  3823. if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
  3824. $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
  3825. $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
  3826. if ($qattempts == 1) {
  3827. $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback');
  3828. } else {
  3829. if (!$result->maxattemptsreached) {
  3830. $result->feedback = $OUTPUT->box(get_string("secondpluswrong", "lesson"), 'feedback');
  3831. } else {
  3832. $result->feedback = $OUTPUT->box(get_string("finalwrong", "lesson"), 'feedback');
  3833. }
  3834. }
  3835. } else {
  3836. $result->feedback = '';
  3837. }
  3838. $class = 'response';
  3839. if ($result->correctanswer) {
  3840. $class .= ' correct'; // CSS over-ride this if they exist (!important).
  3841. } else if (!$result->isessayquestion) {
  3842. $class .= ' incorrect'; // CSS over-ride this if they exist (!important).
  3843. }
  3844. $options = new stdClass;
  3845. $options->noclean = true;
  3846. $options->para = true;
  3847. $options->overflowdiv = true;
  3848. $options->context = $context;
  3849. $options->attemptid = isset($attempt) ? $attempt->id : null;
  3850. $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options),
  3851. 'generalbox boxaligncenter py-3');
  3852. $result->feedback .= '<div class="correctanswer generalbox"><em>'
  3853. . get_string("youranswer", "lesson").'</em> : <div class="studentanswer mt-2 mb-2">';
  3854. // Create a table containing the answers and responses.
  3855. $table = new html_table();
  3856. // Multianswer allowed.
  3857. if ($this->properties->qoption) {
  3858. $studentanswerarray = explode(self::MULTIANSWER_DELIMITER, $result->studentanswer);
  3859. $responsearr = explode(self::MULTIANSWER_DELIMITER, $result->response);
  3860. $studentanswerresponse = array_combine($studentanswerarray, $responsearr);
  3861. foreach ($studentanswerresponse as $answer => $response) {
  3862. // Add a table row containing the answer.
  3863. $studentanswer = $this->format_answer($answer, $context, $result->studentanswerformat, $options);
  3864. $table->data[] = array($studentanswer);
  3865. // If the response exists, add a table row containing the response. If not, add en empty row.
  3866. if (!empty(trim($response))) {
  3867. $studentresponse = isset($result->responseformat) ?
  3868. $this->format_response($response, $context, $result->responseformat, $options) : $response;
  3869. $studentresponsecontent = html_writer::div('<em>' . get_string("response", "lesson") .
  3870. '</em>: <br/>' . $studentresponse, $class);
  3871. $table->data[] = array($studentresponsecontent);
  3872. } else {
  3873. $table->data[] = array('');
  3874. }
  3875. }
  3876. } else {
  3877. // Add a table row containing the answer.
  3878. $studentanswer = $this->format_answer($result->studentanswer, $context, $result->studentanswerformat, $options);
  3879. $table->data[] = array($studentanswer);
  3880. // If the response exists, add a table row containing the response. If not, add en empty row.
  3881. if (!empty(trim($result->response))) {
  3882. $studentresponse = isset($result->responseformat) ?
  3883. $this->format_response($result->response, $context, $result->responseformat,
  3884. $result->answerid, $options) : $result->response;
  3885. $studentresponsecontent = html_writer::div('<em>' . get_string("response", "lesson") .
  3886. '</em>: <br/>' . $studentresponse, $class);
  3887. $table->data[] = array($studentresponsecontent);
  3888. } else {
  3889. $table->data[] = array('');
  3890. }
  3891. }
  3892. $result->feedback .= html_writer::table($table).'</div></div>';
  3893. }
  3894. }
  3895. return $result;
  3896. }
  3897. /**
  3898. * Formats the answer. Override for custom formatting.
  3899. *
  3900. * @param string $answer
  3901. * @param context $context
  3902. * @param int $answerformat
  3903. * @return string Returns formatted string
  3904. */
  3905. public function format_answer($answer, $context, $answerformat, $options = []) {
  3906. if (is_object($options)) {
  3907. $options = (array) $options;
  3908. }
  3909. if (empty($options['context'])) {
  3910. $options['context'] = $context;
  3911. }
  3912. if (empty($options['para'])) {
  3913. $options['para'] = true;
  3914. }
  3915. return format_text($answer, $answerformat, $options);
  3916. }
  3917. /**
  3918. * Formats the response
  3919. *
  3920. * @param string $response
  3921. * @param context $context
  3922. * @param int $responseformat
  3923. * @param int $answerid
  3924. * @param stdClass $options
  3925. * @return string Returns formatted string
  3926. */
  3927. private function format_response($response, $context, $responseformat, $answerid, $options) {
  3928. $convertstudentresponse = file_rewrite_pluginfile_urls($response, 'pluginfile.php',
  3929. $context->id, 'mod_lesson', 'page_responses', $answerid);
  3930. return format_text($convertstudentresponse, $responseformat, $options);
  3931. }
  3932. /**
  3933. * Returns the string for a jump name
  3934. *
  3935. * @final
  3936. * @param int $jumpto Jump code or page ID
  3937. * @return string
  3938. **/
  3939. final protected function get_jump_name($jumpto) {
  3940. global $DB;
  3941. static $jumpnames = array();
  3942. if (!array_key_exists($jumpto, $jumpnames)) {
  3943. if ($jumpto == LESSON_THISPAGE) {
  3944. $jumptitle = get_string('thispage', 'lesson');
  3945. } elseif ($jumpto == LESSON_NEXTPAGE) {
  3946. $jumptitle = get_string('nextpage', 'lesson');
  3947. } elseif ($jumpto == LESSON_EOL) {
  3948. $jumptitle = get_string('endoflesson', 'lesson');
  3949. } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
  3950. $jumptitle = get_string('unseenpageinbranch', 'lesson');
  3951. } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
  3952. $jumptitle = get_string('previouspage', 'lesson');
  3953. } elseif ($jumpto == LESSON_RANDOMPAGE) {
  3954. $jumptitle = get_string('randompageinbranch', 'lesson');
  3955. } elseif ($jumpto == LESSON_RANDOMBRANCH) {
  3956. $jumptitle = get_string('randombranch', 'lesson');
  3957. } elseif ($jumpto == LESSON_CLUSTERJUMP) {
  3958. $jumptitle = get_string('clusterjump', 'lesson');
  3959. } else {
  3960. if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
  3961. $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
  3962. }
  3963. }
  3964. $jumpnames[$jumpto] = format_string($jumptitle,true);
  3965. }
  3966. return $jumpnames[$jumpto];
  3967. }
  3968. /**
  3969. * Constructor method
  3970. * @param object $properties
  3971. * @param lesson $lesson
  3972. */
  3973. public function __construct($properties, lesson $lesson) {
  3974. parent::__construct($properties);
  3975. $this->lesson = $lesson;
  3976. }
  3977. /**
  3978. * Returns the score for the attempt
  3979. * This may be overridden by page types that require manual grading
  3980. * @param array $answers
  3981. * @param object $attempt
  3982. * @return int
  3983. */
  3984. public function earned_score($answers, $attempt) {
  3985. return $answers[$attempt->answerid]->score;
  3986. }
  3987. /**
  3988. * This is a callback method that can be override and gets called when ever a page
  3989. * is viewed
  3990. *
  3991. * @param bool $canmanage True if the user has the manage cap
  3992. * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect.
  3993. * @return mixed
  3994. */
  3995. public function callback_on_view($canmanage, $redirect = true) {
  3996. return true;
  3997. }
  3998. /**
  3999. * save editor answers files and update answer record
  4000. *
  4001. * @param object $context
  4002. * @param int $maxbytes
  4003. * @param object $answer
  4004. * @param object $answereditor
  4005. * @param object $responseeditor
  4006. */
  4007. public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') {
  4008. global $DB;
  4009. if (isset($answereditor['itemid'])) {
  4010. $answer->answer = file_save_draft_area_files($answereditor['itemid'],
  4011. $context->id, 'mod_lesson', 'page_answers', $answer->id,
  4012. array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
  4013. $answer->answer, null);
  4014. $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id));
  4015. }
  4016. if (isset($responseeditor['itemid'])) {
  4017. $answer->response = file_save_draft_area_files($responseeditor['itemid'],
  4018. $context->id, 'mod_lesson', 'page_responses', $answer->id,
  4019. array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes),
  4020. $answer->response, null);
  4021. $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id));
  4022. }
  4023. }
  4024. /**
  4025. * Rewrite urls in response and optionality answer of a question answer
  4026. *
  4027. * @param object $answer
  4028. * @param bool $rewriteanswer must rewrite answer
  4029. * @return object answer with rewritten urls
  4030. */
  4031. public static function rewrite_answers_urls($answer, $rewriteanswer = true) {
  4032. global $PAGE;
  4033. $context = context_module::instance($PAGE->cm->id);
  4034. if ($rewriteanswer) {
  4035. $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id,
  4036. 'mod_lesson', 'page_answers', $answer->id);
  4037. }
  4038. $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id,
  4039. 'mod_lesson', 'page_responses', $answer->id);
  4040. return $answer;
  4041. }
  4042. /**
  4043. * Updates a lesson page and its answers within the database
  4044. *
  4045. * @param object $properties
  4046. * @return bool
  4047. */
  4048. public function update($properties, $context = null, $maxbytes = null) {
  4049. global $DB, $PAGE;
  4050. $answers = $this->get_answers();
  4051. $properties->id = $this->properties->id;
  4052. $properties->lessonid = $this->lesson->id;
  4053. if (empty($properties->qoption)) {
  4054. $properties->qoption = '0';
  4055. }
  4056. if (empty($context)) {
  4057. $context = $PAGE->context;
  4058. }
  4059. if ($maxbytes === null) {
  4060. $maxbytes = get_user_max_upload_file_size($context);
  4061. }
  4062. $properties->timemodified = time();
  4063. $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id);
  4064. $DB->update_record("lesson_pages", $properties);
  4065. // Trigger an event: page updated.
  4066. \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger();
  4067. if ($this->type == self::TYPE_STRUCTURE && $this->get_typeid() != LESSON_PAGE_BRANCHTABLE) {
  4068. // These page types have only one answer to save the jump and score.
  4069. if (count($answers) > 1) {
  4070. $answer = array_shift($answers);
  4071. foreach ($answers as $a) {
  4072. $DB->delete_records('lesson_answers', array('id' => $a->id));
  4073. }
  4074. } else if (count($answers) == 1) {
  4075. $answer = array_shift($answers);
  4076. } else {
  4077. $answer = new stdClass;
  4078. $answer->lessonid = $properties->lessonid;
  4079. $answer->pageid = $properties->id;
  4080. $answer->timecreated = time();
  4081. }
  4082. $answer->timemodified = time();
  4083. if (isset($properties->jumpto[0])) {
  4084. $answer->jumpto = $properties->jumpto[0];
  4085. }
  4086. if (isset($properties->score[0])) {
  4087. $answer->score = $properties->score[0];
  4088. }
  4089. if (!empty($answer->id)) {
  4090. $DB->update_record("lesson_answers", $answer->properties());
  4091. } else {
  4092. $DB->insert_record("lesson_answers", $answer);
  4093. }
  4094. } else {
  4095. for ($i = 0; $i < count($properties->answer_editor); $i++) {
  4096. if (!array_key_exists($i, $this->answers)) {
  4097. $this->answers[$i] = new stdClass;
  4098. $this->answers[$i]->lessonid = $this->lesson->id;
  4099. $this->answers[$i]->pageid = $this->id;
  4100. $this->answers[$i]->timecreated = $this->timecreated;
  4101. $this->answers[$i]->answer = null;
  4102. }
  4103. if (isset($properties->answer_editor[$i])) {
  4104. if (is_array($properties->answer_editor[$i])) {
  4105. // Multichoice and true/false pages have an HTML editor.
  4106. $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
  4107. $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
  4108. } else {
  4109. // Branch tables, shortanswer and mumerical pages have only a text field.
  4110. $this->answers[$i]->answer = $properties->answer_editor[$i];
  4111. $this->answers[$i]->answerformat = FORMAT_MOODLE;
  4112. }
  4113. } else {
  4114. // If there is no data posted which means we want to reset the stored values.
  4115. $this->answers[$i]->answer = null;
  4116. }
  4117. if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
  4118. $this->answers[$i]->response = $properties->response_editor[$i]['text'];
  4119. $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
  4120. }
  4121. if ($this->answers[$i]->answer !== null && $this->answers[$i]->answer !== '') {
  4122. if (isset($properties->jumpto[$i])) {
  4123. $this->answers[$i]->jumpto = $properties->jumpto[$i];
  4124. }
  4125. if ($this->lesson->custom && isset($properties->score[$i])) {
  4126. $this->answers[$i]->score = $properties->score[$i];
  4127. }
  4128. if (!isset($this->answers[$i]->id)) {
  4129. $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]);
  4130. } else {
  4131. $DB->update_record("lesson_answers", $this->answers[$i]->properties());
  4132. }
  4133. // Save files in answers and responses.
  4134. if (isset($properties->response_editor[$i])) {
  4135. $this->save_answers_files($context, $maxbytes, $this->answers[$i],
  4136. $properties->answer_editor[$i], $properties->response_editor[$i]);
  4137. } else {
  4138. $this->save_answers_files($context, $maxbytes, $this->answers[$i],
  4139. $properties->answer_editor[$i]);
  4140. }
  4141. } else if (isset($this->answers[$i]->id)) {
  4142. $DB->delete_records('lesson_answers', array('id' => $this->answers[$i]->id));
  4143. unset($this->answers[$i]);
  4144. }
  4145. }
  4146. }
  4147. return true;
  4148. }
  4149. /**
  4150. * Can be set to true if the page requires a static link to create a new instance
  4151. * instead of simply being included in the dropdown
  4152. * @param int $previd
  4153. * @return bool
  4154. */
  4155. public function add_page_link($previd) {
  4156. return false;
  4157. }
  4158. /**
  4159. * Returns true if a page has been viewed before
  4160. *
  4161. * @param array|int $param Either an array of pages that have been seen or the
  4162. * number of retakes a user has had
  4163. * @return bool
  4164. */
  4165. public function is_unseen($param) {
  4166. global $USER, $DB;
  4167. if (is_array($param)) {
  4168. $seenpages = $param;
  4169. return (!array_key_exists($this->properties->id, $seenpages));
  4170. } else {
  4171. $nretakes = $param;
  4172. if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
  4173. return true;
  4174. }
  4175. }
  4176. return false;
  4177. }
  4178. /**
  4179. * Checks to see if a page has been answered previously
  4180. * @param int $nretakes
  4181. * @return bool
  4182. */
  4183. public function is_unanswered($nretakes) {
  4184. global $DB, $USER;
  4185. if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
  4186. return true;
  4187. }
  4188. return false;
  4189. }
  4190. /**
  4191. * Creates answers within the database for this lesson_page. Usually only ever
  4192. * called when creating a new page instance
  4193. * @param object $properties
  4194. * @return array
  4195. */
  4196. public function create_answers($properties) {
  4197. global $DB, $PAGE;
  4198. // now add the answers
  4199. $newanswer = new stdClass;
  4200. $newanswer->lessonid = $this->lesson->id;
  4201. $newanswer->pageid = $this->properties->id;
  4202. $newanswer->timecreated = $this->properties->timecreated;
  4203. $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course);
  4204. $context = context_module::instance($cm->id);
  4205. $answers = array();
  4206. for ($i = 0; $i < ($this->lesson->maxanswers + 1); $i++) {
  4207. $answer = clone($newanswer);
  4208. if (isset($properties->answer_editor[$i])) {
  4209. if (is_array($properties->answer_editor[$i])) {
  4210. // Multichoice and true/false pages have an HTML editor.
  4211. $answer->answer = $properties->answer_editor[$i]['text'];
  4212. $answer->answerformat = $properties->answer_editor[$i]['format'];
  4213. } else {
  4214. // Branch tables, shortanswer and mumerical pages have only a text field.
  4215. $answer->answer = $properties->answer_editor[$i];
  4216. $answer->answerformat = FORMAT_MOODLE;
  4217. }
  4218. }
  4219. if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) {
  4220. $answer->response = $properties->response_editor[$i]['text'];
  4221. $answer->responseformat = $properties->response_editor[$i]['format'];
  4222. }
  4223. if (isset($answer->answer) && $answer->answer != '') {
  4224. if (isset($properties->jumpto[$i])) {
  4225. $answer->jumpto = $properties->jumpto[$i];
  4226. }
  4227. if ($this->lesson->custom && isset($properties->score[$i])) {
  4228. $answer->score = $properties->score[$i];
  4229. }
  4230. $answer->id = $DB->insert_record("lesson_answers", $answer);
  4231. if (isset($properties->response_editor[$i])) {
  4232. $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
  4233. $properties->answer_editor[$i], $properties->response_editor[$i]);
  4234. } else {
  4235. $this->save_answers_files($context, $PAGE->course->maxbytes, $answer,
  4236. $properties->answer_editor[$i]);
  4237. }
  4238. $answers[$answer->id] = new lesson_page_answer($answer);
  4239. }
  4240. }
  4241. $this->answers = $answers;
  4242. return $answers;
  4243. }
  4244. /**
  4245. * This method MUST be overridden by all question page types, or page types that
  4246. * wish to score a page.
  4247. *
  4248. * The structure of result should always be the same so it is a good idea when
  4249. * overriding this method on a page type to call
  4250. * <code>
  4251. * $result = parent::check_answer();
  4252. * </code>
  4253. * before modifying it as required.
  4254. *
  4255. * @return stdClass
  4256. */
  4257. public function check_answer() {
  4258. $result = new stdClass;
  4259. $result->answerid = 0;
  4260. $result->noanswer = false;
  4261. $result->correctanswer = false;
  4262. $result->isessayquestion = false; // use this to turn off review button on essay questions
  4263. $result->response = '';
  4264. $result->newpageid = 0; // stay on the page
  4265. $result->studentanswer = ''; // use this to store student's answer(s) in order to display it on feedback page
  4266. $result->studentanswerformat = FORMAT_MOODLE;
  4267. $result->userresponse = null;
  4268. $result->feedback = '';
  4269. // Store data that was POSTd by a form. This is currently used to perform any logic after the 1st write to the db
  4270. // of the attempt.
  4271. $result->postdata = false;
  4272. $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off
  4273. $result->inmediatejump = false; // Flag to detect when we should do a jump from the page without further processing.
  4274. return $result;
  4275. }
  4276. /**
  4277. * Do any post persistence processing logic of an attempt. E.g. in cases where we need update file urls in an editor
  4278. * and we need to have the id of the stored attempt. Should be overridden in each individual child
  4279. * pagetype on a as required basis
  4280. *
  4281. * @param object $attempt The attempt corresponding to the db record
  4282. * @param object $result The result from the 'check_answer' method
  4283. * @return array False if nothing to be modified, updated $attempt and $result if update required.
  4284. */
  4285. public function on_after_write_attempt($attempt, $result) {
  4286. return [false, false];
  4287. }
  4288. /**
  4289. * True if the page uses a custom option
  4290. *
  4291. * Should be override and set to true if the page uses a custom option.
  4292. *
  4293. * @return bool
  4294. */
  4295. public function has_option() {
  4296. return false;
  4297. }
  4298. /**
  4299. * Returns the maximum number of answers for this page given the maximum number
  4300. * of answers permitted by the lesson.
  4301. *
  4302. * @param int $default
  4303. * @return int
  4304. */
  4305. public function max_answers($default) {
  4306. return $default;
  4307. }
  4308. /**
  4309. * Returns the properties of this lesson page as an object
  4310. * @return stdClass;
  4311. */
  4312. public function properties() {
  4313. $properties = clone($this->properties);
  4314. if ($this->answers === null) {
  4315. $this->get_answers();
  4316. }
  4317. if (count($this->answers)>0) {
  4318. $count = 0;
  4319. $qtype = $properties->qtype;
  4320. foreach ($this->answers as $answer) {
  4321. $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat);
  4322. if ($qtype != LESSON_PAGE_MATCHING) {
  4323. $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat);
  4324. } else {
  4325. $properties->{'response_editor['.$count.']'} = $answer->response;
  4326. }
  4327. $properties->{'jumpto['.$count.']'} = $answer->jumpto;
  4328. $properties->{'score['.$count.']'} = $answer->score;
  4329. $count++;
  4330. }
  4331. }
  4332. return $properties;
  4333. }
  4334. /**
  4335. * Returns an array of options to display when choosing the jumpto for a page/answer
  4336. * @static
  4337. * @param int $pageid
  4338. * @param lesson $lesson
  4339. * @return array
  4340. */
  4341. public static function get_jumptooptions($pageid, lesson $lesson) {
  4342. global $DB;
  4343. $jump = array();
  4344. $jump[0] = get_string("thispage", "lesson");
  4345. $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
  4346. $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
  4347. $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
  4348. if ($pageid == 0) {
  4349. return $jump;
  4350. }
  4351. $pages = $lesson->load_all_pages();
  4352. 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))) {
  4353. $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
  4354. $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
  4355. }
  4356. if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
  4357. $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
  4358. }
  4359. if (!optional_param('firstpage', 0, PARAM_INT)) {
  4360. $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
  4361. while (true) {
  4362. if ($apageid) {
  4363. $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
  4364. $jump[$apageid] = strip_tags(format_string($title,true));
  4365. $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
  4366. } else {
  4367. // last page reached
  4368. break;
  4369. }
  4370. }
  4371. }
  4372. return $jump;
  4373. }
  4374. /**
  4375. * Returns the contents field for the page properly formatted and with plugin
  4376. * file url's converted
  4377. * @return string
  4378. */
  4379. public function get_contents() {
  4380. global $PAGE;
  4381. if (!empty($this->properties->contents)) {
  4382. if (!isset($this->properties->contentsformat)) {
  4383. $this->properties->contentsformat = FORMAT_HTML;
  4384. }
  4385. $context = context_module::instance($PAGE->cm->id);
  4386. $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson',
  4387. 'page_contents', $this->properties->id); // Must do this BEFORE format_text()!
  4388. return format_text($contents, $this->properties->contentsformat,
  4389. array('context' => $context, 'noclean' => true,
  4390. 'overflowdiv' => true)); // Page edit is marked with XSS, we want all content here.
  4391. } else {
  4392. return '';
  4393. }
  4394. }
  4395. /**
  4396. * Set to true if this page should display in the menu block
  4397. * @return bool
  4398. */
  4399. protected function get_displayinmenublock() {
  4400. return false;
  4401. }
  4402. /**
  4403. * Get the string that describes the options of this page type
  4404. * @return string
  4405. */
  4406. public function option_description_string() {
  4407. return '';
  4408. }
  4409. /**
  4410. * Updates a table with the answers for this page
  4411. * @param html_table $table
  4412. * @return html_table
  4413. */
  4414. public function display_answers(html_table $table) {
  4415. $answers = $this->get_answers();
  4416. $i = 1;
  4417. foreach ($answers as $answer) {
  4418. $cells = array();
  4419. $cells[] = '<label>' . get_string('jump', 'lesson') . ' ' . $i . '</label>:';
  4420. $cells[] = $this->get_jump_name($answer->jumpto);
  4421. $table->data[] = new html_table_row($cells);
  4422. if ($i === 1){
  4423. $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
  4424. }
  4425. $i++;
  4426. }
  4427. return $table;
  4428. }
  4429. /**
  4430. * Determines if this page should be grayed out on the management/report screens
  4431. * @return int 0 or 1
  4432. */
  4433. protected function get_grayout() {
  4434. return 0;
  4435. }
  4436. /**
  4437. * Adds stats for this page to the &pagestats object. This should be defined
  4438. * for all page types that grade
  4439. * @param array $pagestats
  4440. * @param int $tries
  4441. * @return bool
  4442. */
  4443. public function stats(array &$pagestats, $tries) {
  4444. return true;
  4445. }
  4446. /**
  4447. * Formats the answers of this page for a report
  4448. *
  4449. * @param object $answerpage
  4450. * @param object $answerdata
  4451. * @param object $useranswer
  4452. * @param array $pagestats
  4453. * @param int $i Count of first level answers
  4454. * @param int $n Count of second level answers
  4455. * @return object The answer page for this
  4456. */
  4457. public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
  4458. $answers = $this->get_answers();
  4459. $formattextdefoptions = new stdClass;
  4460. $formattextdefoptions->para = false; //I'll use it widely in this page
  4461. foreach ($answers as $answer) {
  4462. $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
  4463. $answerdata->answers[] = array($data, "");
  4464. $answerpage->answerdata = $answerdata;
  4465. }
  4466. return $answerpage;
  4467. }
  4468. /**
  4469. * Gets an array of the jumps used by the answers of this page
  4470. *
  4471. * @return array
  4472. */
  4473. public function get_jumps() {
  4474. global $DB;
  4475. $jumps = array();
  4476. $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
  4477. if ($answers = $this->get_answers()) {
  4478. foreach ($answers as $answer) {
  4479. $jumps[] = $this->get_jump_name($answer->jumpto);
  4480. }
  4481. } else {
  4482. $jumps[] = $this->get_jump_name($this->properties->nextpageid);
  4483. }
  4484. return $jumps;
  4485. }
  4486. /**
  4487. * Informs whether this page type require manual grading or not
  4488. * @return bool
  4489. */
  4490. public function requires_manual_grading() {
  4491. return false;
  4492. }
  4493. /**
  4494. * A callback method that allows a page to override the next page a user will
  4495. * see during when this page is being completed.
  4496. * @return false|int
  4497. */
  4498. public function override_next_page() {
  4499. return false;
  4500. }
  4501. /**
  4502. * This method is used to determine if this page is a valid page
  4503. *
  4504. * @param array $validpages
  4505. * @param array $pageviews
  4506. * @return int The next page id to check
  4507. */
  4508. public function valid_page_and_view(&$validpages, &$pageviews) {
  4509. $validpages[$this->properties->id] = 1;
  4510. return $this->properties->nextpageid;
  4511. }
  4512. /**
  4513. * Get files from the page area file.
  4514. *
  4515. * @param bool $includedirs whether or not include directories
  4516. * @param int $updatedsince return files updated since this time
  4517. * @return array list of stored_file objects
  4518. * @since Moodle 3.2
  4519. */
  4520. public function get_files($includedirs = true, $updatedsince = 0) {
  4521. $fs = get_file_storage();
  4522. return $fs->get_area_files($this->lesson->context->id, 'mod_lesson', 'page_contents', $this->properties->id,
  4523. 'itemid, filepath, filename', $includedirs, $updatedsince);
  4524. }
  4525. /**
  4526. * Make updates to the form data if required.
  4527. *
  4528. * @since Moodle 3.7
  4529. * @param stdClass $data The form data to update.
  4530. * @return stdClass The updated fom data.
  4531. */
  4532. public function update_form_data(stdClass $data) : stdClass {
  4533. return $data;
  4534. }
  4535. }
  4536. /**
  4537. * Class used to represent an answer to a page
  4538. *
  4539. * @property int $id The ID of this answer in the database
  4540. * @property int $lessonid The ID of the lesson this answer belongs to
  4541. * @property int $pageid The ID of the page this answer belongs to
  4542. * @property int $jumpto Identifies where the user goes upon completing a page with this answer
  4543. * @property int $grade The grade this answer is worth
  4544. * @property int $score The score this answer will give
  4545. * @property int $flags Used to store options for the answer
  4546. * @property int $timecreated A timestamp of when the answer was created
  4547. * @property int $timemodified A timestamp of when the answer was modified
  4548. * @property string $answer The answer itself
  4549. * @property string $response The response the user sees if selecting this answer
  4550. *
  4551. * @copyright 2009 Sam Hemelryk
  4552. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4553. */
  4554. class lesson_page_answer extends lesson_base {
  4555. /**
  4556. * Loads an page answer from the DB
  4557. *
  4558. * @param int $id
  4559. * @return lesson_page_answer
  4560. */
  4561. public static function load($id) {
  4562. global $DB;
  4563. $answer = $DB->get_record("lesson_answers", array("id" => $id));
  4564. return new lesson_page_answer($answer);
  4565. }
  4566. /**
  4567. * Given an object of properties and a page created answer(s) and saves them
  4568. * in the database.
  4569. *
  4570. * @param stdClass $properties
  4571. * @param lesson_page $page
  4572. * @return array
  4573. */
  4574. public static function create($properties, lesson_page $page) {
  4575. return $page->create_answers($properties);
  4576. }
  4577. /**
  4578. * Get files from the answer area file.
  4579. *
  4580. * @param bool $includedirs whether or not include directories
  4581. * @param int $updatedsince return files updated since this time
  4582. * @return array list of stored_file objects
  4583. * @since Moodle 3.2
  4584. */
  4585. public function get_files($includedirs = true, $updatedsince = 0) {
  4586. $lesson = lesson::load($this->properties->lessonid);
  4587. $fs = get_file_storage();
  4588. $answerfiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_answers', $this->properties->id,
  4589. 'itemid, filepath, filename', $includedirs, $updatedsince);
  4590. $responsefiles = $fs->get_area_files($lesson->context->id, 'mod_lesson', 'page_responses', $this->properties->id,
  4591. 'itemid, filepath, filename', $includedirs, $updatedsince);
  4592. return array_merge($answerfiles, $responsefiles);
  4593. }
  4594. }
  4595. /**
  4596. * A management class for page types
  4597. *
  4598. * This class is responsible for managing the different pages. A manager object can
  4599. * be retrieved by calling the following line of code:
  4600. * <code>
  4601. * $manager = lesson_page_type_manager::get($lesson);
  4602. * </code>
  4603. * The first time the page type manager is retrieved the it includes all of the
  4604. * different page types located in mod/lesson/pagetypes.
  4605. *
  4606. * @copyright 2009 Sam Hemelryk
  4607. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4608. */
  4609. class lesson_page_type_manager {
  4610. /**
  4611. * An array of different page type classes
  4612. * @var array
  4613. */
  4614. protected $types = array();
  4615. /**
  4616. * Retrieves the lesson page type manager object
  4617. *
  4618. * If the object hasn't yet been created it is created here.
  4619. *
  4620. * @staticvar lesson_page_type_manager $pagetypemanager
  4621. * @param lesson $lesson
  4622. * @return lesson_page_type_manager
  4623. */
  4624. public static function get(lesson $lesson) {
  4625. static $pagetypemanager;
  4626. if (!($pagetypemanager instanceof lesson_page_type_manager)) {
  4627. $pagetypemanager = new lesson_page_type_manager();
  4628. $pagetypemanager->load_lesson_types($lesson);
  4629. }
  4630. return $pagetypemanager;
  4631. }
  4632. /**
  4633. * Finds and loads all lesson page types in mod/lesson/pagetypes
  4634. *
  4635. * @param lesson $lesson
  4636. */
  4637. public function load_lesson_types(lesson $lesson) {
  4638. global $CFG;
  4639. $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
  4640. $dir = dir($basedir);
  4641. while (false !== ($entry = $dir->read())) {
  4642. if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
  4643. continue;
  4644. }
  4645. require_once($basedir.$entry);
  4646. $class = 'lesson_page_type_'.strtok($entry,'.');
  4647. if (class_exists($class)) {
  4648. $pagetype = new $class(new stdClass, $lesson);
  4649. $this->types[$pagetype->typeid] = $pagetype;
  4650. }
  4651. }
  4652. }
  4653. /**
  4654. * Returns an array of strings to describe the loaded page types
  4655. *
  4656. * @param int $type Can be used to return JUST the string for the requested type
  4657. * @return array
  4658. */
  4659. public function get_page_type_strings($type=null, $special=true) {
  4660. $types = array();
  4661. foreach ($this->types as $pagetype) {
  4662. if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
  4663. $types[$pagetype->typeid] = $pagetype->typestring;
  4664. }
  4665. }
  4666. return $types;
  4667. }
  4668. /**
  4669. * Returns the basic string used to identify a page type provided with an id
  4670. *
  4671. * This string can be used to instantiate or identify the page type class.
  4672. * If the page type id is unknown then 'unknown' is returned
  4673. *
  4674. * @param int $id
  4675. * @return string
  4676. */
  4677. public function get_page_type_idstring($id) {
  4678. foreach ($this->types as $pagetype) {
  4679. if ((int)$pagetype->typeid === (int)$id) {
  4680. return $pagetype->idstring;
  4681. }
  4682. }
  4683. return 'unknown';
  4684. }
  4685. /**
  4686. * Loads a page for the provided lesson given it's id
  4687. *
  4688. * This function loads a page from the lesson when given both the lesson it belongs
  4689. * to as well as the page's id.
  4690. * If the page doesn't exist an error is thrown
  4691. *
  4692. * @param int $pageid The id of the page to load
  4693. * @param lesson $lesson The lesson the page belongs to
  4694. * @return lesson_page A class that extends lesson_page
  4695. */
  4696. public function load_page($pageid, lesson $lesson) {
  4697. global $DB;
  4698. if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
  4699. print_error('cannotfindpages', 'lesson');
  4700. }
  4701. $pagetype = get_class($this->types[$page->qtype]);
  4702. $page = new $pagetype($page, $lesson);
  4703. return $page;
  4704. }
  4705. /**
  4706. * This function detects errors in the ordering between 2 pages and updates the page records.
  4707. *
  4708. * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list.
  4709. * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list.
  4710. */
  4711. protected function check_page_order($page1, $page2) {
  4712. global $DB;
  4713. if (empty($page1)) {
  4714. if ($page2->prevpageid != 0) {
  4715. debugging("***prevpageid of page " . $page2->id . " set to 0***");
  4716. $page2->prevpageid = 0;
  4717. $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id));
  4718. }
  4719. } else if (empty($page2)) {
  4720. if ($page1->nextpageid != 0) {
  4721. debugging("***nextpageid of page " . $page1->id . " set to 0***");
  4722. $page1->nextpageid = 0;
  4723. $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id));
  4724. }
  4725. } else {
  4726. if ($page1->nextpageid != $page2->id) {
  4727. debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***");
  4728. $page1->nextpageid = $page2->id;
  4729. $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id));
  4730. }
  4731. if ($page2->prevpageid != $page1->id) {
  4732. debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***");
  4733. $page2->prevpageid = $page1->id;
  4734. $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id));
  4735. }
  4736. }
  4737. }
  4738. /**
  4739. * This function loads ALL pages that belong to the lesson.
  4740. *
  4741. * @param lesson $lesson
  4742. * @return array An array of lesson_page_type_*
  4743. */
  4744. public function load_all_pages(lesson $lesson) {
  4745. global $DB;
  4746. if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
  4747. return array(); // Records returned empty.
  4748. }
  4749. foreach ($pages as $key=>$page) {
  4750. $pagetype = get_class($this->types[$page->qtype]);
  4751. $pages[$key] = new $pagetype($page, $lesson);
  4752. }
  4753. $orderedpages = array();
  4754. $lastpageid = 0;
  4755. $morepages = true;
  4756. while ($morepages) {
  4757. $morepages = false;
  4758. foreach ($pages as $page) {
  4759. if ((int)$page->prevpageid === (int)$lastpageid) {
  4760. // Check for errors in page ordering and fix them on the fly.
  4761. $prevpage = null;
  4762. if ($lastpageid !== 0) {
  4763. $prevpage = $orderedpages[$lastpageid];
  4764. }
  4765. $this->check_page_order($prevpage, $page);
  4766. $morepages = true;
  4767. $orderedpages[$page->id] = $page;
  4768. unset($pages[$page->id]);
  4769. $lastpageid = $page->id;
  4770. if ((int)$page->nextpageid===0) {
  4771. break 2;
  4772. } else {
  4773. break 1;
  4774. }
  4775. }
  4776. }
  4777. }
  4778. // Add remaining pages and fix the nextpageid links for each page.
  4779. foreach ($pages as $page) {
  4780. // Check for errors in page ordering and fix them on the fly.
  4781. $prevpage = null;
  4782. if ($lastpageid !== 0) {
  4783. $prevpage = $orderedpages[$lastpageid];
  4784. }
  4785. $this->check_page_order($prevpage, $page);
  4786. $orderedpages[$page->id] = $page;
  4787. unset($pages[$page->id]);
  4788. $lastpageid = $page->id;
  4789. }
  4790. if ($lastpageid !== 0) {
  4791. $this->check_page_order($orderedpages[$lastpageid], null);
  4792. }
  4793. return $orderedpages;
  4794. }
  4795. /**
  4796. * Fetches an mform that can be used to create/edit an page
  4797. *
  4798. * @param int $type The id for the page type
  4799. * @param array $arguments Any arguments to pass to the mform
  4800. * @return lesson_add_page_form_base
  4801. */
  4802. public function get_page_form($type, $arguments) {
  4803. $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
  4804. if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
  4805. debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
  4806. $class = 'lesson_add_page_form_selection';
  4807. } else if ($class === 'lesson_add_page_form_unknown') {
  4808. $class = 'lesson_add_page_form_selection';
  4809. }
  4810. return new $class(null, $arguments);
  4811. }
  4812. /**
  4813. * Returns an array of links to use as add page links
  4814. * @param int $previd The id of the previous page
  4815. * @return array
  4816. */
  4817. public function get_add_page_type_links($previd) {
  4818. global $OUTPUT;
  4819. $links = array();
  4820. foreach ($this->types as $key=>$type) {
  4821. if ($link = $type->add_page_link($previd)) {
  4822. $links[$key] = $link;
  4823. }
  4824. }
  4825. return $links;
  4826. }
  4827. }