PageRenderTime 85ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 1ms

/mod/lesson/locallib.php

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