3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Local library file for Lesson. These are non-standard functions that are used
23 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
27 /** Make sure this isn't being directly accessed */
28 defined('MOODLE_INTERNAL') || die();
30 /** Include the files that are required by this module */
31 require_once($CFG->dirroot.'/course/moodleform_mod.php');
32 require_once($CFG->dirroot . '/mod/lesson/lib.php');
33 require_once($CFG->libdir . '/filelib.php');
36 define('LESSON_THISPAGE', 0);
37 /** Next page -> any page not seen before */
38 define("LESSON_UNSEENPAGE", 1);
39 /** Next page -> any page not answered correctly */
40 define("LESSON_UNANSWEREDPAGE", 2);
41 /** Jump to Next Page */
42 define("LESSON_NEXTPAGE", -1);
44 define("LESSON_EOL", -9);
45 /** Jump to an unseen page within a branch and end of branch or end of lesson */
46 define("LESSON_UNSEENBRANCHPAGE", -50);
47 /** Jump to Previous Page */
48 define("LESSON_PREVIOUSPAGE", -40);
49 /** Jump to a random page within a branch and end of branch or end of lesson */
50 define("LESSON_RANDOMPAGE", -60);
51 /** Jump to a random Branch */
52 define("LESSON_RANDOMBRANCH", -70);
54 define("LESSON_CLUSTERJUMP", -80);
56 define("LESSON_UNDEFINED", -99);
58 /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
59 define("LESSON_MAX_EVENT_LENGTH", "432000");
61 /** Answer format is HTML */
62 define("LESSON_ANSWER_HTML", "HTML");
64 //////////////////////////////////////////////////////////////////////////////////////
65 /// Any other lesson functions go here. Each of them must have a name that
66 /// starts with lesson_
69 * Checks to see if a LESSON_CLUSTERJUMP or
70 * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
72 * This function is only executed when a teacher is
73 * checking the navigation for a lesson.
75 * @param stdClass $lesson Id of the lesson that is to be checked.
76 * @return boolean True or false.
78 function lesson_display_teacher_warning($lesson) {
81 // get all of the lesson answers
82 $params = array ("lessonid" => $lesson->id);
83 if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
84 // no answers, then not using cluster or unseen
87 // just check for the first one that fulfills the requirements
88 foreach ($lessonanswers as $lessonanswer) {
89 if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
94 // if no answers use either of the two jumps
99 * Interprets the LESSON_UNSEENBRANCHPAGE jump.
101 * will return the pageid of a random unseen page that is within a branch
103 * @param lesson $lesson
104 * @param int $userid Id of the user.
105 * @param int $pageid Id of the page from which we are jumping.
106 * @return int Id of the next page.
108 function lesson_unseen_question_jump($lesson, $user, $pageid) {
111 // get the number of retakes
112 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
116 // get all the lesson_attempts aka what the user has seen
117 if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
118 foreach($viewedpages as $viewed) {
119 $seenpages[] = $viewed->pageid;
122 $seenpages = array();
125 // get the lesson pages
126 $lessonpages = $lesson->load_all_pages();
128 if ($pageid == LESSON_UNSEENBRANCHPAGE) { // this only happens when a student leaves in the middle of an unseen question within a branch series
129 $pageid = $seenpages[0]; // just change the pageid to the last page viewed inside the branch table
132 // go up the pages till branch table
133 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
134 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
137 $pageid = $lessonpages[$pageid]->prevpageid;
140 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
142 // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
144 foreach($pagesinbranch as $page) {
145 if (!in_array($page->id, $seenpages)) {
146 $unseen[] = $page->id;
150 if(count($unseen) == 0) {
151 if(isset($pagesinbranch)) {
152 $temp = end($pagesinbranch);
153 $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
155 // there are no pages inside the branch, so return the next page
156 $nextpage = $lessonpages[$pageid]->nextpageid;
158 if ($nextpage == 0) {
164 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
169 * Handles the unseen branch table jump.
171 * @param lesson $lesson
172 * @param int $userid User id.
173 * @return int Will return the page id of a branch table or end of lesson
175 function lesson_unseen_branch_jump($lesson, $userid) {
178 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
182 if (!$seenbranches = $lesson->get_content_pages_viewed($retakes, $userid, 'timeseen DESC')) {
183 print_error('cannotfindrecords', 'lesson');
186 // get the lesson pages
187 $lessonpages = $lesson->load_all_pages();
189 // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
190 // which is the branch table that starts the unseenbranch function
192 foreach ($seenbranches as $seenbranch) {
193 if (!$seenbranch->flag) {
194 $seen[$seenbranch->pageid] = $seenbranch->pageid;
196 $start = $seenbranch->pageid;
200 // this function searches through the lesson pages to find all the branch tables
201 // that follow the flagged branch table
202 $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
203 $branchtables = array();
204 while ($pageid != 0) { // grab all of the branch table till eol
205 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
206 $branchtables[] = $lessonpages[$pageid]->id;
208 $pageid = $lessonpages[$pageid]->nextpageid;
211 foreach ($branchtables as $branchtable) {
212 // load all of the unseen branch tables into unseen
213 if (!array_key_exists($branchtable, $seen)) {
214 $unseen[] = $branchtable;
217 if (count($unseen) > 0) {
218 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
220 return LESSON_EOL; // has viewed all of the branch tables
225 * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
227 * @param lesson $lesson
228 * @param int $pageid The id of the page that we are jumping from (?)
229 * @return int The pageid of a random page that is within a branch table
231 function lesson_random_question_jump($lesson, $pageid) {
234 // get the lesson pages
235 $params = array ("lessonid" => $lesson->id);
236 if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
237 print_error('cannotfindpages', 'lesson');
240 // go up the pages till branch table
241 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
243 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
246 $pageid = $lessonpages[$pageid]->prevpageid;
249 // get the pages within the branch
250 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
252 if(count($pagesinbranch) == 0) {
253 // there are no pages inside the branch, so return the next page
254 return $lessonpages[$pageid]->nextpageid;
256 return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id; // returns a random page id for the next page
261 * Calculates a user's grade for a lesson.
263 * @param object $lesson The lesson that the user is taking.
264 * @param int $retries The attempt number.
265 * @param int $userid Id of the user (optional, default current user).
266 * @return object { nquestions => number of questions answered
267 attempts => number of question attempts
268 total => max points possible
269 earned => points earned by student
270 grade => calculated percentage grade
271 nmanual => number of manually graded questions
272 manualpoints => point value for manually graded questions }
274 function lesson_grade($lesson, $ntries, $userid = 0) {
277 if (empty($userid)) {
281 // Zero out everything
292 $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
293 if ($useranswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND
294 userid = :userid AND retry = :retry", $params, "timeseen")) {
295 // group each try with its page
296 $attemptset = array();
297 foreach ($useranswers as $useranswer) {
298 $attemptset[$useranswer->pageid][] = $useranswer;
301 // Drop all attempts that go beyond max attempts for the lesson
302 foreach ($attemptset as $key => $set) {
303 $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
306 // get only the pages and their answers that the user answered
307 list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
308 array_unshift($parameters, $lesson->id);
309 $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
310 $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
312 // Number of pages answered
313 $nquestions = count($pages);
315 foreach ($attemptset as $attempts) {
316 $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
317 if ($lesson->custom) {
318 $attempt = end($attempts);
319 // If essay question, handle it, otherwise add to score
320 if ($page->requires_manual_grading()) {
321 $useranswerobj = unserialize($attempt->useranswer);
322 if (isset($useranswerobj->score)) {
323 $earned += $useranswerobj->score;
326 $manualpoints += $answers[$attempt->answerid]->score;
327 } else if (!empty($attempt->answerid)) {
328 $earned += $page->earned_score($answers, $attempt);
331 foreach ($attempts as $attempt) {
332 $earned += $attempt->correct;
334 $attempt = end($attempts); // doesn't matter which one
335 // If essay question, increase numbers
336 if ($page->requires_manual_grading()) {
341 // Number of times answered
342 $nviewed += count($attempts);
345 if ($lesson->custom) {
346 $bestscores = array();
347 // Find the highest possible score per page to get our total
348 foreach ($answers as $answer) {
349 if(!isset($bestscores[$answer->pageid])) {
350 $bestscores[$answer->pageid] = $answer->score;
351 } else if ($bestscores[$answer->pageid] < $answer->score) {
352 $bestscores[$answer->pageid] = $answer->score;
355 $total = array_sum($bestscores);
357 // Check to make sure the student has answered the minimum questions
358 if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
359 // Nope, increase number viewed by the amount of unanswered questions
360 $total = $nviewed + ($lesson->minquestions - $nquestions);
367 if ($total) { // not zero
368 $thegrade = round(100 * $earned / $total, 5);
371 // Build the grade information object
372 $gradeinfo = new stdClass;
373 $gradeinfo->nquestions = $nquestions;
374 $gradeinfo->attempts = $nviewed;
375 $gradeinfo->total = $total;
376 $gradeinfo->earned = $earned;
377 $gradeinfo->grade = $thegrade;
378 $gradeinfo->nmanual = $nmanual;
379 $gradeinfo->manualpoints = $manualpoints;
385 * Determines if a user can view the left menu. The determining factor
386 * is whether a user has a grade greater than or equal to the lesson setting
389 * @param object $lesson Lesson object of the current lesson
390 * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
392 function lesson_displayleftif($lesson) {
393 global $CFG, $USER, $DB;
395 if (!empty($lesson->displayleftif)) {
396 // get the current user's max grade for this lesson
397 $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
398 if ($maxgrade = $DB->get_record_sql('SELECT userid, MAX(grade) AS maxgrade FROM {lesson_grades} WHERE userid = :userid AND lessonid = :lessonid GROUP BY userid', $params)) {
399 if ($maxgrade->maxgrade < $lesson->displayleftif) {
400 return 0; // turn off the displayleft
403 return 0; // no grades
407 // if we get to here, keep the original state of displayleft lesson setting
408 return $lesson->displayleft;
416 * @return unknown_type
418 function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
419 $bc = lesson_menu_block_contents($cm->id, $lesson);
421 $regions = $page->blocks->get_regions();
422 $firstregion = reset($regions);
423 $page->blocks->add_fake_block($bc, $firstregion);
426 $bc = lesson_mediafile_block_contents($cm->id, $lesson);
428 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
431 if (!empty($timer)) {
432 $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
434 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
440 * If there is a media file associated with this
441 * lesson, return a block_contents that displays it.
443 * @param int $cmid Course Module ID for this lesson
444 * @param object $lesson Full lesson record object
445 * @return block_contents
447 function lesson_mediafile_block_contents($cmid, $lesson) {
449 if (empty($lesson->mediafile)) {
454 $options['menubar'] = 0;
455 $options['location'] = 0;
456 $options['left'] = 5;
458 $options['scrollbars'] = 1;
459 $options['resizable'] = 1;
460 $options['width'] = $lesson->mediawidth;
461 $options['height'] = $lesson->mediaheight;
463 $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
464 $action = new popup_action('click', $link, 'lessonmediafile', $options);
465 $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
467 $bc = new block_contents();
468 $bc->title = get_string('linkedmedia', 'lesson');
469 $bc->attributes['class'] = 'mediafile block';
470 $bc->content = $content;
476 * If a timed lesson and not a teacher, then
477 * return a block_contents containing the clock.
479 * @param int $cmid Course Module ID for this lesson
480 * @param object $lesson Full lesson record object
481 * @param object $timer Full timer record object
482 * @return block_contents
484 function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
485 // Display for timed lessons and for students only
486 $context = context_module::instance($cmid);
487 if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) {
491 $content = '<div id="lesson-timer">';
492 $content .= $lesson->time_remaining($timer->starttime);
493 $content .= '</div>';
495 $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit);
496 $page->requires->data_for_js('clocksettings', $clocksettings, true);
497 $page->requires->strings_for_js(array('timeisup'), 'lesson');
498 $page->requires->js('/mod/lesson/timer.js');
499 $page->requires->js_init_call('show_clock');
501 $bc = new block_contents();
502 $bc->title = get_string('timeremaining', 'lesson');
503 $bc->attributes['class'] = 'clock block';
504 $bc->content = $content;
510 * If left menu is turned on, then this will
511 * print the menu in a block
513 * @param int $cmid Course Module ID for this lesson
514 * @param lesson $lesson Full lesson record object
517 function lesson_menu_block_contents($cmid, $lesson) {
520 if (!$lesson->displayleft) {
524 $pages = $lesson->load_all_pages();
525 foreach ($pages as $page) {
526 if ((int)$page->prevpageid === 0) {
531 $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
533 if (!$pageid || !$pages) {
537 $content = '<a href="#maincontent" class="accesshide">' .
538 get_string('skip', 'lesson') .
539 "</a>\n<div class=\"menuwrapper\">\n<ul>\n";
541 while ($pageid != 0) {
542 $page = $pages[$pageid];
544 // Only process branch tables with display turned on
545 if ($page->displayinmenublock && $page->display) {
546 if ($page->id == $currentpageid) {
547 $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
549 $content .= "<li class=\"notselected\"><a href=\"$CFG->wwwroot/mod/lesson/view.php?id=$cmid&pageid=$page->id\">".format_string($page->title,true)."</a></li>\n";
553 $pageid = $page->nextpageid;
555 $content .= "</ul>\n</div>\n";
557 $bc = new block_contents();
558 $bc->title = get_string('lessonmenu', 'lesson');
559 $bc->attributes['class'] = 'menu block';
560 $bc->content = $content;
566 * Adds header buttons to the page for the lesson
569 * @param object $context
570 * @param bool $extraeditbuttons
571 * @param int $lessonpageid
573 function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
574 global $CFG, $PAGE, $OUTPUT;
575 if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
576 if ($lessonpageid === null) {
577 print_error('invalidpageid', 'lesson');
579 if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
580 $url = new moodle_url('/mod/lesson/editpage.php', array(
582 'pageid' => $lessonpageid,
584 'returnto' => $PAGE->url->out(false)
586 $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
592 * This is a function used to detect media types and generate html code.
594 * @global object $CFG
595 * @global object $PAGE
596 * @param object $lesson
597 * @param object $context
598 * @return string $code the html code of media
600 function lesson_get_media_html($lesson, $context) {
601 global $CFG, $PAGE, $OUTPUT;
602 require_once("$CFG->libdir/resourcelib.php");
604 // get the media file link
605 if (strpos($lesson->mediafile, '://') !== false) {
606 $url = new moodle_url($lesson->mediafile);
608 // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
609 $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
611 $title = $lesson->mediafile;
613 $clicktoopen = html_writer::link($url, get_string('download'));
615 $mimetype = resourcelib_guess_url_mimetype($url);
617 $extension = resourcelib_get_extension($url->out(false));
619 $mediamanager = core_media_manager::instance($PAGE);
620 $embedoptions = array(
621 core_media_manager::OPTION_TRUSTED => true,
622 core_media_manager::OPTION_BLOCK => true
625 // find the correct type and print it out
626 if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image
627 $code = resourcelib_embed_image($url, $title);
629 } else if ($mediamanager->can_embed_url($url, $embedoptions)) {
630 // Media (audio/video) file.
631 $code = $mediamanager->embed_url($url, $title, 0, 0, $embedoptions);
634 // anything else - just try object tag enlarged as much as possible
635 $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
642 * Logic to happen when a/some group(s) has/have been deleted in a course.
644 * @param int $courseid The course ID.
645 * @param int $groupid The group id if it is known
648 function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
651 $params = array('courseid' => $courseid);
653 $params['groupid'] = $groupid;
654 // We just update the group that was deleted.
655 $sql = "SELECT o.id, o.lessonid
656 FROM {lesson_overrides} o
657 JOIN {lesson} lesson ON lesson.id = o.lessonid
658 WHERE lesson.course = :courseid
659 AND o.groupid = :groupid";
661 // No groupid, we update all orphaned group overrides for all lessons in course.
662 $sql = "SELECT o.id, o.lessonid
663 FROM {lesson_overrides} o
664 JOIN {lesson} lesson ON lesson.id = o.lessonid
665 LEFT JOIN {groups} grp ON grp.id = o.groupid
666 WHERE lesson.course = :courseid
667 AND o.groupid IS NOT NULL
670 $records = $DB->get_records_sql_menu($sql, $params);
672 return; // Nothing to do.
674 $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
678 * Return the overview report table and data.
680 * @param lesson $lesson lesson instance
681 * @param mixed $currentgroup false if not group used, 0 for all groups, group id (int) to filter by that groups
682 * @return mixed false if there is no information otherwise html_table and stdClass with the table and data
685 function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) {
687 require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
689 $context = $lesson->context;
691 // Count the number of branch and question pages in this lesson.
692 $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
693 $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
695 // Only load students if there attempts for this lesson.
696 $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
697 $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
698 $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
699 if ($attempts or $branches or $timer) {
700 list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
701 list($sort, $sortparams) = users_order_by_sql('u');
703 $extrafields = get_extra_user_fields($context);
705 $params['a1lessonid'] = $lesson->id;
706 $params['b1lessonid'] = $lesson->id;
707 $params['c1lessonid'] = $lesson->id;
708 $ufields = user_picture::fields('u', $extrafields);
709 $sql = "SELECT DISTINCT $ufields
712 SELECT userid, lessonid FROM {lesson_attempts} a1
713 WHERE a1.lessonid = :a1lessonid
715 SELECT userid, lessonid FROM {lesson_branch} b1
716 WHERE b1.lessonid = :b1lessonid
718 SELECT userid, lessonid FROM {lesson_timer} c1
719 WHERE c1.lessonid = :c1lessonid
720 ) a ON u.id = a.userid
721 JOIN ($esql) ue ON ue.id = a.userid
724 $students = $DB->get_recordset_sql($sql, $params);
725 if (!$students->valid()) {
727 return array(false, false);
730 return array(false, false);
733 if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
737 if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
741 // Build an array for output.
742 $studentdata = array();
744 $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
745 foreach ($attempts as $attempt) {
746 // 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.
747 if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
748 // restore/setup defaults
755 // search for the grade record for this try. if not there, the nulls defined above will be used.
756 foreach($grades as $grade) {
757 // check to see if the grade matches the correct user
758 if ($grade->userid == $attempt->userid) {
759 // see if n is = to the retry
760 if ($n == $attempt->retry) {
762 $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
765 $n++; // if not equal, then increment n
769 // search for the time record for this try. if not there, the nulls defined above will be used.
770 foreach($times as $time) {
771 // check to see if the grade matches the correct user
772 if ($time->userid == $attempt->userid) {
773 // see if n is = to the retry
774 if ($n == $attempt->retry) {
776 $timeend = $time->lessontime;
777 $timestart = $time->starttime;
778 $eol = $time->completed;
781 $n++; // if not equal, then increment n
785 // build up the array.
786 // this array represents each student and all of their tries at the lesson
787 $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
788 "timeend" => $timeend,
789 "grade" => $usergrade,
791 "try" => $attempt->retry,
792 "userid" => $attempt->userid);
797 $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
798 foreach ($branches as $branch) {
799 // 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.
800 if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
801 // Restore/setup defaults.
807 // Search for the time record for this try. if not there, the nulls defined above will be used.
808 foreach ($times as $time) {
809 // Check to see if the grade matches the correct user.
810 if ($time->userid == $branch->userid) {
811 // See if n is = to the retry.
812 if ($n == $branch->retry) {
814 $timeend = $time->lessontime;
815 $timestart = $time->starttime;
816 $eol = $time->completed;
819 $n++; // If not equal, then increment n.
823 // Build up the array.
824 // This array represents each student and all of their tries at the lesson.
825 $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
826 "timeend" => $timeend,
827 "grade" => $usergrade,
829 "try" => $branch->retry,
830 "userid" => $branch->userid);
835 // Need the same thing for timed entries that were not completed.
836 foreach ($times as $time) {
837 $endoflesson = $time->completed;
838 // If the time start is the same with another record then we shouldn't be adding another item to this array.
839 if (isset($studentdata[$time->userid])) {
842 foreach ($studentdata[$time->userid] as $key => $value) {
843 if ($value['timestart'] == $time->starttime) {
844 // Don't add this to the array.
849 $n = count($studentdata[$time->userid]) + 1;
852 $studentdata[$time->userid][] = array(
853 "timestart" => $time->starttime,
854 "timeend" => $time->lessontime,
856 "end" => $endoflesson,
858 "userid" => $time->userid
862 $studentdata[$time->userid][] = array(
863 "timestart" => $time->starttime,
864 "timeend" => $time->lessontime,
866 "end" => $endoflesson,
868 "userid" => $time->userid
873 // To store all the data to be returned by the function.
874 $data = new stdClass();
876 // Determine if lesson should have a score.
877 if ($branchcount > 0 AND $questioncount == 0) {
878 // This lesson only contains content pages and is not graded.
879 $data->lessonscored = false;
881 // This lesson is graded.
882 $data->lessonscored = true;
884 // set all the stats variables
885 $data->numofattempts = 0;
888 $data->highscore = null;
889 $data->lowscore = null;
890 $data->hightime = null;
891 $data->lowtime = null;
892 $data->students = array();
894 $table = new html_table();
896 $headers = [get_string('name')];
898 foreach ($extrafields as $field) {
899 $headers[] = get_user_field_name($field);
902 $headers [] = get_string('attempts', 'lesson');
904 // Set up the table object.
905 if ($data->lessonscored) {
906 $headers [] = get_string('highscore', 'lesson');
909 $colcount = count($headers);
911 $table->head = $headers;
914 $table->align = array_pad($table->align, $colcount, 'center');
915 $table->align[$colcount - 1] = 'left';
917 if ($data->lessonscored) {
918 $table->align[$colcount - 2] = 'left';
922 $table->wrap = array_pad($table->wrap, $colcount, 'nowrap');
924 $table->attributes['class'] = 'standardtable generaltable';
926 // print out the $studentdata array
927 // going through each student that has attempted the lesson, so, each student should have something to be displayed
928 foreach ($students as $student) {
929 // check to see if the student has attempts to print out
930 if (array_key_exists($student->id, $studentdata)) {
931 // set/reset some variables
933 $dataforstudent = new stdClass;
934 $dataforstudent->attempts = array();
935 // gather the data for each user attempt
937 $bestgradefound = false;
938 // $tries holds all the tries/retries a student has done
939 $tries = $studentdata[$student->id];
940 $studentname = fullname($student, true);
942 foreach ($tries as $try) {
943 $dataforstudent->attempts[] = $try;
945 // Start to build up the checkbox and link.
946 if (has_capability('mod/lesson:edit', $context)) {
947 $temp = '<input type="checkbox" id="attempts" name="attempts['.$try['userid'].']['.$try['try'].']" /> ';
952 $temp .= "<a href=\"report.php?id=$cm->id&action=reportdetail&userid=".$try['userid']
953 .'&try='.$try['try'].'" class="lesson-attempt-link">';
954 if ($try["grade"] !== null) { // if null then not done yet
955 // this is what the link does when the user has completed the try
956 $timetotake = $try["timeend"] - $try["timestart"];
958 $temp .= $try["grade"]."%";
959 $bestgradefound = true;
960 if ($try["grade"] > $bestgrade) {
961 $bestgrade = $try["grade"];
963 $temp .= " ".userdate($try["timestart"]);
964 $temp .= ", (".format_time($timetotake).")</a>";
967 // User finished the lesson but has no grade. (Happens when there are only content pages).
968 $temp .= " ".userdate($try["timestart"]);
969 $timetotake = $try["timeend"] - $try["timestart"];
970 $temp .= ", (".format_time($timetotake).")</a>";
972 // This is what the link does/looks like when the user has not completed the attempt.
973 $temp .= get_string("notcompleted", "lesson");
974 if ($try['timestart'] !== 0) {
975 // Teacher previews do not track time spent.
976 $temp .= " ".userdate($try["timestart"]);
982 // build up the attempts array
985 // Run these lines for the stats only if the user finnished the lesson.
987 // User has completed the lesson.
988 $data->numofattempts++;
989 $data->avetime += $timetotake;
990 if ($timetotake > $data->hightime || $data->hightime == null) {
991 $data->hightime = $timetotake;
993 if ($timetotake < $data->lowtime || $data->lowtime == null) {
994 $data->lowtime = $timetotake;
996 if ($try["grade"] !== null) {
997 // The lesson was scored.
998 $data->avescore += $try["grade"];
999 if ($try["grade"] > $data->highscore || $data->highscore === null) {
1000 $data->highscore = $try["grade"];
1002 if ($try["grade"] < $data->lowscore || $data->lowscore === null) {
1003 $data->lowscore = $try["grade"];
1009 // get line breaks in after each attempt
1010 $attempts = implode("<br />\n", $attempts);
1011 $row = [$studentname];
1013 foreach ($extrafields as $field) {
1014 $row[] = $student->$field;
1019 if ($data->lessonscored) {
1020 // Add the grade if the lesson is graded.
1021 $row[] = $bestgrade."%";
1024 $table->data[] = $row;
1026 // Add the student data.
1027 $dataforstudent->id = $student->id;
1028 $dataforstudent->fullname = $studentname;
1029 $dataforstudent->bestgrade = $bestgrade;
1030 $data->students[] = $dataforstudent;
1034 if ($data->numofattempts > 0) {
1035 $data->avescore = $data->avescore / $data->numofattempts;
1038 return array($table, $data);
1042 * Return information about one user attempt (including answers)
1043 * @param lesson $lesson lesson instance
1044 * @param int $userid the user id
1045 * @param int $attempt the attempt number
1046 * @return array the user answers (array) and user data stats (object)
1049 function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
1052 $context = $lesson->context;
1053 if (!empty($userid)) {
1055 $lesson->update_effective_access($userid);
1059 $lessonpages = $lesson->load_all_pages();
1060 foreach ($lessonpages as $lessonpage) {
1061 if ($lessonpage->prevpageid == 0) {
1062 $pageid = $lessonpage->id;
1066 // now gather the stats into an object
1067 $firstpageid = $pageid;
1068 $pagestats = array();
1069 while ($pageid != 0) { // EOL
1070 $page = $lessonpages[$pageid];
1071 $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
1072 if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
1073 // get them ready for processing
1074 $orderedanswers = array();
1075 foreach ($allanswers as $singleanswer) {
1076 // ordering them like this, will help to find the single attempt record that we want to keep.
1077 $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
1079 // this is foreach user and for each try for that user, keep one attempt record
1080 foreach ($orderedanswers as $orderedanswer) {
1081 foreach($orderedanswer as $tries) {
1082 $page->stats($pagestats, $tries);
1086 // no one answered yet...
1088 //unset($orderedanswers); initialized above now
1089 $pageid = $page->nextpageid;
1092 $manager = lesson_page_type_manager::get($lesson);
1093 $qtypes = $manager->get_page_type_strings();
1095 $answerpages = array();
1097 $pageid = $firstpageid;
1098 // cycle through all the pages
1099 // foreach page, add to the $answerpages[] array all the data that is needed
1100 // from the question, the users attempt, and the statistics
1101 // grayout pages that the user did not answer and Branch, end of branch, cluster
1102 // and end of cluster pages
1103 while ($pageid != 0) { // EOL
1104 $page = $lessonpages[$pageid];
1105 $answerpage = new stdClass;
1106 // Keep the original page object.
1107 $answerpage->page = $page;
1110 $answerdata = new stdClass;
1111 // Set some defaults for the answer data.
1112 $answerdata->score = null;
1113 $answerdata->response = null;
1114 $answerdata->responseformat = FORMAT_PLAIN;
1116 $answerpage->title = format_string($page->title);
1118 $options = new stdClass;
1119 $options->noclean = true;
1120 $options->overflowdiv = true;
1121 $options->context = $context;
1122 $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
1124 $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
1125 $answerpage->grayout = $page->grayout;
1126 $answerpage->context = $context;
1128 if (empty($userid)) {
1129 // there is no userid, so set these vars and display stats.
1130 $answerpage->grayout = 0;
1132 } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
1133 // get the user's answer for this page
1134 // need to find the right one
1136 foreach ($useranswers as $userattempt) {
1137 $useranswer = $userattempt;
1139 if ($lesson->maxattempts == $i) {
1140 break; // reached maxattempts, break out
1144 // user did not answer this page, gray it out and set some nulls
1145 $answerpage->grayout = 1;
1150 $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
1151 $pageid = $page->nextpageid;
1154 $userstats = new stdClass;
1155 if (!empty($userid)) {
1156 $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
1158 $alreadycompleted = true;
1160 if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
1161 $userstats->grade = -1;
1162 $userstats->completed = -1;
1163 $alreadycompleted = false;
1165 $userstats->grade = current($grades);
1166 $userstats->completed = $userstats->grade->completed;
1167 $userstats->grade = round($userstats->grade->grade, 2);
1170 if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
1171 $userstats->timetotake = -1;
1172 $alreadycompleted = false;
1174 $userstats->timetotake = current($times);
1175 $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
1178 if ($alreadycompleted) {
1179 $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
1183 return array($answerpages, $userstats);
1188 * Abstract class that page type's MUST inherit from.
1190 * This is the abstract class that ALL add page type forms must extend.
1191 * You will notice that all but two of the methods this class contains are final.
1192 * Essentially the only thing that extending classes can do is extend custom_definition.
1193 * OR if it has a special requirement on creation it can extend construction_override
1196 * @copyright 2009 Sam Hemelryk
1197 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1199 abstract class lesson_add_page_form_base extends moodleform {
1202 * This is the classic define that is used to identify this pagetype.
1203 * Will be one of LESSON_*
1209 * The simple string that describes the page type e.g. truefalse, multichoice
1212 public $qtypestring;
1215 * An array of options used in the htmleditor
1218 protected $editoroptions = array();
1221 * True if this is a standard page of false if it does something special.
1222 * Questions are standard pages, branch tables are not
1225 protected $standard = true;
1228 * Answer format supported by question type.
1230 protected $answerformat = '';
1233 * Response format supported by question type.
1235 protected $responseformat = '';
1238 * Each page type can and should override this to add any custom elements to
1239 * the basic form that they want
1241 public function custom_definition() {}
1244 * Returns answer format used by question type.
1246 public function get_answer_format() {
1247 return $this->answerformat;
1251 * Returns response format used by question type.
1253 public function get_response_format() {
1254 return $this->responseformat;
1258 * Used to determine if this is a standard page or a special page
1261 public final function is_standard() {
1262 return (bool)$this->standard;
1266 * Add the required basic elements to the form.
1268 * This method adds the basic elements to the form including title and contents
1269 * and then calls custom_definition();
1271 public final function definition() {
1272 $mform = $this->_form;
1273 $editoroptions = $this->_customdata['editoroptions'];
1275 if ($this->qtypestring != 'selectaqtype') {
1276 if ($this->_customdata['edit']) {
1277 $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
1279 $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
1283 if (!empty($this->_customdata['returnto'])) {
1284 $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
1285 $mform->setType('returnto', PARAM_URL);
1288 $mform->addElement('hidden', 'id');
1289 $mform->setType('id', PARAM_INT);
1291 $mform->addElement('hidden', 'pageid');
1292 $mform->setType('pageid', PARAM_INT);
1294 if ($this->standard === true) {
1295 $mform->addElement('hidden', 'qtype');
1296 $mform->setType('qtype', PARAM_INT);
1298 $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
1299 $mform->setType('title', PARAM_TEXT);
1300 $mform->addRule('title', get_string('required'), 'required', null, 'client');
1302 $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
1303 $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
1304 $mform->setType('contents_editor', PARAM_RAW);
1305 $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
1308 $this->custom_definition();
1310 if ($this->_customdata['edit'] === true) {
1311 $mform->addElement('hidden', 'edit', 1);
1312 $mform->setType('edit', PARAM_BOOL);
1313 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
1314 } else if ($this->qtype === 'questiontype') {
1315 $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
1317 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
1322 * Convenience function: Adds a jumpto select element
1324 * @param string $name
1325 * @param string|null $label
1326 * @param int $selected The page to select by default
1328 protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
1329 $title = get_string("jump", "lesson");
1330 if ($label === null) {
1333 if (is_int($name)) {
1334 $name = "jumpto[$name]";
1336 $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
1337 $this->_form->setDefault($name, $selected);
1338 $this->_form->addHelpButton($name, 'jumps', 'lesson');
1342 * Convenience function: Adds a score input element
1344 * @param string $name
1345 * @param string|null $label
1346 * @param mixed $value The default value
1348 protected final function add_score($name, $label=null, $value=null) {
1349 if ($label === null) {
1350 $label = get_string("score", "lesson");
1353 if (is_int($name)) {
1354 $name = "score[$name]";
1356 $this->_form->addElement('text', $name, $label, array('size'=>5));
1357 $this->_form->setType($name, PARAM_INT);
1358 if ($value !== null) {
1359 $this->_form->setDefault($name, $value);
1361 $this->_form->addHelpButton($name, 'score', 'lesson');
1363 // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
1364 if (!$this->_customdata['lesson']->custom) {
1365 $this->_form->freeze($name);
1370 * Convenience function: Adds an answer editor
1372 * @param int $count The count of the element to add
1373 * @param string $label, null means default
1374 * @param bool $required
1375 * @param string $format
1378 protected final function add_answer($count, $label = null, $required = false, $format= '') {
1379 if ($label === null) {
1380 $label = get_string('answer', 'lesson');
1383 if ($format == LESSON_ANSWER_HTML) {
1384 $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
1385 array('rows' => '4', 'columns' => '80'),
1386 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1387 $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
1388 $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
1390 $this->_form->addElement('text', 'answer_editor['.$count.']', $label,
1391 array('size' => '50', 'maxlength' => '200'));
1392 $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
1396 $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
1400 * Convenience function: Adds an response editor
1402 * @param int $count The count of the element to add
1403 * @param string $label, null means default
1404 * @param bool $required
1407 protected final function add_response($count, $label = null, $required = false) {
1408 if ($label === null) {
1409 $label = get_string('response', 'lesson');
1411 $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
1412 array('rows' => '4', 'columns' => '80'),
1413 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1414 $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
1415 $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
1418 $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
1423 * A function that gets called upon init of this object by the calling script.
1425 * This can be used to process an immediate action if required. Currently it
1426 * is only used in special cases by non-standard page types.
1430 public function construction_override($pageid, lesson $lesson) {
1438 * Class representation of a lesson
1440 * This class is used the interact with, and manage a lesson once instantiated.
1441 * If you need to fetch a lesson object you can do so by calling
1444 * lesson::load($lessonid);
1446 * $lessonrecord = $DB->get_record('lesson', $lessonid);
1447 * $lesson = new lesson($lessonrecord);
1450 * The class itself extends lesson_base as all classes within the lesson module should
1452 * These properties are from the database
1453 * @property int $id The id of this lesson
1454 * @property int $course The ID of the course this lesson belongs to
1455 * @property string $name The name of this lesson
1456 * @property int $practice Flag to toggle this as a practice lesson
1457 * @property int $modattempts Toggle to allow the user to go back and review answers
1458 * @property int $usepassword Toggle the use of a password for entry
1459 * @property string $password The password to require users to enter
1460 * @property int $dependency ID of another lesson this lesson is dependent on
1461 * @property string $conditions Conditions of the lesson dependency
1462 * @property int $grade The maximum grade a user can achieve (%)
1463 * @property int $custom Toggle custom scoring on or off
1464 * @property int $ongoing Toggle display of an ongoing score
1465 * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
1466 * @property int $maxanswers The max number of answers or branches
1467 * @property int $maxattempts The maximum number of attempts a user can record
1468 * @property int $review Toggle use or wrong answer review button
1469 * @property int $nextpagedefault Override the default next page
1470 * @property int $feedback Toggles display of default feedback
1471 * @property int $minquestions Sets a minimum value of pages seen when calculating grades
1472 * @property int $maxpages Maximum number of pages this lesson can contain
1473 * @property int $retake Flag to allow users to retake a lesson
1474 * @property int $activitylink Relate this lesson to another lesson
1475 * @property string $mediafile File to pop up to or webpage to display
1476 * @property int $mediaheight Sets the height of the media file popup
1477 * @property int $mediawidth Sets the width of the media file popup
1478 * @property int $mediaclose Toggle display of a media close button
1479 * @property int $slideshow Flag for whether branch pages should be shown as slideshows
1480 * @property int $width Width of slideshow
1481 * @property int $height Height of slideshow
1482 * @property string $bgcolor Background colour of slideshow
1483 * @property int $displayleft Display a left menu
1484 * @property int $displayleftif Sets the condition on which the left menu is displayed
1485 * @property int $progressbar Flag to toggle display of a lesson progress bar
1486 * @property int $available Timestamp of when this lesson becomes available
1487 * @property int $deadline Timestamp of when this lesson is no longer available
1488 * @property int $timemodified Timestamp when lesson was last modified
1489 * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app
1491 * These properties are calculated
1492 * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
1493 * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
1495 * @copyright 2009 Sam Hemelryk
1496 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1498 class lesson extends lesson_base {
1501 * The id of the first page (where prevpageid = 0) gets set and retrieved by
1502 * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
1505 protected $firstpageid = null;
1507 * The id of the last page (where nextpageid = 0) gets set and retrieved by
1508 * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
1511 protected $lastpageid = null;
1513 * An array used to cache the pages associated with this lesson after the first
1514 * time they have been loaded.
1515 * A note to developers: If you are going to be working with MORE than one or
1516 * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1517 * in order to save excess database queries.
1518 * @var array An array of lesson_page objects
1520 protected $pages = array();
1522 * Flag that gets set to true once all of the pages associated with the lesson
1526 protected $loadedallpages = false;
1529 * Course module object gets set and retrieved by directly calling <code>$lesson->cm;</code>
1533 protected $cm = null;
1536 * Course object gets set and retrieved by directly calling <code>$lesson->courserecord;</code>
1537 * @see get_courserecord()
1540 protected $courserecord = null;
1543 * Context object gets set and retrieved by directly calling <code>$lesson->context;</code>
1544 * @see get_context()
1547 protected $context = null;
1550 * Constructor method
1552 * @param object $properties
1553 * @param stdClass $cm course module object
1554 * @param stdClass $course course object
1557 public function __construct($properties, $cm = null, $course = null) {
1558 parent::__construct($properties);
1560 $this->courserecord = $course;
1564 * Simply generates a lesson object given an array/object of properties
1565 * Overrides {@see lesson_base->create()}
1567 * @param object|array $properties
1570 public static function create($properties) {
1571 return new lesson($properties);
1575 * Generates a lesson object from the database given its id
1577 * @param int $lessonid
1580 public static function load($lessonid) {
1583 if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1584 print_error('invalidcoursemodule');
1586 return new lesson($lesson);
1590 * Deletes this lesson from the database
1592 public function delete() {
1594 require_once($CFG->libdir.'/gradelib.php');
1595 require_once($CFG->dirroot.'/calendar/lib.php');
1597 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1598 $context = context_module::instance($cm->id);
1600 $this->delete_all_overrides();
1602 $DB->delete_records("lesson", array("id"=>$this->properties->id));
1603 $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1604 $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1605 $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1606 $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1607 $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1608 $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1609 if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
1610 $coursecontext = context_course::instance($cm->course);
1611 foreach($events as $event) {
1612 $event->context = $coursecontext;
1613 $event = calendar_event::load($event);
1618 // Delete files associated with this module.
1619 $fs = get_file_storage();
1620 $fs->delete_area_files($context->id);
1622 grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
1627 * Deletes a lesson override from the database and clears any corresponding calendar events
1629 * @param int $overrideid The id of the override being deleted
1630 * @return bool true on success
1632 public function delete_override($overrideid) {
1635 require_once($CFG->dirroot . '/calendar/lib.php');
1637 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1639 $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST);
1641 // Delete the events.
1642 $conds = array('modulename' => 'lesson',
1643 'instance' => $this->properties->id);
1644 if (isset($override->userid)) {
1645 $conds['userid'] = $override->userid;
1647 $conds['groupid'] = $override->groupid;
1649 $events = $DB->get_records('event', $conds);
1650 foreach ($events as $event) {
1651 $eventold = calendar_event::load($event);
1652 $eventold->delete();
1655 $DB->delete_records('lesson_overrides', array('id' => $overrideid));
1657 // Set the common parameters for one of the events we will be triggering.
1659 'objectid' => $override->id,
1660 'context' => context_module::instance($cm->id),
1662 'lessonid' => $override->lessonid
1665 // Determine which override deleted event to fire.
1666 if (!empty($override->userid)) {
1667 $params['relateduserid'] = $override->userid;
1668 $event = \mod_lesson\event\user_override_deleted::create($params);
1670 $params['other']['groupid'] = $override->groupid;
1671 $event = \mod_lesson\event\group_override_deleted::create($params);
1674 // Trigger the override deleted event.
1675 $event->add_record_snapshot('lesson_overrides', $override);
1682 * Deletes all lesson overrides from the database and clears any corresponding calendar events
1684 public function delete_all_overrides() {
1687 $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id');
1688 foreach ($overrides as $override) {
1689 $this->delete_override($override->id);
1694 * Updates the lesson properties with override information for a user.
1696 * Algorithm: For each lesson setting, if there is a matching user-specific override,
1697 * then use that otherwise, if there are group-specific overrides, return the most
1698 * lenient combination of them. If neither applies, leave the quiz setting unchanged.
1700 * Special case: if there is more than one password that applies to the user, then
1701 * lesson->extrapasswords will contain an array of strings giving the remaining
1704 * @param int $userid The userid.
1706 public function update_effective_access($userid) {
1709 // Check for user override.
1710 $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid));
1713 $override = new stdClass();
1714 $override->available = null;
1715 $override->deadline = null;
1716 $override->timelimit = null;
1717 $override->review = null;
1718 $override->maxattempts = null;
1719 $override->retake = null;
1720 $override->password = null;
1723 // Check for group overrides.
1724 $groupings = groups_get_user_groups($this->properties->course, $userid);
1726 if (!empty($groupings[0])) {
1727 // Select all overrides that apply to the User's groups.
1728 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1729 $sql = "SELECT * FROM {lesson_overrides}
1730 WHERE groupid $extra AND lessonid = ?";
1731 $params[] = $this->properties->id;
1732 $records = $DB->get_records_sql($sql, $params);
1734 // Combine the overrides.
1735 $availables = array();
1736 $deadlines = array();
1737 $timelimits = array();
1739 $attempts = array();
1741 $passwords = array();
1743 foreach ($records as $gpoverride) {
1744 if (isset($gpoverride->available)) {
1745 $availables[] = $gpoverride->available;
1747 if (isset($gpoverride->deadline)) {
1748 $deadlines[] = $gpoverride->deadline;
1750 if (isset($gpoverride->timelimit)) {
1751 $timelimits[] = $gpoverride->timelimit;
1753 if (isset($gpoverride->review)) {
1754 $reviews[] = $gpoverride->review;
1756 if (isset($gpoverride->maxattempts)) {
1757 $attempts[] = $gpoverride->maxattempts;
1759 if (isset($gpoverride->retake)) {
1760 $retakes[] = $gpoverride->retake;
1762 if (isset($gpoverride->password)) {
1763 $passwords[] = $gpoverride->password;
1766 // If there is a user override for a setting, ignore the group override.
1767 if (is_null($override->available) && count($availables)) {
1768 $override->available = min($availables);
1770 if (is_null($override->deadline) && count($deadlines)) {
1771 if (in_array(0, $deadlines)) {
1772 $override->deadline = 0;
1774 $override->deadline = max($deadlines);
1777 if (is_null($override->timelimit) && count($timelimits)) {
1778 if (in_array(0, $timelimits)) {
1779 $override->timelimit = 0;
1781 $override->timelimit = max($timelimits);
1784 if (is_null($override->review) && count($reviews)) {
1785 $override->review = max($reviews);
1787 if (is_null($override->maxattempts) && count($attempts)) {
1788 $override->maxattempts = max($attempts);
1790 if (is_null($override->retake) && count($retakes)) {
1791 $override->retake = max($retakes);
1793 if (is_null($override->password) && count($passwords)) {
1794 $override->password = array_shift($passwords);
1795 if (count($passwords)) {
1796 $override->extrapasswords = $passwords;
1802 // Merge with lesson defaults.
1803 $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake');
1804 foreach ($keys as $key) {
1805 if (isset($override->{$key})) {
1806 $this->properties->{$key} = $override->{$key};
1810 // Special handling of lesson usepassword and password.
1811 if (isset($override->password)) {
1812 if ($override->password == '') {
1813 $this->properties->usepassword = 0;
1815 $this->properties->usepassword = 1;
1816 $this->properties->password = $override->password;
1817 if (isset($override->extrapasswords)) {
1818 $this->properties->extrapasswords = $override->extrapasswords;
1825 * Fetches messages from the session that may have been set in previous page
1829 * // Do not call this method directly instead use
1830 * $lesson->messages;
1835 protected function get_messages() {
1838 $messages = array();
1839 if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1840 $messages = $SESSION->lesson_messages[$this->properties->id];
1841 unset($SESSION->lesson_messages[$this->properties->id]);
1848 * Get all of the attempts for the current user.
1850 * @param int $retries
1851 * @param bool $correct Optional: only fetch correct attempts
1852 * @param int $pageid Optional: only fetch attempts at the given page
1853 * @param int $userid Optional: defaults to the current user if not set
1854 * @return array|false
1856 public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1858 $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1860 $params['correct'] = 1;
1862 if ($pageid !== null) {
1863 $params['pageid'] = $pageid;
1865 if ($userid === null) {
1866 $params['userid'] = $USER->id;
1868 return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
1873 * Get a list of content pages (formerly known as branch tables) viewed in the lesson for the given user during an attempt.
1875 * @param int $lessonattempt the lesson attempt number (also known as retries)
1876 * @param int $userid the user id to retrieve the data from
1877 * @param string $sort an order to sort the results in (a valid SQL ORDER BY parameter)
1878 * @param string $fields a comma separated list of fields to return
1879 * @return array of pages
1882 public function get_content_pages_viewed($lessonattempt, $userid = null, $sort = '', $fields = '*') {
1885 if ($userid === null) {
1886 $userid = $USER->id;
1888 $conditions = array("lessonid" => $this->properties->id, "userid" => $userid, "retry" => $lessonattempt);
1889 return $DB->get_records('lesson_branch', $conditions, $sort, $fields);
1893 * Returns the first page for the lesson or false if there isn't one.
1895 * This method should be called via the magic method __get();
1897 * $firstpage = $lesson->firstpage;
1900 * @return lesson_page|bool Returns the lesson_page specialised object or false
1902 protected function get_firstpage() {
1903 $pages = $this->load_all_pages();
1904 if (count($pages) > 0) {
1905 foreach ($pages as $page) {
1906 if ((int)$page->prevpageid === 0) {
1915 * Returns the last page for the lesson or false if there isn't one.
1917 * This method should be called via the magic method __get();
1919 * $lastpage = $lesson->lastpage;
1922 * @return lesson_page|bool Returns the lesson_page specialised object or false
1924 protected function get_lastpage() {
1925 $pages = $this->load_all_pages();
1926 if (count($pages) > 0) {
1927 foreach ($pages as $page) {
1928 if ((int)$page->nextpageid === 0) {
1937 * Returns the id of the first page of this lesson. (prevpageid = 0)
1940 protected function get_firstpageid() {
1942 if ($this->firstpageid == null) {
1943 if (!$this->loadedallpages) {
1944 $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
1945 if (!$firstpageid) {
1946 print_error('cannotfindfirstpage', 'lesson');
1948 $this->firstpageid = $firstpageid;
1950 $firstpage = $this->get_firstpage();
1951 $this->firstpageid = $firstpage->id;
1954 return $this->firstpageid;
1958 * Returns the id of the last page of this lesson. (nextpageid = 0)
1961 public function get_lastpageid() {
1963 if ($this->lastpageid == null) {
1964 if (!$this->loadedallpages) {
1965 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1967 print_error('cannotfindlastpage', 'lesson');
1969 $this->lastpageid = $lastpageid;
1971 $lastpageid = $this->get_lastpage();
1972 $this->lastpageid = $lastpageid->id;
1976 return $this->lastpageid;
1980 * Gets the next page id to display after the one that is provided.
1981 * @param int $nextpageid
1984 public function get_next_page($nextpageid) {
1986 $allpages = $this->load_all_pages();
1987 if ($this->properties->nextpagedefault) {
1988 // in Flash Card mode...first get number of retakes
1989 $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
1992 if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
1993 foreach ($allpages as $nextpage) {
1994 if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
1999 } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
2000 foreach ($allpages as $nextpage) {
2001 if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
2008 if ($this->properties->maxpages) {
2009 // check number of pages viewed (in the lesson)
2010 if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
2014 return $nextpage->id;
2017 // In a normal lesson mode
2018 foreach ($allpages as $nextpage) {
2019 if ((int)$nextpage->id === (int)$nextpageid) {
2020 return $nextpage->id;
2027 * Sets a message against the session for this lesson that will displayed next
2028 * time the lesson processes messages
2030 * @param string $message
2031 * @param string $class
2032 * @param string $align
2035 public function add_message($message, $class="notifyproblem", $align='center') {
2038 if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
2039 $SESSION->lesson_messages = array();
2040 $SESSION->lesson_messages[$this->properties->id] = array();
2041 } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
2042 $SESSION->lesson_messages[$this->properties->id] = array();
2045 $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
2051 * Check if the lesson is accessible at the present time
2052 * @return bool True if the lesson is accessible, false otherwise
2054 public function is_accessible() {
2055 $available = $this->properties->available;
2056 $deadline = $this->properties->deadline;
2057 return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
2061 * Starts the lesson time for the current user
2062 * @return bool Returns true
2064 public function start_timer() {
2067 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2070 // Trigger lesson started event.
2071 $event = \mod_lesson\event\lesson_started::create(array(
2072 'objectid' => $this->properties()->id,
2073 'context' => context_module::instance($cm->id),
2074 'courseid' => $this->properties()->course
2078 $USER->startlesson[$this->properties->id] = true;
2081 $startlesson = new stdClass;
2082 $startlesson->lessonid = $this->properties->id;
2083 $startlesson->userid = $USER->id;
2084 $startlesson->starttime = $timenow;
2085 $startlesson->lessontime = $timenow;
2087 $startlesson->timemodifiedoffline = $timenow;
2089 $DB->insert_record('lesson_timer', $startlesson);
2090 if ($this->properties->timelimit) {
2091 $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
2097 * Updates the timer to the current time and returns the new timer object
2098 * @param bool $restart If set to true the timer is restarted
2099 * @param bool $continue If set to true AND $restart=true then the timer
2100 * will continue from a previous attempt
2101 * @return stdClass The new timer
2103 public function update_timer($restart=false, $continue=false, $endreached =false) {
2106 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2109 // get time information for this user
2110 if (!$timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
2111 $this->start_timer();
2112 $timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1);
2114 $timer = current($timer); // This will get the latest start time record.
2118 // continue a previous test, need to update the clock (think this option is disabled atm)
2119 $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
2121 // Trigger lesson resumed event.
2122 $event = \mod_lesson\event\lesson_resumed::create(array(
2123 'objectid' => $this->properties->id,
2124 'context' => context_module::instance($cm->id),
2125 'courseid' => $this->properties->course
2130 // starting over, so reset the clock
2131 $timer->starttime = time();
2133 // Trigger lesson restarted event.
2134 $event = \mod_lesson\event\lesson_restarted::create(array(
2135 'objectid' => $this->properties->id,
2136 'context' => context_module::instance($cm->id),
2137 'courseid' => $this->properties->course
2145 $timer->lessontime = $timenow;
2147 $timer->timemodifiedoffline = $timenow;
2149 $timer->completed = $endreached;
2150 $DB->update_record('lesson_timer', $timer);
2152 // Update completion state.
2153 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2155 $course = get_course($cm->course);
2156 $completion = new completion_info($course);
2157 if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) {
2158 $completion->update_state($cm, COMPLETION_COMPLETE);
2164 * Updates the timer to the current time then stops it by unsetting the user var
2165 * @return bool Returns true
2167 public function stop_timer() {
2169 unset($USER->startlesson[$this->properties->id]);
2171 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2174 // Trigger lesson ended event.
2175 $event = \mod_lesson\event\lesson_ended::create(array(
2176 'objectid' => $this->properties()->id,
2177 'context' => context_module::instance($cm->id),
2178 'courseid' => $this->properties()->course
2182 return $this->update_timer(false, false, true);
2186 * Checks to see if the lesson has pages
2188 public function has_pages() {
2190 $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
2191 return ($pagecount>0);
2195 * Returns the link for the related activity
2198 public function link_for_activitylink() {
2200 $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
2202 $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
2204 $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
2205 if ($instancename) {
2206 return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php',
2207 array('id' => $this->properties->activitylink)), get_string('activitylinkname',
2208 'lesson', $instancename), array('class' => 'centerpadded lessonbutton standardbutton p-r-1'));
2216 * Loads the requested page.
2218 * This function will return the requested page id as either a specialised
2219 * lesson_page object OR as a generic lesson_page.
2220 * If the page has been loaded previously it will be returned from the pages
2221 * array, otherwise it will be loaded from the database first
2223 * @param int $pageid
2224 * @return lesson_page A lesson_page object or an object that extends it
2226 public function load_page($pageid) {
2227 if (!array_key_exists($pageid, $this->pages)) {
2228 $manager = lesson_page_type_manager::get($this);
2229 $this->pages[$pageid] = $manager->load_page($pageid, $this);
2231 return $this->pages[$pageid];
2235 * Loads ALL of the pages for this lesson
2237 * @return array An array containing all pages from this lesson
2239 public function load_all_pages() {
2240 if (!$this->loadedallpages) {
2241 $manager = lesson_page_type_manager::get($this);
2242 $this->pages = $manager->load_all_pages($this);
2243 $this->loadedallpages = true;
2245 return $this->pages;
2249 * Duplicate the lesson page.
2251 * @param int $pageid Page ID of the page to duplicate.
2254 public function duplicate_page($pageid) {
2256 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2257 $context = context_module::instance($cm->id);
2259 $page = $this->load_page($pageid);
2260 $properties = $page->properties();
2261 // The create method checks to see if these properties are set and if not sets them to zero, hence the unsetting here.
2262 if (!$properties->qoption) {
2263 unset($properties->qoption);
2265 if (!$properties->layout) {
2266 unset($properties->layout);
2268 if (!$properties->display) {
2269 unset($properties->display);
2272 $properties->pageid = $pageid;
2273 // Add text and format into the format required to create a new page.
2274 $properties->contents_editor = array(
2275 'text' => $properties->contents,
2276 'format' => $properties->contentsformat
2278 $answers = $page->get_answers();
2279 // Answers need to be added to $properties.
2281 $answerids = array();
2282 foreach ($answers as $answer) {
2283 // Needs to be rearranged to work with the create function.
2284 $properties->answer_editor[$i] = array(
2285 'text' => $answer->answer,
2286 'format' => $answer->answerformat
2289 $properties->response_editor[$i] = array(
2290 'text' => $answer->response,
2291 'format' => $answer->responseformat
2293 $answerids[] = $answer->id;
2295 $properties->jumpto[$i] = $answer->jumpto;
2296 $properties->score[$i] = $answer->score;
2300 // Create the duplicate page.
2301 $newlessonpage = lesson_page::create($properties, $this, $context, $PAGE->course->maxbytes);
2302 $newanswers = $newlessonpage->get_answers();
2303 // Copy over the file areas as well.
2304 $this->copy_page_files('page_contents', $pageid, $newlessonpage->id, $context->id);
2306 foreach ($newanswers as $answer) {
2307 if (isset($answer->answer) && strpos($answer->answer, '@@PLUGINFILE@@') !== false) {
2308 $this->copy_page_files('page_answers', $answerids[$j], $answer->id, $context->id);
2310 if (isset($answer->response) && !is_array($answer->response) && strpos($answer->response, '@@PLUGINFILE@@') !== false) {
2311 $this->copy_page_files('page_responses', $answerids[$j], $answer->id, $context->id);
2318 * Copy the files from one page to another.
2320 * @param string $filearea Area that the files are stored.
2321 * @param int $itemid Item ID.
2322 * @param int $newitemid The item ID for the new page.
2323 * @param int $contextid Context ID for this page.
2326 protected function copy_page_files($filearea, $itemid, $newitemid, $contextid) {
2327 $fs = get_file_storage();
2328 $files = $fs->get_area_files($contextid, 'mod_lesson', $filearea, $itemid);
2329 foreach ($files as $file) {
2330 $fieldupdates = array('itemid' => $newitemid);
2331 $fs->create_file_from_storedfile($fieldupdates, $file);
2336 * Determines if a jumpto value is correct or not.
2338 * returns true if jumpto page is (logically) after the pageid page or
2339 * if the jumpto value is a special value. Returns false in all other cases.
2341 * @param int $pageid Id of the page from which you are jumping from.
2342 * @param int $jumpto The jumpto number.
2343 * @return boolean True or false after a series of tests.
2345 public function jumpto_is_correct($pageid, $jumpto) {
2348 // first test the special values
2352 } elseif ($jumpto == LESSON_NEXTPAGE) {
2354 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2356 } elseif ($jumpto == LESSON_RANDOMPAGE) {
2358 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2360 } elseif ($jumpto == LESSON_EOL) {
2364 $pages = $this->load_all_pages();
2365 $apageid = $pages[$pageid]->nextpageid;
2366 while ($apageid != 0) {
2367 if ($jumpto == $apageid) {
2370 $apageid = $pages[$apageid]->nextpageid;
2376 * Returns the time a user has remaining on this lesson
2377 * @param int $starttime Starttime timestamp
2380 public function time_remaining($starttime) {
2381 $timeleft = $starttime + $this->properties->timelimit - time();
2382 $hours = floor($timeleft/3600);
2383 $timeleft = $timeleft - ($hours * 3600);
2384 $minutes = floor($timeleft/60);
2385 $secs = $timeleft - ($minutes * 60);
2387 if ($minutes < 10) {
2388 $minutes = "0$minutes";
2395 $output[] = $minutes;
2397 $output = implode(':', $output);
2402 * Interprets LESSON_CLUSTERJUMP jumpto value.
2404 * This will select a page randomly
2405 * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
2406 * and the page selected will be a page that has not been viewed already
2407 * and if any pages are within a branch table or end of branch then only 1 page within
2408 * the branch table or end of branch will be randomly selected (sub clustering).
2410 * @param int $pageid Id of the current page from which we are jumping from.
2411 * @param int $userid Id of the user.
2412 * @return int The id of the next page.
2414 public function cluster_jump($pageid, $userid=null) {
2417 if ($userid===null) {
2418 $userid = $USER->id;
2420 // get the number of retakes
2421 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
2424 // get all the lesson_attempts aka what the user has seen
2425 $seenpages = array();
2426 if ($attempts = $this->get_attempts($retakes)) {
2427 foreach ($attempts as $attempt) {
2428 $seenpages[$attempt->pageid] = $attempt->pageid;
2433 // get the lesson pages
2434 $lessonpages = $this->load_all_pages();
2435 // find the start of the cluster
2436 while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
2437 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
2440 $pageid = $lessonpages[$pageid]->prevpageid;
2443 $clusterpages = array();
2444 $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
2446 foreach ($clusterpages as $key=>$cluster) {
2447 // Remove the page if it is in a branch table or is an endofbranch.
2448 if ($this->is_sub_page_of_type($cluster->id,
2449 array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))
2450 || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) {
2451 unset($clusterpages[$key]);
2452 } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) {
2453 // If branchtable, check to see if any pages inside have been viewed.
2454 $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2456 foreach ($branchpages as $branchpage) {
2457 if (array_key_exists($branchpage->id, $seenpages)) { // Check if any of the pages have been viewed.
2461 if ($flag && count($branchpages) > 0) {
2462 // Add branch table.
2463 $unseen[] = $cluster;
2465 } elseif ($cluster->is_unseen($seenpages)) {
2466 $unseen[] = $cluster;
2470 if (count($unseen) > 0) {
2471 // it does not contain elements, then use exitjump, otherwise find out next page/branch
2472 $nextpage = $unseen[rand(0, count($unseen)-1)];
2473 if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
2474 // if branch table, then pick a random page inside of it
2475 $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2476 return $branchpages[rand(0, count($branchpages)-1)]->id;
2477 } else { // otherwise, return the page's id
2478 return $nextpage->id;
2481 // seen all there is to see, leave the cluster
2482 if (end($clusterpages)->nextpageid == 0) {
2485 $clusterendid = $pageid;
2486 while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page.
2487 if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) {
2490 $clusterendid = $lessonpages[$clusterendid]->nextpageid;
2492 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
2493 if ($exitjump == LESSON_NEXTPAGE) {
2494 $exitjump = $lessonpages[$clusterendid]->nextpageid;
2496 if ($exitjump == 0) {
2498 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
2501 if (!array_key_exists($exitjump, $lessonpages)) {
2503 foreach ($lessonpages as $page) {
2504 if ($page->id === $clusterendid) {
2506 } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
2507 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
2508 if ($exitjump == LESSON_NEXTPAGE) {
2509 $exitjump = $lessonpages[$page->id]->nextpageid;
2515 if (!array_key_exists($exitjump, $lessonpages)) {
2518 // Check to see that the return type is not a cluster.
2519 if ($lessonpages[$exitjump]->qtype == LESSON_PAGE_CLUSTER) {
2520 // If the exitjump is a cluster then go through this function again and try to find an unseen question.
2521 $exitjump = $this->cluster_jump($exitjump, $userid);
2530 * Finds all pages that appear to be a subtype of the provided pageid until
2531 * an end point specified within $ends is encountered or no more pages exist
2533 * @param int $pageid
2534 * @param array $ends An array of LESSON_PAGE_* types that signify an end of
2536 * @return array An array of specialised lesson_page objects
2538 public function get_sub_pages_of($pageid, array $ends) {
2539 $lessonpages = $this->load_all_pages();
2540 $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table
2544 if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
2547 $pages[] = $lessonpages[$pageid];
2548 $pageid = $lessonpages[$pageid]->nextpageid;
2555 * Checks to see if the specified page[id] is a subpage of a type specified in
2556 * the $types array, until either there are no more pages of we find a type
2557 * corresponding to that of a type specified in $ends
2559 * @param int $pageid The id of the page to check
2560 * @param array $types An array of types that would signify this page was a subpage
2561 * @param array $ends An array of types that mean this is not a subpage
2564 public function is_sub_page_of_type($pageid, array $types, array $ends) {
2565 $pages = $this->load_all_pages();
2566 $pageid = $pages[$pageid]->prevpageid; // move up one
2568 array_unshift($ends, 0);
2569 // go up the pages till branch table
2571 if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
2573 } else if (in_array($pages[$pageid]->qtype, $types)) {
2576 $pageid = $pages[$pageid]->prevpageid;
2581 * Move a page resorting all other pages.
2583 * @param int $pageid
2587 public function resort_pages($pageid, $after) {
2590 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2591 $context = context_module::instance($cm->id);
2593 $pages = $this->load_all_pages();
2595 if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) {
2596 print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id");
2599 $pagetomove = clone($pages[$pageid]);
2600 unset($pages[$pageid]);
2604 $pageids['p0'] = $pageid;
2606 foreach ($pages as $page) {
2607 $pageids[] = $page->id;
2608 if ($page->id == $after) {
2609 $pageids[] = $pageid;
2613 $pageidsref = $pageids;
2616 $next = next($pageidsref);
2617 foreach ($pageids as $pid) {
2618 if ($pid === $pageid) {
2619 $page = $pagetomove;
2621 $page = $pages[$pid];
2623 if ($page->prevpageid != $prev || $page->nextpageid != $next) {
2624 $page->move($next, $prev);
2626 if ($pid === $pageid) {
2627 // We will trigger an event.
2628 $pageupdated = array('next' => $next, 'prev' => $prev);
2633 $next = next($pageidsref);
2639 // Trigger an event: page moved.
2640 if (!empty($pageupdated)) {
2641 $eventparams = array(
2642 'context' => $context,
2643 'objectid' => $pageid,
2645 'pagetype' => $page->get_typestring(),
2646 'prevpageid' => $pageupdated['prev'],
2647 'nextpageid' => $pageupdated['next']
2650 $event = \mod_lesson\event\page_moved::create($eventparams);
2657 * Return the lesson context object.
2659 * @return stdClass context
2662 public function get_context() {
2663 if ($this->context == null) {
2664 $this->context = context_module::instance($this->get_cm()->id);
2666 return $this->context;
2670 * Set the lesson course module object.
2672 * @param stdClass $cm course module objct
2675 private function set_cm($cm) {
2680 * Return the lesson course module object.
2682 * @return stdClass course module
2685 public function get_cm() {
2686 if ($this->cm == null) {
2687 $this->cm = get_coursemodule_from_instance('lesson', $this->properties->id);
2693 * Set the lesson course object.
2695 * @param stdClass $course course objct
2698 private function set_courserecord($course) {
2699 $this->courserecord = $course;
2703 * Return the lesson course object.
2705 * @return stdClass course
2708 public function get_courserecord() {
2711 if ($this->courserecord == null) {
2712 $this->courserecord = $DB->get_record('course', array('id' => $this->properties->course));
2714 return $this->courserecord;
2718 * Check if the user can manage the lesson activity.
2720 * @return bool true if the user can manage the lesson
2723 public function can_manage() {
2724 return has_capability('mod/lesson:manage', $this->get_context());
2728 * Check if time restriction is applied.
2730 * @return mixed false if there aren't restrictions or an object with the restriction information
2733 public function get_time_restriction_status() {
2734 if ($this->can_manage()) {
2738 if (!$this->is_accessible()) {
2739 if ($this->properties->deadline != 0 && time() > $this->properties->deadline) {
2740 $status = ['reason' => 'lessonclosed', 'time' => $this->properties->deadline];
2742 $status = ['reason' => 'lessonopen', 'time' => $this->properties->available];
2744 return (object) $status;
2750 * Check if password restriction is applied.
2752 * @param string $userpassword the user password to check (if the restriction is set)
2753 * @return mixed false if there aren't restrictions or an object with the restriction information
2756 public function get_password_restriction_status($userpassword) {
2758 if ($this->can_manage()) {
2762 if ($this->properties->usepassword && empty($USER->lessonloggedin[$this->id])) {
2763 $correctpass = false;
2764 if (!empty($userpassword) &&
2765 (($this->properties->password == md5(trim($userpassword))) || ($this->properties->password == trim($userpassword)))) {
2766 // With or without md5 for backward compatibility (MDL-11090).
2767 $correctpass = true;
2768 $USER->lessonloggedin[$this->id] = true;
2769 } else if (isset($this->properties->extrapasswords)) {
2770 // Group overrides may have additional passwords.
2771 foreach ($this->properties->extrapasswords as $password) {
2772 if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) {
2773 $correctpass = true;
2774 $USER->lessonloggedin[$this->id] = true;
2778 return !$correctpass;
2784 * Check if dependencies restrictions are applied.
2786 * @return mixed false if there aren't restrictions or an object with the restriction information
2789 public function get_dependencies_restriction_status() {
2791 if ($this->can_manage()) {
2795 if ($dependentlesson = $DB->get_record('lesson', array('id' => $this->properties->dependency))) {
2796 // Lesson exists, so we can proceed.
2797 $conditions = unserialize($this->properties->conditions);
2798 // Assume false for all.
2800 // Check for the timespent condition.
2801 if ($conditions->timespent) {
2803 if ($attempttimes = $DB->get_records('lesson_timer', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2804 // Go through all the times and test to see if any of them satisfy the condition.
2805 foreach ($attempttimes as $attempttime) {
2806 $duration = $attempttime->lessontime - $attempttime->starttime;
2807 if ($conditions->timespent < $duration / 60) {
2813 $errors[] = get_string('timespenterror', 'lesson', $conditions->timespent);
2816 // Check for the gradebetterthan condition.
2817 if ($conditions->gradebetterthan) {
2818 $gradebetterthan = false;
2819 if ($studentgrades = $DB->get_records('lesson_grades', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2820 // Go through all the grades and test to see if any of them satisfy the condition.
2821 foreach ($studentgrades as $studentgrade) {
2822 if ($studentgrade->grade >= $conditions->gradebetterthan) {
2823 $gradebetterthan = true;
2827 if (!$gradebetterthan) {
2828 $errors[] = get_string('gradebetterthanerror', 'lesson', $conditions->gradebetterthan);
2831 // Check for the completed condition.
2832 if ($conditions->completed) {
2833 if (!$DB->count_records('lesson_grades', array('userid' => $USER->id, 'lessonid' => $dependentlesson->id))) {
2834 $errors[] = get_string('completederror', 'lesson');
2837 if (!empty($errors)) {
2838 return (object) ['errors' => $errors, 'dependentlesson' => $dependentlesson];
2845 * Check if the lesson is in review mode. (The user already finished it and retakes are not allowed).
2847 * @return bool true if is in review mode
2850 public function is_in_review_mode() {
2853 $userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
2854 if ($userhasgrade && !$this->properties->retake) {
2861 * Return the last page the current user saw.
2863 * @param int $retriescount the number of retries for the lesson (the last retry number).
2864 * @return mixed false if the user didn't see the lesson or the last page id
2866 public function get_last_page_seen($retriescount) {
2869 $lastpageseen = false;
2870 $allattempts = $this->get_attempts($retriescount);
2871 if (!empty($allattempts)) {
2872 $attempt = end($allattempts);
2873 $attemptpage = $this->load_page($attempt->pageid);
2874 $jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
2875 // Convert the jumpto to a proper page id.
2877 // Check if a question has been incorrectly answered AND no more attempts at it are left.
2878 $nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
2879 if (count($nattempts) >= $this->properties->maxattempts) {
2880 $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
2882 $lastpageseen = $attempt->pageid;
2884 } else if ($jumpto == LESSON_NEXTPAGE) {
2885 $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
2886 } else if ($jumpto == LESSON_CLUSTERJUMP) {
2887 $lastpageseen = $this->cluster_jump($attempt->pageid);
2889 $lastpageseen = $jumpto;
2893 if ($branchtables = $this->get_content_pages_viewed($retriescount, $USER->id, 'timeseen DESC')) {
2894 // In here, user has viewed a branch table.
2895 $lastbranchtable = current($branchtables);
2896 if (count($allattempts) > 0) {
2897 if ($lastbranchtable->timeseen > $attempt->timeseen) {
2898 // This branch table was viewed more recently than the question page.
2899 if (!empty($lastbranchtable->nextpageid)) {
2900 $lastpageseen = $lastbranchtable->nextpageid;
2902 // Next page ID did not exist prior to MDL-34006.
2903 $lastpageseen = $lastbranchtable->pageid;
2907 // Has not answered any questions but has viewed a branch table.
2908 if (!empty($lastbranchtable->nextpageid)) {
2909 $lastpageseen = $lastbranchtable->nextpageid;
2911 // Next page ID did not exist prior to MDL-34006.
2912 $lastpageseen = $lastbranchtable->pageid;
2916 return $lastpageseen;
2920 * Return the number of retries in a lesson for a given user.
2922 * @param int $userid the user id
2923 * @return int the retries count
2926 public function count_user_retries($userid) {
2929 return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid));
2933 * Check if a user left a timed session.
2935 * @param int $retriescount the number of retries for the lesson (the last retry number).
2936 * @return true if the user left the timed session
2939 public function left_during_timed_session($retriescount) {
2942 $conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount);
2943 return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0;
2947 * Trigger module viewed event and set the module viewed for completion.
2951 public function set_module_viewed() {
2953 require_once($CFG->libdir . '/completionlib.php');
2955 // Trigger module viewed event.
2956 $event = \mod_lesson\event\course_module_viewed::create(array(
2957 'objectid' => $this->properties->id,
2958 'context' => $this->get_context()
2960 $event->add_record_snapshot('course_modules', $this->get_cm());
2961 $event->add_record_snapshot('course', $this->get_courserecord());
2965 $completion = new completion_info($this->get_courserecord());
2966 $completion->set_module_viewed($this->get_cm());
2970 * Return the timers in the current lesson for the given user.
2972 * @param int $userid the user id
2973 * @param string $sort an order to sort the results in (optional, a valid SQL ORDER BY parameter).
2974 * @param string $fields a comma separated list of fields to return
2975 * @param int $limitfrom return a subset of records, starting at this point (optional).
2976 * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
2977 * @return array list of timers for the given user in the lesson
2980 public function get_user_timers($userid = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
2983 if ($userid === null) {
2984 $userid = $USER->id;
2987 $params = array('lessonid' => $this->properties->id, 'userid' => $userid);
2988 return $DB->get_records('lesson_timer', $params, $sort, $fields, $limitfrom, $limitnum);
2992 * Check if the user is out of time in a timed lesson.
2994 * @param stdClass $timer timer object
2995 * @return bool True if the user is on time, false is the user ran out of time
2998 public function check_time($timer) {
2999 if ($this->properties->timelimit) {
3000 $timeleft = $timer->starttime + $this->properties->timelimit - time();
3001 if ($timeleft <= 0) {
3003 $this->add_message(get_string('eolstudentoutoftime', 'lesson'));
3005 } else if ($timeleft < 60) {
3006 // One minute warning.
3007 $this->add_message(get_string('studentoneminwarning', 'lesson'));
3014 * Add different informative messages to the given page.
3016 * @param lesson_page $page page object
3017 * @param reviewmode $bool whether we are in review mode or not
3020 public function add_messages_on_page_view(lesson_page $page, $reviewmode) {
3023 if (!$this->can_manage()) {
3024 if ($page->qtype == LESSON_PAGE_BRANCHTABLE && $this->properties->minquestions) {
3025 // Tell student how many questions they have seen, how many are required and their grade.
3026 $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3027 $gradeinfo = lesson_grade($this, $ntries);
3028 if ($gradeinfo->attempts) {
3029 if ($gradeinfo->nquestions < $this->properties->minquestions) {
3031 $a->nquestions = $gradeinfo->nquestions;
3032 $a->minquestions = $this->properties->minquestions;
3033 $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
3036 if (!$reviewmode && $this->properties->ongoing) {
3037 $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
3038 if ($this->properties->grade != GRADE_TYPE_NONE) {
3040 $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
3041 $a->total = $this->properties->grade;
3042 $this->add_message(get_string('yourcurrentgradeisoutof', 'lesson', $a), 'notify');
3048 if ($this->properties->timelimit) {
3049 $this->add_message(get_string('teachertimerwarning', 'lesson'));
3051 if (lesson_display_teacher_warning($this)) {
3052 // This is the warning msg for teachers to inform them that cluster
3053 // and unseen does not work while logged in as a teacher.
3054 $warningvars = new stdClass();
3055 $warningvars->cluster = get_string('clusterjump', 'lesson');
3056 $warningvars->unseen = get_string('unseenpageinbranch', 'lesson');
3057 $this->add_message(get_string('teacherjumpwarning', 'lesson', $warningvars));
3063 * Get the ongoing score message for the user (depending on the user permission and lesson settings).
3065 * @return str the ongoing score message
3068 public function get_ongoing_score_message() {
3071 $context = $this->get_context();
3073 if (has_capability('mod/lesson:manage', $context)) {
3074 return get_string('teacherongoingwarning', 'lesson');
3076 $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3077 if (isset($USER->modattempts[$this->properties->id])) {
3080 $gradeinfo = lesson_grade($this, $ntries);
3082 if ($this->properties->custom) {
3083 $a->score = $gradeinfo->earned;
3084 $a->currenthigh = $gradeinfo->total;
3085 return get_string("ongoingcustom", "lesson", $a);
3087 $a->correct = $gradeinfo->earned;
3088 $a->viewed = $gradeinfo->attempts;
3089 return get_string("ongoingnormal", "lesson", $a);
3095 * Calculate the progress of the current user in the lesson.
3097 * @return int the progress (scale 0-100)
3100 public function calculate_progress() {
3103 // Check if the user is reviewing the attempt.
3104 if (isset($USER->modattempts[$this->properties->id])) {
3108 // All of the lesson pages.
3109 $pages = $this->load_all_pages();
3110 foreach ($pages as $page) {
3111 if ($page->prevpageid == 0) {
3112 $pageid = $page->id; // Find the first page id.
3117 // Current attempt number.
3118 if (!$ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id))) {
3119 $ntries = 0; // May not be necessary.
3122 $viewedpageids = array();
3123 if ($attempts = $this->get_attempts($ntries, false)) {
3124 foreach ($attempts as $attempt) {
3125 $viewedpageids[$attempt->pageid] = $attempt;
3129 $viewedbranches = array();
3130 // Collect all of the branch tables viewed.
3131 if ($branches = $this->get_content_pages_viewed($ntries, $USER->id, 'timeseen ASC', 'id, pageid')) {
3132 foreach ($branches as $branch) {
3133 $viewedbranches[$branch->pageid] = $branch;
3135 $viewedpageids = array_merge($viewedpageids, $viewedbranches);
3138 // Filter out the following pages:
3141 // - Pages found inside of Clusters
3142 // Do not filter out Cluster Page(s) because we count a cluster as one.
3143 // By keeping the cluster page, we get our 1.
3144 $validpages = array();
3145 while ($pageid != 0) {
3146 $pageid = $pages[$pageid]->valid_page_and_view($validpages, $viewedpageids);
3149 // Progress calculation as a percent.
3150 $progress = round(count($viewedpageids) / count($validpages), 2) * 100;
3151 return (int) $progress;
3155 * Calculate the correct page and prepare contents for a given page id (could be a page jump id).
3157 * @param int $pageid the given page id
3158 * @param mod_lesson_renderer $lessonoutput the lesson output rendered
3159 * @param bool $reviewmode whether we are in review mode or not
3160 * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect.
3161 * @return array the page object and contents
3162 * @throws moodle_exception
3165 public function prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, $redirect = true) {
3168 $page = $this->load_page($pageid);
3169 // Check if the page is of a special type and if so take any nessecary action.
3170 $newpageid = $page->callback_on_view($this->can_manage(), $redirect);
3172 // Avoid redirections returning the jump to special page id.
3173 if (!$redirect && is_numeric($newpageid) && $newpageid < 0) {
3174 return array($newpageid, null, null);
3177 if (is_numeric($newpageid)) {
3178 $page = $this->load_page($newpageid);
3181 // Add different informative messages to the given page.
3182 $this->add_messages_on_page_view($page, $reviewmode);
3184 if (is_array($page->answers) && count($page->answers) > 0) {
3185 // This is for modattempts option. Find the users previous answer to this page,
3186 // and then display it below in answer processing.
3187 if (isset($USER->modattempts[$this->properties->id])) {
3188 $retries = $this->count_user_retries($USER->id);
3189 if (!$attempts = $this->get_attempts($retries - 1, false, $page->id)) {
3190 throw new moodle_exception('cannotfindpreattempt', 'lesson');
3192 $attempt = end($attempts);
3193 $USER->modattempts[$this->properties->id] = $attempt;
3197 $lessoncontent = $lessonoutput->display_page($this, $page, $attempt);
3199 require_once($CFG->dirroot . '/mod/lesson/view_form.php');
3200 $data = new stdClass;
3201 $data->id = $this->get_cm()->id;
3202 $data->pageid = $page->id;
3203 $data->newpageid = $this->get_next_page($page->nextpageid);
3205 $customdata = array(
3206 'title' => $page->title,
3207 'contents' => $page->get_contents()
3209 $mform = new lesson_page_without_answers($CFG->wwwroot.'/mod/lesson/continue.php', $customdata);
3210 $mform->set_data($data);
3213 $lessoncontent = ob_get_contents();
3217 return array($page->id, $page, $lessoncontent);
3221 * This returns a real page id to jump to (or LESSON_EOL) after processing page responses.
3223 * @param lesson_page $page lesson page
3224 * @param int $newpageid the new page id
3225 * @return int the real page to jump to (or end of lesson)
3228 public function calculate_new_page_on_jump(lesson_page $page, $newpageid) {
3231 $canmanage = $this->can_manage();
3233 if (isset($USER->modattempts[$this->properties->id])) {
3234 // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time.
3235 if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) {
3236 // Remember, this session variable holds the pageid of the last page that the user saw.
3237 $newpageid = LESSON_EOL;
3239 $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3240 $nretakes--; // Make sure we are looking at the right try.
3241 $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid");
3244 // Make sure that the newpageid always defaults to something valid.
3245 $newpageid = LESSON_EOL;
3246 foreach ($attempts as $attempt) {
3247 if ($found && $temppageid != $attempt->pageid) {
3248 // Now try to find the next page, make sure next few attempts do no belong to current page.
3249 $newpageid = $attempt->pageid;
3252 if ($attempt->pageid == $page->id) {
3253 $found = true; // If found current page.
3254 $temppageid = $attempt->pageid;
3258 } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) {
3259 // Going to check to see if the page that the user is going to view next, is a cluster page.
3260 // If so, dont display, go into the cluster.
3261 // The $newpageid > 0 is used to filter out all of the negative code jumps.
3262 $newpage = $this->load_page($newpageid);
3263 if ($overridenewpageid = $newpage->override_next_page($newpageid)) {
3264 $newpageid = $overridenewpageid;
3266 } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) {
3268 if ($page->nextpageid == 0) {
3269 $newpageid = LESSON_EOL;
3271 $newpageid = $page->nextpageid;
3274 $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id);
3276 } else if ($newpageid == LESSON_PREVIOUSPAGE) {
3277 $newpageid = $page->prevpageid;
3278 } else if ($newpageid == LESSON_RANDOMPAGE) {
3279 $newpageid = lesson_random_question_jump($this, $page->id);
3280 } else if ($newpageid == LESSON_CLUSTERJUMP) {
3282 if ($page->nextpageid == 0) { // If teacher, go to next page.
3283 $newpageid = LESSON_EOL;
3285 $newpageid = $page->nextpageid;
3288 $newpageid = $this->cluster_jump($page->id);
3290 } else if ($newpageid == 0) {
3291 $newpageid = $page->id;
3292 } else if ($newpageid == LESSON_NEXTPAGE) {
3293 $newpageid = $this->get_next_page($page->nextpageid);
3300 * Process page responses.
3302 * @param lesson_page $page page object
3305 public function process_page_responses(lesson_page $page) {
3306 $context = $this->get_context();
3308 // Check the page has answers [MDL-25632].
3309 if (count($page->answers) > 0) {
3310 $result = $page->record_attempt($context);
3312 // The page has no answers so we will just progress to the next page in the
3313 // sequence (as set by newpageid).
3314 $result = new stdClass;
3315 $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT);
3316 $result->nodefaultresponse = true;
3317 $result->inmediatejump = false;
3320 if ($result->inmediatejump) {
3324 $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid);
3330 * Add different informative messages to the given page.
3332 * @param lesson_page $page page object
3333 * @param stdClass $result the page processing result object
3334 * @param bool $reviewmode whether we are in review mode or not
3337 public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) {
3339 if ($this->can_manage()) {
3340 // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
3341 if (lesson_display_teacher_warning($this)) {
3342 $warningvars = new stdClass();
3343 $warningvars->cluster = get_string("clusterjump", "lesson");
3344 $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
3345 $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
3347 // Inform teacher that s/he will not see the timer.
3348 if ($this->properties->timelimit) {
3349 $this->add_message(get_string("teachertimerwarning", "lesson"));
3352 // Report attempts remaining.
3353 if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) {
3354 $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
3359 * Process and return all the information for the end of lesson page.
3361 * @param string $outoftime used to check to see if the student ran out of time
3362 * @return stdclass an object with all the page data ready for rendering
3365 public function process_eol_page($outoftime) {
3368 $course = $this->get_courserecord();
3369 $cm = $this->get_cm();
3370 $canmanage = $this->can_manage();
3372 // Init all the possible fields and values.