MDL-53966 lesson: Allow maximum number of attempts to be unlimited
[moodle.git] / mod / lesson / locallib.php
CommitLineData
87f83794 1<?php
2
0a4abb73
SH
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
5491947a 18/**
19 * Local library file for Lesson. These are non-standard functions that are used
20 * only by Lesson.
21 *
9b24f68b 22 * @package mod_lesson
0a4abb73 23 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
cc3dbaaa 24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late
5491947a 25 **/
4b55d2af 26
0a4abb73 27/** Make sure this isn't being directly accessed */
1e7f8ea2 28defined('MOODLE_INTERNAL') || die();
0a4abb73
SH
29
30/** Include the files that are required by this module */
1e7f8ea2 31require_once($CFG->dirroot.'/course/moodleform_mod.php');
0a4abb73 32require_once($CFG->dirroot . '/mod/lesson/lib.php');
99d19c13 33require_once($CFG->libdir . '/filelib.php');
0a4abb73 34
44cb7e63
PS
35/** This page */
36define('LESSON_THISPAGE', 0);
0a4abb73
SH
37/** Next page -> any page not seen before */
38define("LESSON_UNSEENPAGE", 1);
39/** Next page -> any page not answered correctly */
40define("LESSON_UNANSWEREDPAGE", 2);
41/** Jump to Next Page */
42define("LESSON_NEXTPAGE", -1);
43/** End of Lesson */
44define("LESSON_EOL", -9);
45/** Jump to an unseen page within a branch and end of branch or end of lesson */
46define("LESSON_UNSEENBRANCHPAGE", -50);
47/** Jump to Previous Page */
48define("LESSON_PREVIOUSPAGE", -40);
49/** Jump to a random page within a branch and end of branch or end of lesson */
50define("LESSON_RANDOMPAGE", -60);
51/** Jump to a random Branch */
52define("LESSON_RANDOMBRANCH", -70);
53/** Cluster Jump */
54define("LESSON_CLUSTERJUMP", -80);
55/** Undefined */
56define("LESSON_UNDEFINED", -99);
5e7856af 57
1e7f8ea2
PS
58/** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
59define("LESSON_MAX_EVENT_LENGTH", "432000");
60
a1556aaf
SB
61/** Answer format is HTML */
62define("LESSON_ANSWER_HTML", "HTML");
1e7f8ea2 63
568ef8bb
AG
64/** Placeholder answer for all other answers. */
65define("LESSON_OTHER_ANSWERS", "@#wronganswer#@");
66
5e7856af 67//////////////////////////////////////////////////////////////////////////////////////
86342d63 68/// Any other lesson functions go here. Each of them must have a name that
5e7856af 69/// starts with lesson_
70
4b55d2af 71/**
86342d63 72 * Checks to see if a LESSON_CLUSTERJUMP or
4b55d2af 73 * a LESSON_UNSEENBRANCHPAGE is used in a lesson.
74 *
86342d63 75 * This function is only executed when a teacher is
4b55d2af 76 * checking the navigation for a lesson.
77 *
3983f2dc 78 * @param stdClass $lesson Id of the lesson that is to be checked.
4b55d2af 79 * @return boolean True or false.
80 **/
06469639 81function lesson_display_teacher_warning($lesson) {
646fc290 82 global $DB;
86342d63 83
ac8e16be 84 // get all of the lesson answers
0a4abb73 85 $params = array ("lessonid" => $lesson->id);
646fc290 86 if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) {
64a3ce8c 87 // no answers, then not using cluster or unseen
ac8e16be 88 return false;
89 }
90 // just check for the first one that fulfills the requirements
91 foreach ($lessonanswers as $lessonanswer) {
92 if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) {
93 return true;
94 }
95 }
86342d63 96
ac8e16be 97 // if no answers use either of the two jumps
98 return false;
5e7856af 99}
100
4b55d2af 101/**
102 * Interprets the LESSON_UNSEENBRANCHPAGE jump.
86342d63 103 *
4b55d2af 104 * will return the pageid of a random unseen page that is within a branch
105 *
0a4abb73 106 * @param lesson $lesson
f521f98a 107 * @param int $userid Id of the user.
4b55d2af 108 * @param int $pageid Id of the page from which we are jumping.
109 * @return int Id of the next page.
4b55d2af 110 **/
5e7856af 111function lesson_unseen_question_jump($lesson, $user, $pageid) {
646fc290 112 global $DB;
86342d63 113
ac8e16be 114 // get the number of retakes
0a4abb73 115 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) {
ac8e16be 116 $retakes = 0;
117 }
118
119 // get all the lesson_attempts aka what the user has seen
0a4abb73 120 if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) {
ac8e16be 121 foreach($viewedpages as $viewed) {
122 $seenpages[] = $viewed->pageid;
123 }
124 } else {
125 $seenpages = array();
126 }
127
128 // get the lesson pages
0a4abb73 129 $lessonpages = $lesson->load_all_pages();
86342d63 130
ac8e16be 131 if ($pageid == LESSON_UNSEENBRANCHPAGE) { // this only happens when a student leaves in the middle of an unseen question within a branch series
132 $pageid = $seenpages[0]; // just change the pageid to the last page viewed inside the branch table
133 }
134
135 // go up the pages till branch table
136 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
0a4abb73 137 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
ac8e16be 138 break;
139 }
140 $pageid = $lessonpages[$pageid]->prevpageid;
141 }
86342d63 142
97b4ec5e 143 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
86342d63 144
ac8e16be 145 // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array
146 $unseen = array();
86342d63 147 foreach($pagesinbranch as $page) {
ac8e16be 148 if (!in_array($page->id, $seenpages)) {
149 $unseen[] = $page->id;
150 }
151 }
152
153 if(count($unseen) == 0) {
154 if(isset($pagesinbranch)) {
155 $temp = end($pagesinbranch);
156 $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL
157 } else {
158 // there are no pages inside the branch, so return the next page
159 $nextpage = $lessonpages[$pageid]->nextpageid;
160 }
161 if ($nextpage == 0) {
162 return LESSON_EOL;
163 } else {
164 return $nextpage;
165 }
166 } else {
167 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
168 }
5e7856af 169}
170
4b55d2af 171/**
172 * Handles the unseen branch table jump.
173 *
0a4abb73 174 * @param lesson $lesson
f521f98a 175 * @param int $userid User id.
4b55d2af 176 * @return int Will return the page id of a branch table or end of lesson
4b55d2af 177 **/
0a4abb73 178function lesson_unseen_branch_jump($lesson, $userid) {
646fc290 179 global $DB;
86342d63 180
0a4abb73 181 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) {
ac8e16be 182 $retakes = 0;
183 }
184
b584c49d 185 if (!$seenbranches = $lesson->get_content_pages_viewed($retakes, $userid, 'timeseen DESC')) {
86f93345 186 print_error('cannotfindrecords', 'lesson');
ac8e16be 187 }
188
189 // get the lesson pages
0a4abb73 190 $lessonpages = $lesson->load_all_pages();
86342d63 191
ff85f902 192 // this loads all the viewed branch tables into $seen until it finds the branch table with the flag
ac8e16be 193 // which is the branch table that starts the unseenbranch function
86342d63 194 $seen = array();
ac8e16be 195 foreach ($seenbranches as $seenbranch) {
196 if (!$seenbranch->flag) {
197 $seen[$seenbranch->pageid] = $seenbranch->pageid;
198 } else {
199 $start = $seenbranch->pageid;
200 break;
201 }
202 }
203 // this function searches through the lesson pages to find all the branch tables
204 // that follow the flagged branch table
205 $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table
0f6e2f02 206 $branchtables = array();
ac8e16be 207 while ($pageid != 0) { // grab all of the branch table till eol
0a4abb73 208 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
ac8e16be 209 $branchtables[] = $lessonpages[$pageid]->id;
210 }
211 $pageid = $lessonpages[$pageid]->nextpageid;
212 }
213 $unseen = array();
214 foreach ($branchtables as $branchtable) {
215 // load all of the unseen branch tables into unseen
216 if (!array_key_exists($branchtable, $seen)) {
217 $unseen[] = $branchtable;
218 }
219 }
220 if (count($unseen) > 0) {
221 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page
222 } else {
223 return LESSON_EOL; // has viewed all of the branch tables
224 }
5e7856af 225}
226
4b55d2af 227/**
228 * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE).
86342d63 229 *
0a4abb73 230 * @param lesson $lesson
4b55d2af 231 * @param int $pageid The id of the page that we are jumping from (?)
232 * @return int The pageid of a random page that is within a branch table
4b55d2af 233 **/
0a4abb73 234function lesson_random_question_jump($lesson, $pageid) {
646fc290 235 global $DB;
86342d63 236
ac8e16be 237 // get the lesson pages
0a4abb73 238 $params = array ("lessonid" => $lesson->id);
646fc290 239 if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) {
86f93345 240 print_error('cannotfindpages', 'lesson');
ac8e16be 241 }
242
243 // go up the pages till branch table
244 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page
245
0a4abb73 246 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) {
ac8e16be 247 break;
248 }
249 $pageid = $lessonpages[$pageid]->prevpageid;
250 }
251
86342d63 252 // get the pages within the branch
97b4ec5e 253 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
86342d63 254
ac8e16be 255 if(count($pagesinbranch) == 0) {
256 // there are no pages inside the branch, so return the next page
257 return $lessonpages[$pageid]->nextpageid;
258 } else {
259 return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id; // returns a random page id for the next page
260 }
5e7856af 261}
262
4b55d2af 263/**
264 * Calculates a user's grade for a lesson.
265 *
4b55d2af 266 * @param object $lesson The lesson that the user is taking.
4b55d2af 267 * @param int $retries The attempt number.
ff85f902 268 * @param int $userid Id of the user (optional, default current user).
88427c07 269 * @return object { nquestions => number of questions answered
270 attempts => number of question attempts
271 total => max points possible
272 earned => points earned by student
273 grade => calculated percentage grade
274 nmanual => number of manually graded questions
275 manualpoints => point value for manually graded questions }
4b55d2af 276 */
86342d63 277function lesson_grade($lesson, $ntries, $userid = 0) {
646fc290 278 global $USER, $DB;
ac8e16be 279
88427c07 280 if (empty($userid)) {
281 $userid = $USER->id;
282 }
86342d63 283
88427c07 284 // Zero out everything
285 $ncorrect = 0;
286 $nviewed = 0;
287 $score = 0;
288 $nmanual = 0;
289 $manualpoints = 0;
290 $thegrade = 0;
291 $nquestions = 0;
292 $total = 0;
293 $earned = 0;
294
646fc290 295 $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries);
86342d63 296 if ($useranswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND
646fc290 297 userid = :userid AND retry = :retry", $params, "timeseen")) {
88427c07 298 // group each try with its page
299 $attemptset = array();
300 foreach ($useranswers as $useranswer) {
86342d63 301 $attemptset[$useranswer->pageid][] = $useranswer;
ac8e16be 302 }
86342d63 303
252e85be
MG
304 if (!empty($lesson->maxattempts)) {
305 // Drop all attempts that go beyond max attempts for the lesson.
306 foreach ($attemptset as $key => $set) {
307 $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts);
308 }
88427c07 309 }
86342d63 310
88427c07 311 // get only the pages and their answers that the user answered
646fc290 312 list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset));
0a4abb73
SH
313 array_unshift($parameters, $lesson->id);
314 $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters);
315 $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters);
86342d63 316
88427c07 317 // Number of pages answered
318 $nquestions = count($pages);
319
320 foreach ($attemptset as $attempts) {
0a4abb73 321 $page = lesson_page::load($pages[end($attempts)->pageid], $lesson);
88427c07 322 if ($lesson->custom) {
323 $attempt = end($attempts);
324 // If essay question, handle it, otherwise add to score
0a4abb73 325 if ($page->requires_manual_grading()) {
f672e3e9
RW
326 $useranswerobj = unserialize($attempt->useranswer);
327 if (isset($useranswerobj->score)) {
328 $earned += $useranswerobj->score;
329 }
88427c07 330 $nmanual++;
331 $manualpoints += $answers[$attempt->answerid]->score;
ab1e7c39 332 } else if (!empty($attempt->answerid)) {
0a4abb73 333 $earned += $page->earned_score($answers, $attempt);
88427c07 334 }
335 } else {
336 foreach ($attempts as $attempt) {
337 $earned += $attempt->correct;
338 }
339 $attempt = end($attempts); // doesn't matter which one
340 // If essay question, increase numbers
0a4abb73 341 if ($page->requires_manual_grading()) {
88427c07 342 $nmanual++;
343 $manualpoints++;
ac8e16be 344 }
345 }
88427c07 346 // Number of times answered
347 $nviewed += count($attempts);
348 }
86342d63 349
88427c07 350 if ($lesson->custom) {
ac8e16be 351 $bestscores = array();
88427c07 352 // Find the highest possible score per page to get our total
353 foreach ($answers as $answer) {
46341ab7 354 if(!isset($bestscores[$answer->pageid])) {
88427c07 355 $bestscores[$answer->pageid] = $answer->score;
46341ab7 356 } else if ($bestscores[$answer->pageid] < $answer->score) {
88427c07 357 $bestscores[$answer->pageid] = $answer->score;
ac8e16be 358 }
359 }
88427c07 360 $total = array_sum($bestscores);
361 } else {
362 // Check to make sure the student has answered the minimum questions
363 if ($lesson->minquestions and $nquestions < $lesson->minquestions) {
364 // Nope, increase number viewed by the amount of unanswered questions
365 $total = $nviewed + ($lesson->minquestions - $nquestions);
366 } else {
367 $total = $nviewed;
368 }
ac8e16be 369 }
88427c07 370 }
86342d63 371
88427c07 372 if ($total) { // not zero
373 $thegrade = round(100 * $earned / $total, 5);
374 }
86342d63 375
88427c07 376 // Build the grade information object
377 $gradeinfo = new stdClass;
378 $gradeinfo->nquestions = $nquestions;
379 $gradeinfo->attempts = $nviewed;
380 $gradeinfo->total = $total;
381 $gradeinfo->earned = $earned;
382 $gradeinfo->grade = $thegrade;
383 $gradeinfo->nmanual = $nmanual;
384 $gradeinfo->manualpoints = $manualpoints;
86342d63 385
88427c07 386 return $gradeinfo;
387}
388
62eda6ea 389/**
390 * Determines if a user can view the left menu. The determining factor
391 * is whether a user has a grade greater than or equal to the lesson setting
392 * of displayleftif
393 *
394 * @param object $lesson Lesson object of the current lesson
395 * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged
62eda6ea 396 **/
397function lesson_displayleftif($lesson) {
646fc290 398 global $CFG, $USER, $DB;
86342d63 399
62eda6ea 400 if (!empty($lesson->displayleftif)) {
401 // get the current user's max grade for this lesson
646fc290 402 $params = array ("userid" => $USER->id, "lessonid" => $lesson->id);
403 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)) {
62eda6ea 404 if ($maxgrade->maxgrade < $lesson->displayleftif) {
405 return 0; // turn off the displayleft
406 }
407 } else {
408 return 0; // no grades
409 }
410 }
86342d63 411
62eda6ea 412 // if we get to here, keep the original state of displayleft lesson setting
413 return $lesson->displayleft;
414}
5e7856af 415
4262a2f8 416/**
86342d63 417 *
4262a2f8 418 * @param $cm
419 * @param $lesson
420 * @param $page
421 * @return unknown_type
422 */
d9c26e21 423function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) {
4262a2f8 424 $bc = lesson_menu_block_contents($cm->id, $lesson);
425 if (!empty($bc)) {
426 $regions = $page->blocks->get_regions();
427 $firstregion = reset($regions);
d9c26e21 428 $page->blocks->add_fake_block($bc, $firstregion);
4262a2f8 429 }
430
431 $bc = lesson_mediafile_block_contents($cm->id, $lesson);
432 if (!empty($bc)) {
d9c26e21 433 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
4262a2f8 434 }
435
436 if (!empty($timer)) {
437 $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page);
438 if (!empty($bc)) {
d9c26e21 439 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region());
4262a2f8 440 }
441 }
442}
443
f521f98a 444/**
86342d63 445 * If there is a media file associated with this
4262a2f8 446 * lesson, return a block_contents that displays it.
f521f98a 447 *
448 * @param int $cmid Course Module ID for this lesson
449 * @param object $lesson Full lesson record object
4262a2f8 450 * @return block_contents
f521f98a 451 **/
4262a2f8 452function lesson_mediafile_block_contents($cmid, $lesson) {
d68ccdba 453 global $OUTPUT;
8d1a3963 454 if (empty($lesson->mediafile)) {
4262a2f8 455 return null;
f521f98a 456 }
4262a2f8 457
0a4abb73
SH
458 $options = array();
459 $options['menubar'] = 0;
460 $options['location'] = 0;
461 $options['left'] = 5;
462 $options['top'] = 5;
463 $options['scrollbars'] = 1;
464 $options['resizable'] = 1;
465 $options['width'] = $lesson->mediawidth;
466 $options['height'] = $lesson->mediaheight;
4262a2f8 467
9bf16314
PS
468 $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid);
469 $action = new popup_action('click', $link, 'lessonmediafile', $options);
470 $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson')));
86342d63 471
4262a2f8 472 $bc = new block_contents();
473 $bc->title = get_string('linkedmedia', 'lesson');
ca332713 474 $bc->attributes['class'] = 'mediafile block';
4262a2f8 475 $bc->content = $content;
476
477 return $bc;
f521f98a 478}
479
480/**
481 * If a timed lesson and not a teacher, then
4262a2f8 482 * return a block_contents containing the clock.
f521f98a 483 *
484 * @param int $cmid Course Module ID for this lesson
485 * @param object $lesson Full lesson record object
486 * @param object $timer Full timer record object
4262a2f8 487 * @return block_contents
f521f98a 488 **/
4262a2f8 489function lesson_clock_block_contents($cmid, $lesson, $timer, $page) {
490 // Display for timed lessons and for students only
5918e371 491 $context = context_module::instance($cmid);
a1acc001 492 if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) {
4262a2f8 493 return null;
494 }
f521f98a 495
50d70c5c 496 $content = '<div id="lesson-timer">';
0a4abb73 497 $content .= $lesson->time_remaining($timer->starttime);
4262a2f8 498 $content .= '</div>';
ba458143 499
a1acc001 500 $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit);
50d70c5c
JMV
501 $page->requires->data_for_js('clocksettings', $clocksettings, true);
502 $page->requires->strings_for_js(array('timeisup'), 'lesson');
227255b8 503 $page->requires->js('/mod/lesson/timer.js');
50d70c5c 504 $page->requires->js_init_call('show_clock');
ba458143 505
4262a2f8 506 $bc = new block_contents();
507 $bc->title = get_string('timeremaining', 'lesson');
6605ff8c 508 $bc->attributes['class'] = 'clock block';
4262a2f8 509 $bc->content = $content;
510
511 return $bc;
f521f98a 512}
513
514/**
515 * If left menu is turned on, then this will
516 * print the menu in a block
517 *
518 * @param int $cmid Course Module ID for this lesson
0a4abb73 519 * @param lesson $lesson Full lesson record object
f521f98a 520 * @return void
521 **/
4262a2f8 522function lesson_menu_block_contents($cmid, $lesson) {
646fc290 523 global $CFG, $DB;
f521f98a 524
4262a2f8 525 if (!$lesson->displayleft) {
526 return null;
527 }
f521f98a 528
0a4abb73
SH
529 $pages = $lesson->load_all_pages();
530 foreach ($pages as $page) {
531 if ((int)$page->prevpageid === 0) {
532 $pageid = $page->id;
533 break;
534 }
535 }
4262a2f8 536 $currentpageid = optional_param('pageid', $pageid, PARAM_INT);
f521f98a 537
4262a2f8 538 if (!$pageid || !$pages) {
539 return null;
f521f98a 540 }
f521f98a 541
e584e6ae
DW
542 $content = '<a href="#maincontent" class="accesshide">' .
543 get_string('skip', 'lesson') .
544 "</a>\n<div class=\"menuwrapper\">\n<ul>\n";
888f0c54 545
4262a2f8 546 while ($pageid != 0) {
547 $page = $pages[$pageid];
548
549 // Only process branch tables with display turned on
0a4abb73 550 if ($page->displayinmenublock && $page->display) {
86342d63 551 if ($page->id == $currentpageid) {
4262a2f8 552 $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n";
553 } else {
554 $content .= "<li class=\"notselected\"><a href=\"$CFG->wwwroot/mod/lesson/view.php?id=$cmid&amp;pageid=$page->id\">".format_string($page->title,true)."</a></li>\n";
555 }
86342d63 556
888f0c54 557 }
4262a2f8 558 $pageid = $page->nextpageid;
888f0c54 559 }
4262a2f8 560 $content .= "</ul>\n</div>\n";
888f0c54 561
4262a2f8 562 $bc = new block_contents();
563 $bc->title = get_string('lessonmenu', 'lesson');
6605ff8c 564 $bc->attributes['class'] = 'menu block';
4262a2f8 565 $bc->content = $content;
888f0c54 566
4262a2f8 567 return $bc;
448052a5
SH
568}
569
570/**
571 * Adds header buttons to the page for the lesson
572 *
573 * @param object $cm
574 * @param object $context
575 * @param bool $extraeditbuttons
576 * @param int $lessonpageid
577 */
578function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) {
579 global $CFG, $PAGE, $OUTPUT;
92059c7e
SH
580 if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) {
581 if ($lessonpageid === null) {
582 print_error('invalidpageid', 'lesson');
583 }
584 if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) {
32495414
MA
585 $url = new moodle_url('/mod/lesson/editpage.php', array(
586 'id' => $cm->id,
587 'pageid' => $lessonpageid,
588 'edit' => 1,
6330d488 589 'returnto' => $PAGE->url->out_as_local_url(false)
32495414 590 ));
5c2ed7e2 591 $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson')));
448052a5 592 }
448052a5 593 }
5c2ed7e2 594}
9b56a34f
PS
595
596/**
597 * This is a function used to detect media types and generate html code.
598 *
599 * @global object $CFG
600 * @global object $PAGE
601 * @param object $lesson
602 * @param object $context
603 * @return string $code the html code of media
604 */
605function lesson_get_media_html($lesson, $context) {
606 global $CFG, $PAGE, $OUTPUT;
607 require_once("$CFG->libdir/resourcelib.php");
608
64f93798 609 // get the media file link
8d1a3963
PS
610 if (strpos($lesson->mediafile, '://') !== false) {
611 $url = new moodle_url($lesson->mediafile);
612 } else {
613 // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder
614 $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/'));
615 }
9b56a34f
PS
616 $title = $lesson->mediafile;
617
8d1a3963 618 $clicktoopen = html_writer::link($url, get_string('download'));
9b56a34f
PS
619
620 $mimetype = resourcelib_guess_url_mimetype($url);
621
fcd2cbaf
PS
622 $extension = resourcelib_get_extension($url->out(false));
623
1abd4376 624 $mediamanager = core_media_manager::instance($PAGE);
8b7d95b6 625 $embedoptions = array(
fab11235
MG
626 core_media_manager::OPTION_TRUSTED => true,
627 core_media_manager::OPTION_BLOCK => true
8b7d95b6 628 );
629
9b56a34f
PS
630 // find the correct type and print it out
631 if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image
632 $code = resourcelib_embed_image($url, $title);
633
fab11235 634 } else if ($mediamanager->can_embed_url($url, $embedoptions)) {
8b7d95b6 635 // Media (audio/video) file.
fab11235 636 $code = $mediamanager->embed_url($url, $title, 0, 0, $embedoptions);
9b56a34f
PS
637
638 } else {
639 // anything else - just try object tag enlarged as much as possible
640 $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
641 }
642
643 return $code;
644}
1e7f8ea2 645
e0e1a83e
JMV
646/**
647 * Logic to happen when a/some group(s) has/have been deleted in a course.
648 *
649 * @param int $courseid The course ID.
247980b0 650 * @param int $groupid The group id if it is known
e0e1a83e
JMV
651 * @return void
652 */
247980b0 653function lesson_process_group_deleted_in_course($courseid, $groupid = null) {
e0e1a83e
JMV
654 global $DB;
655
e0e1a83e 656 $params = array('courseid' => $courseid);
247980b0
JMV
657 if ($groupid) {
658 $params['groupid'] = $groupid;
659 // We just update the group that was deleted.
660 $sql = "SELECT o.id, o.lessonid
661 FROM {lesson_overrides} o
662 JOIN {lesson} lesson ON lesson.id = o.lessonid
663 WHERE lesson.course = :courseid
664 AND o.groupid = :groupid";
665 } else {
666 // No groupid, we update all orphaned group overrides for all lessons in course.
667 $sql = "SELECT o.id, o.lessonid
668 FROM {lesson_overrides} o
669 JOIN {lesson} lesson ON lesson.id = o.lessonid
670 LEFT JOIN {groups} grp ON grp.id = o.groupid
671 WHERE lesson.course = :courseid
672 AND o.groupid IS NOT NULL
673 AND grp.id IS NULL";
674 }
e0e1a83e
JMV
675 $records = $DB->get_records_sql_menu($sql, $params);
676 if (!$records) {
677 return; // Nothing to do.
678 }
679 $DB->delete_records_list('lesson_overrides', 'id', array_keys($records));
680}
1e7f8ea2 681
7d5564d9
JL
682/**
683 * Return the overview report table and data.
684 *
685 * @param lesson $lesson lesson instance
686 * @param mixed $currentgroup false if not group used, 0 for all groups, group id (int) to filter by that groups
687 * @return mixed false if there is no information otherwise html_table and stdClass with the table and data
688 * @since Moodle 3.3
689 */
690function lesson_get_overview_report_table_and_data(lesson $lesson, $currentgroup) {
ae612a53 691 global $DB, $CFG, $OUTPUT;
8c218004 692 require_once($CFG->dirroot . '/mod/lesson/pagetypes/branchtable.php');
7d5564d9
JL
693
694 $context = $lesson->context;
695 $cm = $lesson->cm;
696 // Count the number of branch and question pages in this lesson.
697 $branchcount = $DB->count_records('lesson_pages', array('lessonid' => $lesson->id, 'qtype' => LESSON_PAGE_BRANCHTABLE));
698 $questioncount = ($DB->count_records('lesson_pages', array('lessonid' => $lesson->id)) - $branchcount);
699
700 // Only load students if there attempts for this lesson.
701 $attempts = $DB->record_exists('lesson_attempts', array('lessonid' => $lesson->id));
702 $branches = $DB->record_exists('lesson_branch', array('lessonid' => $lesson->id));
703 $timer = $DB->record_exists('lesson_timer', array('lessonid' => $lesson->id));
704 if ($attempts or $branches or $timer) {
705 list($esql, $params) = get_enrolled_sql($context, '', $currentgroup, true);
706 list($sort, $sortparams) = users_order_by_sql('u');
707
6a8e25ef
AH
708 $extrafields = get_extra_user_fields($context);
709
7d5564d9
JL
710 $params['a1lessonid'] = $lesson->id;
711 $params['b1lessonid'] = $lesson->id;
712 $params['c1lessonid'] = $lesson->id;
6a8e25ef 713 $ufields = user_picture::fields('u', $extrafields);
7d5564d9
JL
714 $sql = "SELECT DISTINCT $ufields
715 FROM {user} u
716 JOIN (
717 SELECT userid, lessonid FROM {lesson_attempts} a1
718 WHERE a1.lessonid = :a1lessonid
719 UNION
720 SELECT userid, lessonid FROM {lesson_branch} b1
721 WHERE b1.lessonid = :b1lessonid
722 UNION
723 SELECT userid, lessonid FROM {lesson_timer} c1
724 WHERE c1.lessonid = :c1lessonid
725 ) a ON u.id = a.userid
726 JOIN ($esql) ue ON ue.id = a.userid
727 ORDER BY $sort";
728
729 $students = $DB->get_recordset_sql($sql, $params);
730 if (!$students->valid()) {
731 $students->close();
732 return array(false, false);
733 }
734 } else {
735 return array(false, false);
736 }
737
738 if (! $grades = $DB->get_records('lesson_grades', array('lessonid' => $lesson->id), 'completed')) {
739 $grades = array();
740 }
741
742 if (! $times = $DB->get_records('lesson_timer', array('lessonid' => $lesson->id), 'starttime')) {
743 $times = array();
744 }
745
746 // Build an array for output.
747 $studentdata = array();
748
749 $attempts = $DB->get_recordset('lesson_attempts', array('lessonid' => $lesson->id), 'timeseen');
750 foreach ($attempts as $attempt) {
751 // 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.
752 if (empty($studentdata[$attempt->userid]) || empty($studentdata[$attempt->userid][$attempt->retry])) {
753 // restore/setup defaults
754 $n = 0;
755 $timestart = 0;
756 $timeend = 0;
757 $usergrade = null;
dd608921 758 $eol = 0;
7d5564d9
JL
759
760 // search for the grade record for this try. if not there, the nulls defined above will be used.
761 foreach($grades as $grade) {
762 // check to see if the grade matches the correct user
763 if ($grade->userid == $attempt->userid) {
764 // see if n is = to the retry
765 if ($n == $attempt->retry) {
766 // get grade info
767 $usergrade = round($grade->grade, 2); // round it here so we only have to do it once
768 break;
769 }
770 $n++; // if not equal, then increment n
771 }
772 }
773 $n = 0;
774 // search for the time record for this try. if not there, the nulls defined above will be used.
775 foreach($times as $time) {
776 // check to see if the grade matches the correct user
777 if ($time->userid == $attempt->userid) {
778 // see if n is = to the retry
779 if ($n == $attempt->retry) {
780 // get grade info
781 $timeend = $time->lessontime;
782 $timestart = $time->starttime;
783 $eol = $time->completed;
784 break;
785 }
786 $n++; // if not equal, then increment n
787 }
788 }
789
790 // build up the array.
791 // this array represents each student and all of their tries at the lesson
792 $studentdata[$attempt->userid][$attempt->retry] = array( "timestart" => $timestart,
793 "timeend" => $timeend,
794 "grade" => $usergrade,
795 "end" => $eol,
796 "try" => $attempt->retry,
797 "userid" => $attempt->userid);
798 }
799 }
800 $attempts->close();
801
802 $branches = $DB->get_recordset('lesson_branch', array('lessonid' => $lesson->id), 'timeseen');
803 foreach ($branches as $branch) {
804 // 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.
805 if (empty($studentdata[$branch->userid]) || empty($studentdata[$branch->userid][$branch->retry])) {
806 // Restore/setup defaults.
807 $n = 0;
808 $timestart = 0;
809 $timeend = 0;
810 $usergrade = null;
dd608921 811 $eol = 0;
7d5564d9
JL
812 // Search for the time record for this try. if not there, the nulls defined above will be used.
813 foreach ($times as $time) {
814 // Check to see if the grade matches the correct user.
815 if ($time->userid == $branch->userid) {
816 // See if n is = to the retry.
817 if ($n == $branch->retry) {
818 // Get grade info.
819 $timeend = $time->lessontime;
820 $timestart = $time->starttime;
821 $eol = $time->completed;
822 break;
823 }
824 $n++; // If not equal, then increment n.
825 }
826 }
827
828 // Build up the array.
829 // This array represents each student and all of their tries at the lesson.
830 $studentdata[$branch->userid][$branch->retry] = array( "timestart" => $timestart,
831 "timeend" => $timeend,
832 "grade" => $usergrade,
833 "end" => $eol,
834 "try" => $branch->retry,
835 "userid" => $branch->userid);
836 }
837 }
838 $branches->close();
839
840 // Need the same thing for timed entries that were not completed.
841 foreach ($times as $time) {
842 $endoflesson = $time->completed;
843 // If the time start is the same with another record then we shouldn't be adding another item to this array.
844 if (isset($studentdata[$time->userid])) {
845 $foundmatch = false;
846 $n = 0;
847 foreach ($studentdata[$time->userid] as $key => $value) {
848 if ($value['timestart'] == $time->starttime) {
849 // Don't add this to the array.
850 $foundmatch = true;
851 break;
852 }
853 }
854 $n = count($studentdata[$time->userid]) + 1;
855 if (!$foundmatch) {
856 // Add a record.
857 $studentdata[$time->userid][] = array(
858 "timestart" => $time->starttime,
859 "timeend" => $time->lessontime,
860 "grade" => null,
861 "end" => $endoflesson,
862 "try" => $n,
863 "userid" => $time->userid
864 );
865 }
866 } else {
867 $studentdata[$time->userid][] = array(
868 "timestart" => $time->starttime,
869 "timeend" => $time->lessontime,
870 "grade" => null,
871 "end" => $endoflesson,
872 "try" => 0,
873 "userid" => $time->userid
874 );
875 }
876 }
877
878 // To store all the data to be returned by the function.
879 $data = new stdClass();
880
881 // Determine if lesson should have a score.
882 if ($branchcount > 0 AND $questioncount == 0) {
883 // This lesson only contains content pages and is not graded.
884 $data->lessonscored = false;
885 } else {
886 // This lesson is graded.
887 $data->lessonscored = true;
888 }
889 // set all the stats variables
890 $data->numofattempts = 0;
891 $data->avescore = 0;
892 $data->avetime = 0;
893 $data->highscore = null;
894 $data->lowscore = null;
895 $data->hightime = null;
896 $data->lowtime = null;
897 $data->students = array();
898
899 $table = new html_table();
900
6a8e25ef
AH
901 $headers = [get_string('name')];
902
903 foreach ($extrafields as $field) {
904 $headers[] = get_user_field_name($field);
905 }
906
ae612a53
JP
907 $caneditlesson = has_capability('mod/lesson:edit', $context);
908 $attemptsheader = get_string('attempts', 'lesson');
909 if ($caneditlesson) {
910 $selectall = get_string('selectallattempts', 'lesson');
911 $deselectall = get_string('deselectallattempts', 'lesson');
912 // Build the select/deselect all control.
913 $selectallid = 'selectall-attempts';
914 $mastercheckbox = new \core\output\checkbox_toggleall('lesson-attempts', true, [
915 'id' => $selectallid,
916 'name' => $selectallid,
917 'value' => 1,
918 'label' => $selectall,
919 'selectall' => $selectall,
920 'deselectall' => $deselectall,
921 'labelclasses' => 'form-check-label'
922 ]);
923 $attemptsheader = $OUTPUT->render($mastercheckbox);
924 }
925 $headers [] = $attemptsheader;
6a8e25ef 926
7d5564d9
JL
927 // Set up the table object.
928 if ($data->lessonscored) {
6a8e25ef
AH
929 $headers [] = get_string('highscore', 'lesson');
930 }
931
932 $colcount = count($headers);
933
934 $table->head = $headers;
935
936 $table->align = [];
937 $table->align = array_pad($table->align, $colcount, 'center');
938 $table->align[$colcount - 1] = 'left';
939
940 if ($data->lessonscored) {
941 $table->align[$colcount - 2] = 'left';
7d5564d9 942 }
6a8e25ef
AH
943
944 $table->wrap = [];
945 $table->wrap = array_pad($table->wrap, $colcount, 'nowrap');
946
ae612a53 947 $table->attributes['class'] = 'table table-striped';
7d5564d9
JL
948
949 // print out the $studentdata array
950 // going through each student that has attempted the lesson, so, each student should have something to be displayed
951 foreach ($students as $student) {
952 // check to see if the student has attempts to print out
953 if (array_key_exists($student->id, $studentdata)) {
954 // set/reset some variables
955 $attempts = array();
956 $dataforstudent = new stdClass;
957 $dataforstudent->attempts = array();
958 // gather the data for each user attempt
959 $bestgrade = 0;
ae612a53 960
7d5564d9
JL
961 // $tries holds all the tries/retries a student has done
962 $tries = $studentdata[$student->id];
963 $studentname = fullname($student, true);
964
965 foreach ($tries as $try) {
966 $dataforstudent->attempts[] = $try;
967
968 // Start to build up the checkbox and link.
ae612a53
JP
969 $attempturlparams = [
970 'id' => $cm->id,
971 'action' => 'reportdetail',
972 'userid' => $try['userid'],
973 'try' => $try['try'],
974 ];
975
7d5564d9
JL
976 if ($try["grade"] !== null) { // if null then not done yet
977 // this is what the link does when the user has completed the try
978 $timetotake = $try["timeend"] - $try["timestart"];
979
7d5564d9
JL
980 if ($try["grade"] > $bestgrade) {
981 $bestgrade = $try["grade"];
982 }
32c78892
JP
983
984 $attemptdata = (object)[
985 'grade' => $try["grade"],
986 'timestart' => userdate($try["timestart"]),
987 'duration' => format_time($timetotake),
988 ];
989 $attemptlinkcontents = get_string('attemptinfowithgrade', 'lesson', $attemptdata);
990
7d5564d9
JL
991 } else {
992 if ($try["end"]) {
993 // User finished the lesson but has no grade. (Happens when there are only content pages).
7d5564d9 994 $timetotake = $try["timeend"] - $try["timestart"];
32c78892
JP
995 $attemptdata = (object)[
996 'timestart' => userdate($try["timestart"]),
997 'duration' => format_time($timetotake),
998 ];
999 $attemptlinkcontents = get_string('attemptinfonograde', 'lesson', $attemptdata);
7d5564d9
JL
1000 } else {
1001 // This is what the link does/looks like when the user has not completed the attempt.
7d5564d9
JL
1002 if ($try['timestart'] !== 0) {
1003 // Teacher previews do not track time spent.
32c78892
JP
1004 $attemptlinkcontents = get_string("notcompletedwithdate", "lesson", userdate($try["timestart"]));
1005 } else {
1006 $attemptlinkcontents = get_string("notcompleted", "lesson");
7d5564d9 1007 }
7d5564d9
JL
1008 $timetotake = null;
1009 }
1010 }
ae612a53
JP
1011 $attempturl = new moodle_url('/mod/lesson/report.php', $attempturlparams);
1012 $attemptlink = html_writer::link($attempturl, $attemptlinkcontents, ['class' => 'lesson-attempt-link']);
1013
1014 if ($caneditlesson) {
1015 $attemptid = 'attempt-' . $try['userid'] . '-' . $try['try'];
1016 $attemptname = 'attempts[' . $try['userid'] . '][' . $try['try'] . ']';
1017
1018 $checkbox = new \core\output\checkbox_toggleall('lesson-attempts', false, [
1019 'id' => $attemptid,
1020 'name' => $attemptname,
1021 'label' => $attemptlink
1022 ]);
1023 $attemptlink = $OUTPUT->render($checkbox);
1024 }
1025
7d5564d9 1026 // build up the attempts array
ae612a53 1027 $attempts[] = $attemptlink;
7d5564d9
JL
1028
1029 // Run these lines for the stats only if the user finnished the lesson.
1030 if ($try["end"]) {
1031 // User has completed the lesson.
1032 $data->numofattempts++;
1033 $data->avetime += $timetotake;
1034 if ($timetotake > $data->hightime || $data->hightime == null) {
1035 $data->hightime = $timetotake;
1036 }
1037 if ($timetotake < $data->lowtime || $data->lowtime == null) {
1038 $data->lowtime = $timetotake;
1039 }
1040 if ($try["grade"] !== null) {
1041 // The lesson was scored.
1042 $data->avescore += $try["grade"];
1043 if ($try["grade"] > $data->highscore || $data->highscore === null) {
1044 $data->highscore = $try["grade"];
1045 }
1046 if ($try["grade"] < $data->lowscore || $data->lowscore === null) {
1047 $data->lowscore = $try["grade"];
1048 }
1049
1050 }
1051 }
1052 }
1053 // get line breaks in after each attempt
1054 $attempts = implode("<br />\n", $attempts);
6a8e25ef
AH
1055 $row = [$studentname];
1056
1057 foreach ($extrafields as $field) {
1058 $row[] = $student->$field;
1059 }
1060
1061 $row[] = $attempts;
7d5564d9
JL
1062
1063 if ($data->lessonscored) {
1064 // Add the grade if the lesson is graded.
6a8e25ef 1065 $row[] = $bestgrade."%";
7d5564d9 1066 }
6a8e25ef
AH
1067
1068 $table->data[] = $row;
1069
7d5564d9
JL
1070 // Add the student data.
1071 $dataforstudent->id = $student->id;
1072 $dataforstudent->fullname = $studentname;
1073 $dataforstudent->bestgrade = $bestgrade;
1074 $data->students[] = $dataforstudent;
1075 }
1076 }
1077 $students->close();
1078 if ($data->numofattempts > 0) {
1079 $data->avescore = $data->avescore / $data->numofattempts;
1080 }
1081
1082 return array($table, $data);
1083}
1084
49e35378
JL
1085/**
1086 * Return information about one user attempt (including answers)
1087 * @param lesson $lesson lesson instance
1088 * @param int $userid the user id
1089 * @param int $attempt the attempt number
1090 * @return array the user answers (array) and user data stats (object)
1091 * @since Moodle 3.3
1092 */
1093function lesson_get_user_detailed_report_data(lesson $lesson, $userid, $attempt) {
1094 global $DB;
1095
1096 $context = $lesson->context;
1097 if (!empty($userid)) {
1098 // Apply overrides.
1099 $lesson->update_effective_access($userid);
1100 }
1101
409da213 1102 $pageid = 0;
49e35378
JL
1103 $lessonpages = $lesson->load_all_pages();
1104 foreach ($lessonpages as $lessonpage) {
1105 if ($lessonpage->prevpageid == 0) {
1106 $pageid = $lessonpage->id;
1107 }
1108 }
1109
1110 // now gather the stats into an object
1111 $firstpageid = $pageid;
1112 $pagestats = array();
1113 while ($pageid != 0) { // EOL
1114 $page = $lessonpages[$pageid];
1115 $params = array ("lessonid" => $lesson->id, "pageid" => $page->id);
1116 if ($allanswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND pageid = :pageid", $params, "timeseen")) {
1117 // get them ready for processing
1118 $orderedanswers = array();
1119 foreach ($allanswers as $singleanswer) {
1120 // ordering them like this, will help to find the single attempt record that we want to keep.
1121 $orderedanswers[$singleanswer->userid][$singleanswer->retry][] = $singleanswer;
1122 }
1123 // this is foreach user and for each try for that user, keep one attempt record
1124 foreach ($orderedanswers as $orderedanswer) {
1125 foreach($orderedanswer as $tries) {
1126 $page->stats($pagestats, $tries);
1127 }
1128 }
1129 } else {
1130 // no one answered yet...
1131 }
1132 //unset($orderedanswers); initialized above now
1133 $pageid = $page->nextpageid;
1134 }
1135
1136 $manager = lesson_page_type_manager::get($lesson);
1137 $qtypes = $manager->get_page_type_strings();
1138
1139 $answerpages = array();
1140 $answerpage = "";
1141 $pageid = $firstpageid;
1142 // cycle through all the pages
1143 // foreach page, add to the $answerpages[] array all the data that is needed
1144 // from the question, the users attempt, and the statistics
1145 // grayout pages that the user did not answer and Branch, end of branch, cluster
1146 // and end of cluster pages
1147 while ($pageid != 0) { // EOL
1148 $page = $lessonpages[$pageid];
1149 $answerpage = new stdClass;
63130e32
JL
1150 // Keep the original page object.
1151 $answerpage->page = $page;
49e35378
JL
1152 $data ='';
1153
1154 $answerdata = new stdClass;
1155 // Set some defaults for the answer data.
1156 $answerdata->score = null;
1157 $answerdata->response = null;
1158 $answerdata->responseformat = FORMAT_PLAIN;
1159
1160 $answerpage->title = format_string($page->title);
1161
1162 $options = new stdClass;
1163 $options->noclean = true;
1164 $options->overflowdiv = true;
1165 $options->context = $context;
1166 $answerpage->contents = format_text($page->contents, $page->contentsformat, $options);
1167
1168 $answerpage->qtype = $qtypes[$page->qtype].$page->option_description_string();
1169 $answerpage->grayout = $page->grayout;
1170 $answerpage->context = $context;
1171
1172 if (empty($userid)) {
1173 // there is no userid, so set these vars and display stats.
1174 $answerpage->grayout = 0;
1175 $useranswer = null;
1176 } elseif ($useranswers = $DB->get_records("lesson_attempts",array("lessonid"=>$lesson->id, "userid"=>$userid, "retry"=>$attempt,"pageid"=>$page->id), "timeseen")) {
1177 // get the user's answer for this page
1178 // need to find the right one
1179 $i = 0;
1180 foreach ($useranswers as $userattempt) {
1181 $useranswer = $userattempt;
1182 $i++;
1183 if ($lesson->maxattempts == $i) {
1184 break; // reached maxattempts, break out
1185 }
1186 }
1187 } else {
1188 // user did not answer this page, gray it out and set some nulls
1189 $answerpage->grayout = 1;
1190 $useranswer = null;
1191 }
1192 $i = 0;
1193 $n = 0;
1194 $answerpages[] = $page->report_answers(clone($answerpage), clone($answerdata), $useranswer, $pagestats, $i, $n);
1195 $pageid = $page->nextpageid;
1196 }
1197
1198 $userstats = new stdClass;
1199 if (!empty($userid)) {
1200 $params = array("lessonid"=>$lesson->id, "userid"=>$userid);
1201
1202 $alreadycompleted = true;
1203
1204 if (!$grades = $DB->get_records_select("lesson_grades", "lessonid = :lessonid and userid = :userid", $params, "completed", "*", $attempt, 1)) {
1205 $userstats->grade = -1;
1206 $userstats->completed = -1;
1207 $alreadycompleted = false;
1208 } else {
1209 $userstats->grade = current($grades);
1210 $userstats->completed = $userstats->grade->completed;
1211 $userstats->grade = round($userstats->grade->grade, 2);
1212 }
1213
1214 if (!$times = $lesson->get_user_timers($userid, 'starttime', '*', $attempt, 1)) {
1215 $userstats->timetotake = -1;
1216 $alreadycompleted = false;
1217 } else {
1218 $userstats->timetotake = current($times);
1219 $userstats->timetotake = $userstats->timetotake->lessontime - $userstats->timetotake->starttime;
1220 }
1221
1222 if ($alreadycompleted) {
1223 $userstats->gradeinfo = lesson_grade($lesson, $attempt, $userid);
1224 }
1225 }
1226
1227 return array($answerpages, $userstats);
1228}
1229
a4ffdbf3
JMV
1230/**
1231 * Return user's deadline for all lessons in a course, hereby taking into account group and user overrides.
1232 *
1233 * @param int $courseid the course id.
1234 * @return object An object with of all lessonsids and close unixdates in this course,
1235 * taking into account the most lenient overrides, if existing and 0 if no close date is set.
1236 */
1237function lesson_get_user_deadline($courseid) {
1238 global $DB, $USER;
1239
1240 // For teacher and manager/admins return lesson's deadline.
1241 if (has_capability('moodle/course:update', context_course::instance($courseid))) {
1242 $sql = "SELECT lesson.id, lesson.deadline AS userdeadline
1243 FROM {lesson} lesson
1244 WHERE lesson.course = :courseid";
1245
1246 $results = $DB->get_records_sql($sql, array('courseid' => $courseid));
1247 return $results;
1248 }
1249
1250 $sql = "SELECT a.id,
1251 COALESCE(v.userclose, v.groupclose, a.deadline, 0) AS userdeadline
1252 FROM (
1253 SELECT lesson.id as lessonid,
1254 MAX(leo.deadline) AS userclose, MAX(qgo.deadline) AS groupclose
1255 FROM {lesson} lesson
1256 LEFT JOIN {lesson_overrides} leo on lesson.id = leo.lessonid AND leo.userid = :userid
1257 LEFT JOIN {groups_members} gm ON gm.userid = :useringroupid
1258 LEFT JOIN {lesson_overrides} qgo on lesson.id = qgo.lessonid AND qgo.groupid = gm.groupid
1259 WHERE lesson.course = :courseid
1260 GROUP BY lesson.id
1261 ) v
1262 JOIN {lesson} a ON a.id = v.lessonid";
1263
1264 $results = $DB->get_records_sql($sql, array('userid' => $USER->id, 'useringroupid' => $USER->id, 'courseid' => $courseid));
1265 return $results;
1266
1267}
49e35378 1268
1e7f8ea2
PS
1269/**
1270 * Abstract class that page type's MUST inherit from.
1271 *
1272 * This is the abstract class that ALL add page type forms must extend.
1273 * You will notice that all but two of the methods this class contains are final.
1274 * Essentially the only thing that extending classes can do is extend custom_definition.
1275 * OR if it has a special requirement on creation it can extend construction_override
1276 *
1277 * @abstract
cc3dbaaa
PS
1278 * @copyright 2009 Sam Hemelryk
1279 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1e7f8ea2
PS
1280 */
1281abstract class lesson_add_page_form_base extends moodleform {
1282
1283 /**
1284 * This is the classic define that is used to identify this pagetype.
1285 * Will be one of LESSON_*
1286 * @var int
1287 */
1288 public $qtype;
1289
1290 /**
1291 * The simple string that describes the page type e.g. truefalse, multichoice
1292 * @var string
1293 */
1294 public $qtypestring;
1295
1296 /**
1297 * An array of options used in the htmleditor
1298 * @var array
1299 */
1300 protected $editoroptions = array();
1301
1302 /**
1303 * True if this is a standard page of false if it does something special.
1304 * Questions are standard pages, branch tables are not
1305 * @var bool
1306 */
1307 protected $standard = true;
1308
ceeab150
RT
1309 /**
1310 * Answer format supported by question type.
1311 */
1312 protected $answerformat = '';
1313
1314 /**
1315 * Response format supported by question type.
1316 */
1317 protected $responseformat = '';
1318
1e7f8ea2
PS
1319 /**
1320 * Each page type can and should override this to add any custom elements to
1321 * the basic form that they want
1322 */
1323 public function custom_definition() {}
1324
1325 /**
ceeab150
RT
1326 * Returns answer format used by question type.
1327 */
1328 public function get_answer_format() {
1329 return $this->answerformat;
1330 }
1331
1332 /**
1333 * Returns response format used by question type.
1334 */
1335 public function get_response_format() {
1336 return $this->responseformat;
1337 }
1338
1339 /**
1e7f8ea2
PS
1340 * Used to determine if this is a standard page or a special page
1341 * @return bool
1342 */
1343 public final function is_standard() {
1344 return (bool)$this->standard;
1345 }
1346
1347 /**
1348 * Add the required basic elements to the form.
1349 *
1350 * This method adds the basic elements to the form including title and contents
1351 * and then calls custom_definition();
1352 */
1353 public final function definition() {
197efffd 1354 global $CFG;
1e7f8ea2
PS
1355 $mform = $this->_form;
1356 $editoroptions = $this->_customdata['editoroptions'];
1357
a06face2
JE
1358 if ($this->qtypestring != 'selectaqtype') {
1359 if ($this->_customdata['edit']) {
1360 $mform->addElement('header', 'qtypeheading', get_string('edit'. $this->qtypestring, 'lesson'));
1361 } else {
1362 $mform->addElement('header', 'qtypeheading', get_string('add'. $this->qtypestring, 'lesson'));
1363 }
1364 }
1e7f8ea2 1365
32495414
MA
1366 if (!empty($this->_customdata['returnto'])) {
1367 $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']);
6330d488 1368 $mform->setType('returnto', PARAM_LOCALURL);
32495414
MA
1369 }
1370
1e7f8ea2
PS
1371 $mform->addElement('hidden', 'id');
1372 $mform->setType('id', PARAM_INT);
1373
1374 $mform->addElement('hidden', 'pageid');
1375 $mform->setType('pageid', PARAM_INT);
1376
1377 if ($this->standard === true) {
1378 $mform->addElement('hidden', 'qtype');
05db3cc7 1379 $mform->setType('qtype', PARAM_INT);
1e7f8ea2 1380
3fa2f030 1381 $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70));
3fa2f030 1382 $mform->addRule('title', get_string('required'), 'required', null, 'client');
197efffd
P
1383 if (!empty($CFG->formatstringstriptags)) {
1384 $mform->setType('title', PARAM_TEXT);
1385 } else {
1386 $mform->setType('title', PARAM_CLEANHTML);
1387 }
3fa2f030 1388
1e7f8ea2 1389 $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
3fa2f030
PS
1390 $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions);
1391 $mform->setType('contents_editor', PARAM_RAW);
1392 $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client');
1e7f8ea2
PS
1393 }
1394
1395 $this->custom_definition();
1396
1397 if ($this->_customdata['edit'] === true) {
1398 $mform->addElement('hidden', 'edit', 1);
747d5b1c 1399 $mform->setType('edit', PARAM_BOOL);
3fa2f030 1400 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
272bc0a6 1401 } else if ($this->qtype === 'questiontype') {
3fa2f030 1402 $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson'));
272bc0a6
RW
1403 } else {
1404 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson'));
1e7f8ea2
PS
1405 }
1406 }
1407
1408 /**
1409 * Convenience function: Adds a jumpto select element
1410 *
1411 * @param string $name
1412 * @param string|null $label
1413 * @param int $selected The page to select by default
1414 */
1415 protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
1416 $title = get_string("jump", "lesson");
1417 if ($label === null) {
1418 $label = $title;
1419 }
1420 if (is_int($name)) {
1421 $name = "jumpto[$name]";
1422 }
1423 $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
1424 $this->_form->setDefault($name, $selected);
1425 $this->_form->addHelpButton($name, 'jumps', 'lesson');
1426 }
1427
1428 /**
1429 * Convenience function: Adds a score input element
1430 *
1431 * @param string $name
1432 * @param string|null $label
1433 * @param mixed $value The default value
1434 */
1435 protected final function add_score($name, $label=null, $value=null) {
1436 if ($label === null) {
1437 $label = get_string("score", "lesson");
1438 }
a015bc03 1439
1e7f8ea2
PS
1440 if (is_int($name)) {
1441 $name = "score[$name]";
1442 }
1443 $this->_form->addElement('text', $name, $label, array('size'=>5));
a015bc03 1444 $this->_form->setType($name, PARAM_INT);
1e7f8ea2
PS
1445 if ($value !== null) {
1446 $this->_form->setDefault($name, $value);
1447 }
20685b28
AG
1448 $this->_form->addHelpButton($name, 'score', 'lesson');
1449
1450 // Score is only used for custom scoring. Disable the element when not in use to stop some confusion.
1451 if (!$this->_customdata['lesson']->custom) {
1452 $this->_form->freeze($name);
1453 }
1e7f8ea2
PS
1454 }
1455
1456 /**
1457 * Convenience function: Adds an answer editor
1458 *
1459 * @param int $count The count of the element to add
ecea65ca 1460 * @param string $label, null means default
a675ada5 1461 * @param bool $required
a1556aaf 1462 * @param string $format
407a6660
P
1463 * @param array $help Add help text via the addHelpButton. Must be an array which contains the string identifier and
1464 * component as it's elements
a675ada5 1465 * @return void
1e7f8ea2 1466 */
407a6660 1467 protected final function add_answer($count, $label = null, $required = false, $format= '', array $help = []) {
ecea65ca 1468 if ($label === null) {
a675ada5
PS
1469 $label = get_string('answer', 'lesson');
1470 }
0abc18cf 1471
a1556aaf 1472 if ($format == LESSON_ANSWER_HTML) {
0abc18cf
JMV
1473 $this->_form->addElement('editor', 'answer_editor['.$count.']', $label,
1474 array('rows' => '4', 'columns' => '80'),
1475 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1476 $this->_form->setType('answer_editor['.$count.']', PARAM_RAW);
1477 $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
a1556aaf 1478 } else {
a1300e98 1479 $this->_form->addElement('text', 'answer_editor['.$count.']', $label,
407a6660 1480 array('size' => '50', 'maxlength' => '200'));
a1300e98 1481 $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT);
0abc18cf
JMV
1482 }
1483
a675ada5
PS
1484 if ($required) {
1485 $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client');
1486 }
407a6660
P
1487
1488 if ($help) {
1489 $this->_form->addHelpButton("answer_editor[$count]", $help['identifier'], $help['component']);
1490 }
1e7f8ea2
PS
1491 }
1492 /**
1493 * Convenience function: Adds an response editor
1494 *
1495 * @param int $count The count of the element to add
ecea65ca 1496 * @param string $label, null means default
a675ada5
PS
1497 * @param bool $required
1498 * @return void
1e7f8ea2 1499 */
ecea65ca
RW
1500 protected final function add_response($count, $label = null, $required = false) {
1501 if ($label === null) {
a675ada5
PS
1502 $label = get_string('response', 'lesson');
1503 }
0abc18cf
JMV
1504 $this->_form->addElement('editor', 'response_editor['.$count.']', $label,
1505 array('rows' => '4', 'columns' => '80'),
1506 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes']));
1507 $this->_form->setType('response_editor['.$count.']', PARAM_RAW);
1508 $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML));
1509
a675ada5
PS
1510 if ($required) {
1511 $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client');
1512 }
1e7f8ea2
PS
1513 }
1514
1515 /**
1516 * A function that gets called upon init of this object by the calling script.
1517 *
1518 * This can be used to process an immediate action if required. Currently it
1519 * is only used in special cases by non-standard page types.
1520 *
1521 * @return bool
1522 */
0ed6f712 1523 public function construction_override($pageid, lesson $lesson) {
1e7f8ea2
PS
1524 return true;
1525 }
1526}
1527
1528
1529
1530/**
1531 * Class representation of a lesson
1532 *
1533 * This class is used the interact with, and manage a lesson once instantiated.
1534 * If you need to fetch a lesson object you can do so by calling
1535 *
1536 * <code>
1537 * lesson::load($lessonid);
1538 * // or
1539 * $lessonrecord = $DB->get_record('lesson', $lessonid);
1540 * $lesson = new lesson($lessonrecord);
1541 * </code>
1542 *
1543 * The class itself extends lesson_base as all classes within the lesson module should
1544 *
1545 * These properties are from the database
1546 * @property int $id The id of this lesson
1547 * @property int $course The ID of the course this lesson belongs to
1548 * @property string $name The name of this lesson
1549 * @property int $practice Flag to toggle this as a practice lesson
1550 * @property int $modattempts Toggle to allow the user to go back and review answers
1551 * @property int $usepassword Toggle the use of a password for entry
1552 * @property string $password The password to require users to enter
ff85f902 1553 * @property int $dependency ID of another lesson this lesson is dependent on
1e7f8ea2
PS
1554 * @property string $conditions Conditions of the lesson dependency
1555 * @property int $grade The maximum grade a user can achieve (%)
1556 * @property int $custom Toggle custom scoring on or off
1557 * @property int $ongoing Toggle display of an ongoing score
1558 * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
1559 * @property int $maxanswers The max number of answers or branches
1560 * @property int $maxattempts The maximum number of attempts a user can record
1561 * @property int $review Toggle use or wrong answer review button
1562 * @property int $nextpagedefault Override the default next page
1563 * @property int $feedback Toggles display of default feedback
1564 * @property int $minquestions Sets a minimum value of pages seen when calculating grades
1565 * @property int $maxpages Maximum number of pages this lesson can contain
1566 * @property int $retake Flag to allow users to retake a lesson
1567 * @property int $activitylink Relate this lesson to another lesson
1568 * @property string $mediafile File to pop up to or webpage to display
1569 * @property int $mediaheight Sets the height of the media file popup
1570 * @property int $mediawidth Sets the width of the media file popup
1571 * @property int $mediaclose Toggle display of a media close button
1572 * @property int $slideshow Flag for whether branch pages should be shown as slideshows
1573 * @property int $width Width of slideshow
1574 * @property int $height Height of slideshow
1575 * @property string $bgcolor Background colour of slideshow
ff85f902 1576 * @property int $displayleft Display a left menu
1e7f8ea2
PS
1577 * @property int $displayleftif Sets the condition on which the left menu is displayed
1578 * @property int $progressbar Flag to toggle display of a lesson progress bar
1e7f8ea2
PS
1579 * @property int $available Timestamp of when this lesson becomes available
1580 * @property int $deadline Timestamp of when this lesson is no longer available
1581 * @property int $timemodified Timestamp when lesson was last modified
87e472bd 1582 * @property int $allowofflineattempts Whether to allow the lesson to be attempted offline in the mobile app
1e7f8ea2
PS
1583 *
1584 * These properties are calculated
1585 * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
1586 * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
1587 *
cc3dbaaa
PS
1588 * @copyright 2009 Sam Hemelryk
1589 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1e7f8ea2
PS
1590 */
1591class lesson extends lesson_base {
1592
1593 /**
1594 * The id of the first page (where prevpageid = 0) gets set and retrieved by
1595 * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
1596 * @var int
1597 */
1598 protected $firstpageid = null;
1599 /**
1600 * The id of the last page (where nextpageid = 0) gets set and retrieved by
1601 * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
1602 * @var int
1603 */
1604 protected $lastpageid = null;
1605 /**
1606 * An array used to cache the pages associated with this lesson after the first
1607 * time they have been loaded.
1608 * A note to developers: If you are going to be working with MORE than one or
1609 * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1610 * in order to save excess database queries.
1611 * @var array An array of lesson_page objects
1612 */
1613 protected $pages = array();
1614 /**
1615 * Flag that gets set to true once all of the pages associated with the lesson
1616 * have been loaded.
1617 * @var bool
1618 */
1619 protected $loadedallpages = false;
1620
4a3391b6
JL
1621 /**
1622 * Course module object gets set and retrieved by directly calling <code>$lesson->cm;</code>
1623 * @see get_cm()
1624 * @var stdClass
1625 */
1626 protected $cm = null;
1627
7d7a2a4e
JL
1628 /**
1629 * Course object gets set and retrieved by directly calling <code>$lesson->courserecord;</code>
1630 * @see get_courserecord()
1631 * @var stdClass
1632 */
1633 protected $courserecord = null;
1634
4a3391b6
JL
1635 /**
1636 * Context object gets set and retrieved by directly calling <code>$lesson->context;</code>
1637 * @see get_context()
1638 * @var stdClass
1639 */
1640 protected $context = null;
1641
1642 /**
1643 * Constructor method
1644 *
1645 * @param object $properties
1646 * @param stdClass $cm course module object
7d7a2a4e 1647 * @param stdClass $course course object
4a3391b6
JL
1648 * @since Moodle 3.3
1649 */
7d7a2a4e 1650 public function __construct($properties, $cm = null, $course = null) {
4a3391b6
JL
1651 parent::__construct($properties);
1652 $this->cm = $cm;
7d7a2a4e 1653 $this->courserecord = $course;
4a3391b6
JL
1654 }
1655
1e7f8ea2
PS
1656 /**
1657 * Simply generates a lesson object given an array/object of properties
1658 * Overrides {@see lesson_base->create()}
1659 * @static
1660 * @param object|array $properties
1661 * @return lesson
1662 */
1663 public static function create($properties) {
1664 return new lesson($properties);
1665 }
1666
1667 /**
1668 * Generates a lesson object from the database given its id
1669 * @static
1670 * @param int $lessonid
1671 * @return lesson
1672 */
1673 public static function load($lessonid) {
3983f2dc
PS
1674 global $DB;
1675
1e7f8ea2
PS
1676 if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1677 print_error('invalidcoursemodule');
1678 }
1679 return new lesson($lesson);
1680 }
1681
1682 /**
1683 * Deletes this lesson from the database
1684 */
1685 public function delete() {
1686 global $CFG, $DB;
1687 require_once($CFG->libdir.'/gradelib.php');
1688 require_once($CFG->dirroot.'/calendar/lib.php');
1689
582ba677
FM
1690 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1691 $context = context_module::instance($cm->id);
1692
e0e1a83e
JMV
1693 $this->delete_all_overrides();
1694
40fe1538
DW
1695 grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1));
1696
1697 // We must delete the module record after we delete the grade item.
0e35ba6f 1698 $DB->delete_records("lesson", array("id"=>$this->properties->id));
1e7f8ea2
PS
1699 $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1700 $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1701 $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1702 $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1703 $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1704 $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1e7f8ea2 1705 if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
70b62308 1706 $coursecontext = context_course::instance($cm->course);
1e7f8ea2 1707 foreach($events as $event) {
70b62308 1708 $event->context = $coursecontext;
e1cd93ce 1709 $event = calendar_event::load($event);
1e7f8ea2
PS
1710 $event->delete();
1711 }
1712 }
1713
582ba677
FM
1714 // Delete files associated with this module.
1715 $fs = get_file_storage();
1716 $fs->delete_area_files($context->id);
1717
1e7f8ea2
PS
1718 return true;
1719 }
1720
e0e1a83e
JMV
1721 /**
1722 * Deletes a lesson override from the database and clears any corresponding calendar events
1723 *
1724 * @param int $overrideid The id of the override being deleted
1725 * @return bool true on success
1726 */
1727 public function delete_override($overrideid) {
1728 global $CFG, $DB;
1729
1730 require_once($CFG->dirroot . '/calendar/lib.php');
1731
1732 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
1733
1734 $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST);
1735
1736 // Delete the events.
1737 $conds = array('modulename' => 'lesson',
1738 'instance' => $this->properties->id);
1739 if (isset($override->userid)) {
1740 $conds['userid'] = $override->userid;
1741 } else {
1742 $conds['groupid'] = $override->groupid;
1743 }
1744 $events = $DB->get_records('event', $conds);
1745 foreach ($events as $event) {
e1cd93ce 1746 $eventold = calendar_event::load($event);
e0e1a83e
JMV
1747 $eventold->delete();
1748 }
1749
1750 $DB->delete_records('lesson_overrides', array('id' => $overrideid));
1751
1752 // Set the common parameters for one of the events we will be triggering.
1753 $params = array(
1754 'objectid' => $override->id,
1755 'context' => context_module::instance($cm->id),
1756 'other' => array(
1757 'lessonid' => $override->lessonid
1758 )
1759 );
1760 // Determine which override deleted event to fire.
1761 if (!empty($override->userid)) {
1762 $params['relateduserid'] = $override->userid;
1763 $event = \mod_lesson\event\user_override_deleted::create($params);
1764 } else {
1765 $params['other']['groupid'] = $override->groupid;
1766 $event = \mod_lesson\event\group_override_deleted::create($params);
1767 }
1768
1769 // Trigger the override deleted event.
1770 $event->add_record_snapshot('lesson_overrides', $override);
1771 $event->trigger();
1772
1773 return true;
1774 }
1775
1776 /**
1777 * Deletes all lesson overrides from the database and clears any corresponding calendar events
1778 */
1779 public function delete_all_overrides() {
1780 global $DB;
1781
1782 $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id');
1783 foreach ($overrides as $override) {
1784 $this->delete_override($override->id);
1785 }
1786 }
1787
cea8e276
P
1788 /**
1789 * Checks user enrollment in the current course.
1790 *
1791 * @param int $userid
1792 * @return null|stdClass user record
1793 */
1794 public function is_participant($userid) {
1795 return is_enrolled($this->get_context(), $userid, 'mod/lesson:view', $this->show_only_active_users());
1796 }
1797
1798 /**
1799 * Check is only active users in course should be shown.
1800 *
1801 * @return bool true if only active users should be shown.
1802 */
1803 public function show_only_active_users() {
1804 return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
1805 }
1806
e0e1a83e
JMV
1807 /**
1808 * Updates the lesson properties with override information for a user.
1809 *
1810 * Algorithm: For each lesson setting, if there is a matching user-specific override,
1811 * then use that otherwise, if there are group-specific overrides, return the most
1812 * lenient combination of them. If neither applies, leave the quiz setting unchanged.
1813 *
1814 * Special case: if there is more than one password that applies to the user, then
1815 * lesson->extrapasswords will contain an array of strings giving the remaining
1816 * passwords.
1817 *
1818 * @param int $userid The userid.
1819 */
1820 public function update_effective_access($userid) {
1821 global $DB;
1822
1823 // Check for user override.
1824 $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid));
1825
1826 if (!$override) {
1827 $override = new stdClass();
1828 $override->available = null;
1829 $override->deadline = null;
1830 $override->timelimit = null;
1831 $override->review = null;
1832 $override->maxattempts = null;
1833 $override->retake = null;
1834 $override->password = null;
1835 }
1836
1837 // Check for group overrides.
1838 $groupings = groups_get_user_groups($this->properties->course, $userid);
1839
1840 if (!empty($groupings[0])) {
1841 // Select all overrides that apply to the User's groups.
1842 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0]));
1843 $sql = "SELECT * FROM {lesson_overrides}
1844 WHERE groupid $extra AND lessonid = ?";
1845 $params[] = $this->properties->id;
1846 $records = $DB->get_records_sql($sql, $params);
1847
1848 // Combine the overrides.
1849 $availables = array();
1850 $deadlines = array();
1851 $timelimits = array();
1852 $reviews = array();
1853 $attempts = array();
1854 $retakes = array();
1855 $passwords = array();
1856
1857 foreach ($records as $gpoverride) {
1858 if (isset($gpoverride->available)) {
1859 $availables[] = $gpoverride->available;
1860 }
1861 if (isset($gpoverride->deadline)) {
1862 $deadlines[] = $gpoverride->deadline;
1863 }
1864 if (isset($gpoverride->timelimit)) {
1865 $timelimits[] = $gpoverride->timelimit;
1866 }
1867 if (isset($gpoverride->review)) {
1868 $reviews[] = $gpoverride->review;
1869 }
1870 if (isset($gpoverride->maxattempts)) {
1871 $attempts[] = $gpoverride->maxattempts;
1872 }
1873 if (isset($gpoverride->retake)) {
1874 $retakes[] = $gpoverride->retake;
1875 }
1876 if (isset($gpoverride->password)) {
1877 $passwords[] = $gpoverride->password;
1878 }
1879 }
1880 // If there is a user override for a setting, ignore the group override.
1881 if (is_null($override->available) && count($availables)) {
1882 $override->available = min($availables);
1883 }
1884 if (is_null($override->deadline) && count($deadlines)) {
1885 if (in_array(0, $deadlines)) {
1886 $override->deadline = 0;
1887 } else {
1888 $override->deadline = max($deadlines);
1889 }
1890 }
1891 if (is_null($override->timelimit) && count($timelimits)) {
1892 if (in_array(0, $timelimits)) {
1893 $override->timelimit = 0;
1894 } else {
1895 $override->timelimit = max($timelimits);
1896 }
1897 }
1898 if (is_null($override->review) && count($reviews)) {
1899 $override->review = max($reviews);
1900 }
1901 if (is_null($override->maxattempts) && count($attempts)) {
1902 $override->maxattempts = max($attempts);
1903 }
1904 if (is_null($override->retake) && count($retakes)) {
1905 $override->retake = max($retakes);
1906 }
1907 if (is_null($override->password) && count($passwords)) {
1908 $override->password = array_shift($passwords);
1909 if (count($passwords)) {
1910 $override->extrapasswords = $passwords;
1911 }
1912 }
1913
1914 }
1915
1916 // Merge with lesson defaults.
1917 $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake');
1918 foreach ($keys as $key) {
1919 if (isset($override->{$key})) {
1920 $this->properties->{$key} = $override->{$key};
1921 }
1922 }
1923
1924 // Special handling of lesson usepassword and password.
1925 if (isset($override->password)) {
1926 if ($override->password == '') {
1927 $this->properties->usepassword = 0;
1928 } else {
1929 $this->properties->usepassword = 1;
1930 $this->properties->password = $override->password;
1931 if (isset($override->extrapasswords)) {
1932 $this->properties->extrapasswords = $override->extrapasswords;
1933 }
1934 }
1935 }
1936 }
1937
1e7f8ea2
PS
1938 /**
1939 * Fetches messages from the session that may have been set in previous page
1940 * actions.
1941 *
1942 * <code>
1943 * // Do not call this method directly instead use
1944 * $lesson->messages;
1945 * </code>
1946 *
1947 * @return array
1948 */
1949 protected function get_messages() {
1950 global $SESSION;
1951
1952 $messages = array();
1953 if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1954 $messages = $SESSION->lesson_messages[$this->properties->id];
1955 unset($SESSION->lesson_messages[$this->properties->id]);
1956 }
1957
1958 return $messages;
1959 }
1960
1961 /**
1962 * Get all of the attempts for the current user.
1963 *
1964 * @param int $retries
1965 * @param bool $correct Optional: only fetch correct attempts
1966 * @param int $pageid Optional: only fetch attempts at the given page
1967 * @param int $userid Optional: defaults to the current user if not set
1968 * @return array|false
1969 */
1970 public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1971 global $USER, $DB;
1972 $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1973 if ($correct) {
1974 $params['correct'] = 1;
1975 }
1976 if ($pageid !== null) {
1977 $params['pageid'] = $pageid;
1978 }
1979 if ($userid === null) {
1980 $params['userid'] = $USER->id;
1981 }
ef4ba975 1982 return $DB->get_records('lesson_attempts', $params, 'timeseen ASC');
1e7f8ea2
PS
1983 }
1984
b584c49d
JL
1985
1986 /**
1987 * Get a list of content pages (formerly known as branch tables) viewed in the lesson for the given user during an attempt.
1988 *
1989 * @param int $lessonattempt the lesson attempt number (also known as retries)
1990 * @param int $userid the user id to retrieve the data from
1991 * @param string $sort an order to sort the results in (a valid SQL ORDER BY parameter)
1992 * @param string $fields a comma separated list of fields to return
1993 * @return array of pages
1994 * @since Moodle 3.3
1995 */
1996 public function get_content_pages_viewed($lessonattempt, $userid = null, $sort = '', $fields = '*') {
1997 global $USER, $DB;
1998
1999 if ($userid === null) {
2000 $userid = $USER->id;
2001 }
2002 $conditions = array("lessonid" => $this->properties->id, "userid" => $userid, "retry" => $lessonattempt);
2003 return $DB->get_records('lesson_branch', $conditions, $sort, $fields);
2004 }
2005
1e7f8ea2
PS
2006 /**
2007 * Returns the first page for the lesson or false if there isn't one.
2008 *
2009 * This method should be called via the magic method __get();
2010 * <code>
2011 * $firstpage = $lesson->firstpage;
2012 * </code>
2013 *
2014 * @return lesson_page|bool Returns the lesson_page specialised object or false
2015 */
2016 protected function get_firstpage() {
2017 $pages = $this->load_all_pages();
2018 if (count($pages) > 0) {
2019 foreach ($pages as $page) {
2020 if ((int)$page->prevpageid === 0) {
2021 return $page;
2022 }
2023 }
2024 }
2025 return false;
2026 }
2027
2028 /**
2029 * Returns the last page for the lesson or false if there isn't one.
2030 *
2031 * This method should be called via the magic method __get();
2032 * <code>
2033 * $lastpage = $lesson->lastpage;
2034 * </code>
2035 *
2036 * @return lesson_page|bool Returns the lesson_page specialised object or false
2037 */
2038 protected function get_lastpage() {
2039 $pages = $this->load_all_pages();
2040 if (count($pages) > 0) {
2041 foreach ($pages as $page) {
2042 if ((int)$page->nextpageid === 0) {
2043 return $page;
2044 }
2045 }
2046 }
2047 return false;
2048 }
2049
2050 /**
2051 * Returns the id of the first page of this lesson. (prevpageid = 0)
2052 * @return int
2053 */
2054 protected function get_firstpageid() {
2055 global $DB;
2056 if ($this->firstpageid == null) {
2057 if (!$this->loadedallpages) {
2058 $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
2059 if (!$firstpageid) {
2060 print_error('cannotfindfirstpage', 'lesson');
2061 }
2062 $this->firstpageid = $firstpageid;
2063 } else {
2064 $firstpage = $this->get_firstpage();
2065 $this->firstpageid = $firstpage->id;
2066 }
2067 }
2068 return $this->firstpageid;
2069 }
2070
2071 /**
2072 * Returns the id of the last page of this lesson. (nextpageid = 0)
2073 * @return int
2074 */
2075 public function get_lastpageid() {
2076 global $DB;
2077 if ($this->lastpageid == null) {
2078 if (!$this->loadedallpages) {
2079 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
2080 if (!$lastpageid) {
2081 print_error('cannotfindlastpage', 'lesson');
2082 }
2083 $this->lastpageid = $lastpageid;
2084 } else {
2085 $lastpageid = $this->get_lastpage();
2086 $this->lastpageid = $lastpageid->id;
2087 }
2088 }
2089
2090 return $this->lastpageid;
2091 }
2092
2093 /**
0343553f 2094 * Gets the next page id to display after the one that is provided.
1e7f8ea2
PS
2095 * @param int $nextpageid
2096 * @return bool
2097 */
2098 public function get_next_page($nextpageid) {
3983f2dc 2099 global $USER, $DB;
1e7f8ea2
PS
2100 $allpages = $this->load_all_pages();
2101 if ($this->properties->nextpagedefault) {
2102 // in Flash Card mode...first get number of retakes
0343553f 2103 $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
1e7f8ea2
PS
2104 shuffle($allpages);
2105 $found = false;
2106 if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
2107 foreach ($allpages as $nextpage) {
0343553f 2108 if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) {
1e7f8ea2
PS
2109 $found = true;
2110 break;
2111 }
2112 }
2113 } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
2114 foreach ($allpages as $nextpage) {
0343553f 2115 if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) {
1e7f8ea2
PS
2116 $found = true;
2117 break;
2118 }
2119 }
2120 }
2121 if ($found) {
2122 if ($this->properties->maxpages) {
2123 // check number of pages viewed (in the lesson)
0343553f
RW
2124 if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) {
2125 return LESSON_EOL;
1e7f8ea2
PS
2126 }
2127 }
0343553f 2128 return $nextpage->id;
1e7f8ea2
PS
2129 }
2130 }
2131 // In a normal lesson mode
2132 foreach ($allpages as $nextpage) {
0343553f
RW
2133 if ((int)$nextpage->id === (int)$nextpageid) {
2134 return $nextpage->id;
1e7f8ea2
PS
2135 }
2136 }
0343553f 2137 return LESSON_EOL;
1e7f8ea2
PS
2138 }
2139
2140 /**
2141 * Sets a message against the session for this lesson that will displayed next
2142 * time the lesson processes messages
2143 *
2144 * @param string $message
2145 * @param string $class
2146 * @param string $align
2147 * @return bool
2148 */
2149 public function add_message($message, $class="notifyproblem", $align='center') {
2150 global $SESSION;
2151
2152 if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
2153 $SESSION->lesson_messages = array();
2154 $SESSION->lesson_messages[$this->properties->id] = array();
2155 } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
2156 $SESSION->lesson_messages[$this->properties->id] = array();
2157 }
2158
2159 $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
2160
2161 return true;
2162 }
2163
2164 /**
2165 * Check if the lesson is accessible at the present time
2166 * @return bool True if the lesson is accessible, false otherwise
2167 */
2168 public function is_accessible() {
2169 $available = $this->properties->available;
2170 $deadline = $this->properties->deadline;
2171 return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
2172 }
2173
2174 /**
2175 * Starts the lesson time for the current user
2176 * @return bool Returns true
2177 */
2178 public function start_timer() {
2179 global $USER, $DB;
9c192d81
MN
2180
2181 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2182 false, MUST_EXIST);
2183
2184 // Trigger lesson started event.
2185 $event = \mod_lesson\event\lesson_started::create(array(
2186 'objectid' => $this->properties()->id,
2187 'context' => context_module::instance($cm->id),
2188 'courseid' => $this->properties()->course
2189 ));
2190 $event->trigger();
2191
1e7f8ea2 2192 $USER->startlesson[$this->properties->id] = true;
87e472bd
JL
2193
2194 $timenow = time();
1e7f8ea2
PS
2195 $startlesson = new stdClass;
2196 $startlesson->lessonid = $this->properties->id;
2197 $startlesson->userid = $USER->id;
87e472bd
JL
2198 $startlesson->starttime = $timenow;
2199 $startlesson->lessontime = $timenow;
2200 if (WS_SERVER) {
2201 $startlesson->timemodifiedoffline = $timenow;
2202 }
1e7f8ea2 2203 $DB->insert_record('lesson_timer', $startlesson);
a1acc001
JMV
2204 if ($this->properties->timelimit) {
2205 $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center');
1e7f8ea2
PS
2206 }
2207 return true;
2208 }
2209
2210 /**
2211 * Updates the timer to the current time and returns the new timer object
2212 * @param bool $restart If set to true the timer is restarted
2213 * @param bool $continue If set to true AND $restart=true then the timer
2214 * will continue from a previous attempt
2215 * @return stdClass The new timer
2216 */
25345cb4 2217 public function update_timer($restart=false, $continue=false, $endreached =false) {
1e7f8ea2 2218 global $USER, $DB;
e0f7b963
SB
2219
2220 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2221
1e7f8ea2
PS
2222 // clock code
2223 // get time information for this user
592c94f3 2224 if (!$timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1)) {
5ca2e765 2225 $this->start_timer();
592c94f3 2226 $timer = $this->get_user_timers($USER->id, 'starttime DESC', '*', 0, 1);
1e7f8ea2 2227 }
5ca2e765 2228 $timer = current($timer); // This will get the latest start time record.
1e7f8ea2
PS
2229
2230 if ($restart) {
2231 if ($continue) {
2232 // continue a previous test, need to update the clock (think this option is disabled atm)
2233 $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
e0f7b963
SB
2234
2235 // Trigger lesson resumed event.
2236 $event = \mod_lesson\event\lesson_resumed::create(array(
2237 'objectid' => $this->properties->id,
2238 'context' => context_module::instance($cm->id),
2239 'courseid' => $this->properties->course
2240 ));
2241 $event->trigger();
2242
1e7f8ea2
PS
2243 } else {
2244 // starting over, so reset the clock
2245 $timer->starttime = time();
e0f7b963
SB
2246
2247 // Trigger lesson restarted event.
2248 $event = \mod_lesson\event\lesson_restarted::create(array(
2249 'objectid' => $this->properties->id,
2250 'context' => context_module::instance($cm->id),
2251 'courseid' => $this->properties->course
2252 ));
2253 $event->trigger();
2254
1e7f8ea2
PS
2255 }
2256 }
2257
87e472bd
JL
2258 $timenow = time();
2259 $timer->lessontime = $timenow;
2260 if (WS_SERVER) {
2261 $timer->timemodifiedoffline = $timenow;
2262 }
25345cb4 2263 $timer->completed = $endreached;
1e7f8ea2 2264 $DB->update_record('lesson_timer', $timer);
4b570f71
JMV
2265
2266 // Update completion state.
2267 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2268 false, MUST_EXIST);
2269 $course = get_course($cm->course);
2270 $completion = new completion_info($course);
2271 if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) {
2272 $completion->update_state($cm, COMPLETION_COMPLETE);
2273 }
1e7f8ea2
PS
2274 return $timer;
2275 }
2276
2277 /**
2278 * Updates the timer to the current time then stops it by unsetting the user var
2279 * @return bool Returns true
2280 */
2281 public function stop_timer() {
2282 global $USER, $DB;
2283 unset($USER->startlesson[$this->properties->id]);
00c027c7
MN
2284
2285 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course,
2286 false, MUST_EXIST);
2287
2288 // Trigger lesson ended event.
2289 $event = \mod_lesson\event\lesson_ended::create(array(
2290 'objectid' => $this->properties()->id,
2291 'context' => context_module::instance($cm->id),
2292 'courseid' => $this->properties()->course
2293 ));
2294 $event->trigger();
2295
25345cb4 2296 return $this->update_timer(false, false, true);
1e7f8ea2
PS
2297 }
2298
2299 /**
2300 * Checks to see if the lesson has pages
2301 */
2302 public function has_pages() {
2303 global $DB;
2304 $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
2305 return ($pagecount>0);
2306 }
2307
2308 /**
2309 * Returns the link for the related activity
457b3ed1 2310 * @return string
1e7f8ea2
PS
2311 */
2312 public function link_for_activitylink() {
2313 global $DB;
2314 $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
2315 if ($module) {
2316 $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
2317 if ($modname) {
2318 $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
2319 if ($instancename) {
457b3ed1
LB
2320 return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php',
2321 array('id' => $this->properties->activitylink)), get_string('activitylinkname',
4394f9e3 2322 'lesson', $instancename), array('class' => 'centerpadded lessonbutton standardbutton pr-3'));
1e7f8ea2
PS
2323 }
2324 }
2325 }
2326 return '';
2327 }
2328
2329 /**
2330 * Loads the requested page.
2331 *
2332 * This function will return the requested page id as either a specialised
2333 * lesson_page object OR as a generic lesson_page.
2334 * If the page has been loaded previously it will be returned from the pages
2335 * array, otherwise it will be loaded from the database first
2336 *
2337 * @param int $pageid
2338 * @return lesson_page A lesson_page object or an object that extends it
2339 */
2340 public function load_page($pageid) {
2341 if (!array_key_exists($pageid, $this->pages)) {
2342 $manager = lesson_page_type_manager::get($this);
2343 $this->pages[$pageid] = $manager->load_page($pageid, $this);
2344 }
2345 return $this->pages[$pageid];
2346 }
2347
2348 /**
2349 * Loads ALL of the pages for this lesson
2350 *
2351 * @return array An array containing all pages from this lesson
2352 */
2353 public function load_all_pages() {
2354 if (!$this->loadedallpages) {
2355 $manager = lesson_page_type_manager::get($this);
2356 $this->pages = $manager->load_all_pages($this);
2357 $this->loadedallpages = true;
2358 }
2359 return $this->pages;
2360 }
2361
9f937c3b
AG
2362 /**
2363 * Duplicate the lesson page.
2364 *
2365 * @param int $pageid Page ID of the page to duplicate.
2366 * @return void.
2367 */
2368 public function duplicate_page($pageid) {
2369 global $PAGE;
2370 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2371 $context = context_module::instance($cm->id);
2372 // Load the page.
2373 $page = $this->load_page($pageid);
2374 $properties = $page->properties();
2375 // The create method checks to see if these properties are set and if not sets them to zero, hence the unsetting here.
2376 if (!$properties->qoption) {
2377 unset($properties->qoption);
2378 }
2379 if (!$properties->layout) {
2380 unset($properties->layout);
2381 }
2382 if (!$properties->display) {
2383 unset($properties->display);
2384 }
2385
2386 $properties->pageid = $pageid;
2387 // Add text and format into the format required to create a new page.
2388 $properties->contents_editor = array(
2389 'text' => $properties->contents,
2390 'format' => $properties->contentsformat
2391 );
2392 $answers = $page->get_answers();
2393 // Answers need to be added to $properties.
2394 $i = 0;
2395 $answerids = array();
2396 foreach ($answers as $answer) {
2397 // Needs to be rearranged to work with the create function.
2398 $properties->answer_editor[$i] = array(
2399 'text' => $answer->answer,
2400 'format' => $answer->answerformat
2401 );
2402
2403 $properties->response_editor[$i] = array(
2404 'text' => $answer->response,
2405 'format' => $answer->responseformat
2406 );
2407 $answerids[] = $answer->id;
2408
2409 $properties->jumpto[$i] = $answer->jumpto;
2410 $properties->score[$i] = $answer->score;
2411
2412 $i++;
2413 }
2414 // Create the duplicate page.
2415 $newlessonpage = lesson_page::create($properties, $this, $context, $PAGE->course->maxbytes);
2416 $newanswers = $newlessonpage->get_answers();
2417 // Copy over the file areas as well.
2418 $this->copy_page_files('page_contents', $pageid, $newlessonpage->id, $context->id);
2419 $j = 0;
2420 foreach ($newanswers as $answer) {
f4eef5fb 2421 if (isset($answer->answer) && strpos($answer->answer, '@@PLUGINFILE@@') !== false) {
9f937c3b
AG
2422 $this->copy_page_files('page_answers', $answerids[$j], $answer->id, $context->id);
2423 }
f4eef5fb 2424 if (isset($answer->response) && !is_array($answer->response) && strpos($answer->response, '@@PLUGINFILE@@') !== false) {
9f937c3b
AG
2425 $this->copy_page_files('page_responses', $answerids[$j], $answer->id, $context->id);
2426 }
2427 $j++;
2428 }
2429 }
2430
2431 /**
2432 * Copy the files from one page to another.
2433 *
2434 * @param string $filearea Area that the files are stored.
2435 * @param int $itemid Item ID.
2436 * @param int $newitemid The item ID for the new page.
2437 * @param int $contextid Context ID for this page.
2438 * @return void.
2439 */
2440 protected function copy_page_files($filearea, $itemid, $newitemid, $contextid) {
2441 $fs = get_file_storage();
2442 $files = $fs->get_area_files($contextid, 'mod_lesson', $filearea, $itemid);
2443 foreach ($files as $file) {
2444 $fieldupdates = array('itemid' => $newitemid);
2445 $fs->create_file_from_storedfile($fieldupdates, $file);
2446 }
2447 }
2448
1e7f8ea2 2449 /**
ff85f902 2450 * Determines if a jumpto value is correct or not.
1e7f8ea2
PS
2451 *
2452 * returns true if jumpto page is (logically) after the pageid page or
2453 * if the jumpto value is a special value. Returns false in all other cases.
2454 *
2455 * @param int $pageid Id of the page from which you are jumping from.
2456 * @param int $jumpto The jumpto number.
2457 * @return boolean True or false after a series of tests.
2458 **/
2459 public function jumpto_is_correct($pageid, $jumpto) {
2460 global $DB;
2461
2462 // first test the special values
2463 if (!$jumpto) {
2464 // same page
2465 return false;
2466 } elseif ($jumpto == LESSON_NEXTPAGE) {
2467 return true;
2468 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2469 return true;
2470 } elseif ($jumpto == LESSON_RANDOMPAGE) {
2471 return true;
2472 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2473 return true;
2474 } elseif ($jumpto == LESSON_EOL) {
2475 return true;
2476 }
2477
2478 $pages = $this->load_all_pages();
2479 $apageid = $pages[$pageid]->nextpageid;
2480 while ($apageid != 0) {
2481 if ($jumpto == $apageid) {
2482 return true;
2483 }
2484 $apageid = $pages[$apageid]->nextpageid;
2485 }
2486 return false;
2487 }
2488
2489 /**
2490 * Returns the time a user has remaining on this lesson
2491 * @param int $starttime Starttime timestamp
2492 * @return string
2493 */
2494 public function time_remaining($starttime) {
e0e1a83e 2495 $timeleft = $starttime + $this->properties->timelimit - time();
1e7f8ea2
PS
2496 $hours = floor($timeleft/3600);
2497 $timeleft = $timeleft - ($hours * 3600);
2498 $minutes = floor($timeleft/60);
2499 $secs = $timeleft - ($minutes * 60);
2500
2501 if ($minutes < 10) {
2502 $minutes = "0$minutes";
2503 }
2504 if ($secs < 10) {
2505 $secs = "0$secs";
2506 }
2507 $output = array();
2508 $output[] = $hours;
2509 $output[] = $minutes;
2510 $output[] = $secs;
2511 $output = implode(':', $output);
2512 return $output;
2513 }
2514
2515 /**
2516 * Interprets LESSON_CLUSTERJUMP jumpto value.
2517 *
2518 * This will select a page randomly
ff85f902 2519 * and the page selected will be inbetween a cluster page and end of clutter or end of lesson
1e7f8ea2
PS
2520 * and the page selected will be a page that has not been viewed already
2521 * and if any pages are within a branch table or end of branch then only 1 page within
2522 * the branch table or end of branch will be randomly selected (sub clustering).
2523 *
2524 * @param int $pageid Id of the current page from which we are jumping from.
2525 * @param int $userid Id of the user.
2526 * @return int The id of the next page.
2527 **/
2528 public function cluster_jump($pageid, $userid=null) {
2529 global $DB, $USER;
2530
2531 if ($userid===null) {
2532 $userid = $USER->id;
2533 }
2534 // get the number of retakes
2535 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
2536 $retakes = 0;
2537 }
2538 // get all the lesson_attempts aka what the user has seen
2539 $seenpages = array();
2540 if ($attempts = $this->get_attempts($retakes)) {
2541 foreach ($attempts as $attempt) {
2542 $seenpages[$attempt->pageid] = $attempt->pageid;
2543 }
2544
2545 }
2546
2547 // get the lesson pages
2548 $lessonpages = $this->load_all_pages();
2549 // find the start of the cluster
2550 while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
2551 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
2552 break;
2553 }
2554 $pageid = $lessonpages[$pageid]->prevpageid;
2555 }
2556
2557 $clusterpages = array();
2558 $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
2559 $unseen = array();
2560 foreach ($clusterpages as $key=>$cluster) {
fc16a1ac
JMV
2561 // Remove the page if it is in a branch table or is an endofbranch.
2562 if ($this->is_sub_page_of_type($cluster->id,
2563 array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER))
2564 || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) {
1e7f8ea2 2565 unset($clusterpages[$key]);
fc16a1ac
JMV
2566 } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) {
2567 // If branchtable, check to see if any pages inside have been viewed.
2568 $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2569 $flag = true;
2570 foreach ($branchpages as $branchpage) {
2571 if (array_key_exists($branchpage->id, $seenpages)) { // Check if any of the pages have been viewed.
2572 $flag = false;
2573 }
2574 }
2575 if ($flag && count($branchpages) > 0) {
2576 // Add branch table.
2577 $unseen[] = $cluster;
2578 }
1e7f8ea2
PS
2579 } elseif ($cluster->is_unseen($seenpages)) {
2580 $unseen[] = $cluster;
2581 }
2582 }
2583
2584 if (count($unseen) > 0) {
2585 // it does not contain elements, then use exitjump, otherwise find out next page/branch
2586 $nextpage = $unseen[rand(0, count($unseen)-1)];
2587 if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
2588 // if branch table, then pick a random page inside of it
2589 $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
2590 return $branchpages[rand(0, count($branchpages)-1)]->id;
2591 } else { // otherwise, return the page's id
2592 return $nextpage->id;
2593 }
2594 } else {
2595 // seen all there is to see, leave the cluster
2596 if (end($clusterpages)->nextpageid == 0) {
2597 return LESSON_EOL;
2598 } else {
2599 $clusterendid = $pageid;
a30e935a
JMV
2600 while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page.
2601 if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1e7f8ea2
PS
2602 break;
2603 }
a30e935a 2604 $clusterendid = $lessonpages[$clusterendid]->nextpageid;
1e7f8ea2
PS
2605 }
2606 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
2607 if ($exitjump == LESSON_NEXTPAGE) {
a30e935a 2608 $exitjump = $lessonpages[$clusterendid]->nextpageid;
1e7f8ea2
PS
2609 }
2610 if ($exitjump == 0) {
2611 return LESSON_EOL;
2612 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
2613 return $exitjump;
2614 } else {
2615 if (!array_key_exists($exitjump, $lessonpages)) {
2616 $found = false;
2617 foreach ($lessonpages as $page) {
2618 if ($page->id === $clusterendid) {
2619 $found = true;
2620 } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
2621 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
0d4705c7
JMV
2622 if ($exitjump == LESSON_NEXTPAGE) {
2623 $exitjump = $lessonpages[$page->id]->nextpageid;
2624 }
1e7f8ea2
PS
2625 break;
2626 }
2627 }
2628 }
2629 if (!array_key_exists($exitjump, $lessonpages)) {
2630 return LESSON_EOL;
2631 }
2ffd2afd
LB
2632 // Check to see that the return type is not a cluster.
2633 if ($lessonpages[$exitjump]->qtype == LESSON_PAGE_CLUSTER) {
2634 // If the exitjump is a cluster then go through this function again and try to find an unseen question.
2635 $exitjump = $this->cluster_jump($exitjump, $userid);
2636 }
1e7f8ea2
PS
2637 return $exitjump;
2638 }
2639 }
2640 }
2641 }
2642
2643 /**
2644 * Finds all pages that appear to be a subtype of the provided pageid until
2645 * an end point specified within $ends is encountered or no more pages exist
2646 *
2647 * @param int $pageid
2648 * @param array $ends An array of LESSON_PAGE_* types that signify an end of
2649 * the subtype
2650 * @return array An array of specialised lesson_page objects
2651 */
2652 public function get_sub_pages_of($pageid, array $ends) {
2653 $lessonpages = $this->load_all_pages();
2654 $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table
2655 $pages = array();
2656
2657 while (true) {
2658 if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
2659 break;
2660 }
2661 $pages[] = $lessonpages[$pageid];
2662 $pageid = $lessonpages[$pageid]->nextpageid;
2663 }
2664
2665 return $pages;
2666 }
2667
2668 /**
2669 * Checks to see if the specified page[id] is a subpage of a type specified in
2670 * the $types array, until either there are no more pages of we find a type
ff85f902 2671 * corresponding to that of a type specified in $ends
1e7f8ea2
PS
2672 *
2673 * @param int $pageid The id of the page to check
2674 * @param array $types An array of types that would signify this page was a subpage
2675 * @param array $ends An array of types that mean this is not a subpage
2676 * @return bool
2677 */
2678 public function is_sub_page_of_type($pageid, array $types, array $ends) {
2679 $pages = $this->load_all_pages();
2680 $pageid = $pages[$pageid]->prevpageid; // move up one
2681
2682 array_unshift($ends, 0);
2683 // go up the pages till branch table
2684 while (true) {
2685 if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
2686 return false;
2687 } else if (in_array($pages[$pageid]->qtype, $types)) {
2688 return true;
2689 }
2690 $pageid = $pages[$pageid]->prevpageid;
2691 }
2692 }
b7bfbfee
DM
2693
2694 /**
2695 * Move a page resorting all other pages.
2696 *
2697 * @param int $pageid
2698 * @param int $after
2699 * @return void
2700 */
2701 public function resort_pages($pageid, $after) {
2702 global $CFG;
2703
2704 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course);
2705 $context = context_module::instance($cm->id);
2706
2707 $pages = $this->load_all_pages();
2708
2709 if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) {
2710 print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id");
2711 }
2712
2713 $pagetomove = clone($pages[$pageid]);
2714 unset($pages[$pageid]);
2715
2716 $pageids = array();
2717 if ($after === 0) {
2718 $pageids['p0'] = $pageid;
2719 }
2720 foreach ($pages as $page) {
2721 $pageids[] = $page->id;
2722 if ($page->id == $after) {
2723 $pageids[] = $pageid;
2724 }
2725 }
2726
2727 $pageidsref = $pageids;
2728 reset($pageidsref);
2729 $prev = 0;
2730 $next = next($pageidsref);
2731 foreach ($pageids as $pid) {
2732 if ($pid === $pageid) {
2733 $page = $pagetomove;
2734 } else {
2735 $page = $pages[$pid];
2736 }
2737 if ($page->prevpageid != $prev || $page->nextpageid != $next) {
2738 $page->move($next, $prev);
2739
2740 if ($pid === $pageid) {
2741 // We will trigger an event.
2742 $pageupdated = array('next' => $next, 'prev' => $prev);
2743 }
2744 }
2745
2746 $prev = $page->id;
2747 $next = next($pageidsref);
2748 if (!$next) {
2749 $next = 0;
2750 }
2751 }
2752
2753 // Trigger an event: page moved.
2754 if (!empty($pageupdated)) {
2755 $eventparams = array(
2756 'context' => $context,
2757 'objectid' => $pageid,
2758 'other' => array(
2759 'pagetype' => $page->get_typestring(),
2760 'prevpageid' => $pageupdated['prev'],
2761 'nextpageid' => $pageupdated['next']
2762 )
2763 );
2764 $event = \mod_lesson\event\page_moved::create($eventparams);
2765 $event->trigger();
2766 }
2767
2768 }
4a3391b6
JL
2769
2770 /**
2771 * Return the lesson context object.
2772 *
2773 * @return stdClass context
2774 * @since Moodle 3.3
2775 */
2776 public function get_context() {
2777 if ($this->context == null) {
2778 $this->context = context_module::instance($this->get_cm()->id);
2779 }
2780 return $this->context;
2781 }
2782
2783 /**
2784 * Set the lesson course module object.
2785 *
2786 * @param stdClass $cm course module objct
2787 * @since Moodle 3.3
2788 */
2789 private function set_cm($cm) {
2790 $this->cm = $cm;
2791 }
2792
2793 /**
2794 * Return the lesson course module object.
2795 *
2796 * @return stdClass course module
2797 * @since Moodle 3.3
2798 */
2799 public function get_cm() {
2800 if ($this->cm == null) {
2801 $this->cm = get_coursemodule_from_instance('lesson', $this->properties->id);
2802 }
2803 return $this->cm;
2804 }
2805
7d7a2a4e
JL
2806 /**
2807 * Set the lesson course object.
2808 *
2809 * @param stdClass $course course objct
2810 * @since Moodle 3.3
2811 */
2812 private function set_courserecord($course) {
2813 $this->courserecord = $course;
2814 }
2815
2816 /**
2817 * Return the lesson course object.
2818 *
2819 * @return stdClass course
2820 * @since Moodle 3.3
2821 */
2822 public function get_courserecord() {
2823 global $DB;
2824
2825 if ($this->courserecord == null) {
2826 $this->courserecord = $DB->get_record('course', array('id' => $this->properties->course));
2827 }
2828 return $this->courserecord;
2829 }
2830
4a3391b6
JL
2831 /**
2832 * Check if the user can manage the lesson activity.
2833 *
2834 * @return bool true if the user can manage the lesson
2835 * @since Moodle 3.3
2836 */
2837 public function can_manage() {
2838 return has_capability('mod/lesson:manage', $this->get_context());
2839 }
2840
2841 /**
2842 * Check if time restriction is applied.
2843 *
2844 * @return mixed false if there aren't restrictions or an object with the restriction information
2845 * @since Moodle 3.3
2846 */
2847 public function get_time_restriction_status() {
2848 if ($this->can_manage()) {
2849 return false;
2850 }
2851
2852 if (!$this->is_accessible()) {
2853 if ($this->properties->deadline != 0 && time() > $this->properties->deadline) {
2854 $status = ['reason' => 'lessonclosed', 'time' => $this->properties->deadline];
2855 } else {
2856 $status = ['reason' => 'lessonopen', 'time' => $this->properties->available];
2857 }
2858 return (object) $status;
2859 }
2860 return false;
2861 }
2862
2863 /**
2864 * Check if password restriction is applied.
2865 *
2866 * @param string $userpassword the user password to check (if the restriction is set)
2867 * @return mixed false if there aren't restrictions or an object with the restriction information
2868 * @since Moodle 3.3
2869 */
2870 public function get_password_restriction_status($userpassword) {
2871 global $USER;
2872 if ($this->can_manage()) {
2873 return false;
2874 }
2875
2876 if ($this->properties->usepassword && empty($USER->lessonloggedin[$this->id])) {
2877 $correctpass = false;
2878 if (!empty($userpassword) &&
2879 (($this->properties->password == md5(trim($userpassword))) || ($this->properties->password == trim($userpassword)))) {
2880 // With or without md5 for backward compatibility (MDL-11090).
2881 $correctpass = true;
2882 $USER->lessonloggedin[$this->id] = true;
2883 } else if (isset($this->properties->extrapasswords)) {
2884 // Group overrides may have additional passwords.
2885 foreach ($this->properties->extrapasswords as $password) {
2886 if (strcmp($password, md5(trim($userpassword))) === 0 || strcmp($password, trim($userpassword)) === 0) {
2887 $correctpass = true;
2888 $USER->lessonloggedin[$this->id] = true;
2889 }
2890 }
2891 }
2892 return !$correctpass;
2893 }
2894 return false;
2895 }
2896
2897 /**
2898 * Check if dependencies restrictions are applied.
2899 *
2900 * @return mixed false if there aren't restrictions or an object with the restriction information
2901 * @since Moodle 3.3
2902 */
2903 public function get_dependencies_restriction_status() {
2904 global $DB, $USER;
2905 if ($this->can_manage()) {
2906 return false;
2907 }
2908
2909 if ($dependentlesson = $DB->get_record('lesson', array('id' => $this->properties->dependency))) {
2910 // Lesson exists, so we can proceed.
2911 $conditions = unserialize($this->properties->conditions);
2912 // Assume false for all.
2913 $errors = array();
2914 // Check for the timespent condition.
2915 if ($conditions->timespent) {
2916 $timespent = false;
2917 if ($attempttimes = $DB->get_records('lesson_timer', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2918 // Go through all the times and test to see if any of them satisfy the condition.
2919 foreach ($attempttimes as $attempttime) {
2920 $duration = $attempttime->lessontime - $attempttime->starttime;
2921 if ($conditions->timespent < $duration / 60) {
2922 $timespent = true;
2923 }
2924 }
2925 }
2926 if (!$timespent) {
2927 $errors[] = get_string('timespenterror', 'lesson', $conditions->timespent);
2928 }
2929 }
2930 // Check for the gradebetterthan condition.
2931 if ($conditions->gradebetterthan) {
2932 $gradebetterthan = false;
2933 if ($studentgrades = $DB->get_records('lesson_grades', array("userid" => $USER->id, "lessonid" => $dependentlesson->id))) {
2934 // Go through all the grades and test to see if any of them satisfy the condition.
2935 foreach ($studentgrades as $studentgrade) {
2936 if ($studentgrade->grade >= $conditions->gradebetterthan) {
2937 $gradebetterthan = true;
2938 }
2939 }
2940 }
2941 if (!$gradebetterthan) {
2942 $errors[] = get_string('gradebetterthanerror', 'lesson', $conditions->gradebetterthan);
2943 }
2944 }
2945 // Check for the completed condition.
2946 if ($conditions->completed) {
2947 if (!$DB->count_records('lesson_grades', array('userid' => $USER->id, 'lessonid' => $dependentlesson->id))) {
2948 $errors[] = get_string('completederror', 'lesson');
2949 }
2950 }
2951 if (!empty($errors)) {
2952 return (object) ['errors' => $errors, 'dependentlesson' => $dependentlesson];
2953 }
2954 }
2955 return false;
2956 }
37029e46
JL
2957
2958 /**
2959 * Check if the lesson is in review mode. (The user already finished it and retakes are not allowed).
2960 *
2961 * @return bool true if is in review mode
2962 * @since Moodle 3.3
2963 */
2964 public function is_in_review_mode() {
2965 global $DB, $USER;
2966
2967 $userhasgrade = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
2968 if ($userhasgrade && !$this->properties->retake) {
2969 return true;
2970 }
2971 return false;
2972 }
2973
2974 /**
2975 * Return the last page the current user saw.
2976 *
2977 * @param int $retriescount the number of retries for the lesson (the last retry number).
2978 * @return mixed false if the user didn't see the lesson or the last page id
2979 */
2980 public function get_last_page_seen($retriescount) {
2981 global $DB, $USER;
2982
2983 $lastpageseen = false;
2984 $allattempts = $this->get_attempts($retriescount);
2985 if (!empty($allattempts)) {
2986 $attempt = end($allattempts);
2987 $attemptpage = $this->load_page($attempt->pageid);
2988 $jumpto = $DB->get_field('lesson_answers', 'jumpto', array('id' => $attempt->answerid));
2989 // Convert the jumpto to a proper page id.
2990 if ($jumpto == 0) {
2991 // Check if a question has been incorrectly answered AND no more attempts at it are left.
2992 $nattempts = $this->get_attempts($attempt->retry, false, $attempt->pageid, $USER->id);
2993 if (count($nattempts) >= $this->properties->maxattempts) {
2994 $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
2995 } else {
2996 $lastpageseen = $attempt->pageid;
2997 }
2998 } else if ($jumpto == LESSON_NEXTPAGE) {
2999 $lastpageseen = $this->get_next_page($attemptpage->nextpageid);
3000 } else if ($jumpto == LESSON_CLUSTERJUMP) {
3001 $lastpageseen = $this->cluster_jump($attempt->pageid);
3002 } else {
3003 $lastpageseen = $jumpto;
3004 }
3005 }
3006
b584c49d 3007 if ($branchtables = $this->get_content_pages_viewed($retriescount, $USER->id, 'timeseen DESC')) {
37029e46
JL
3008 // In here, user has viewed a branch table.
3009 $lastbranchtable = current($branchtables);
3010 if (count($allattempts) > 0) {
3011 if ($lastbranchtable->timeseen > $attempt->timeseen) {
3012 // This branch table was viewed more recently than the question page.
3013 if (!empty($lastbranchtable->nextpageid)) {
3014 $lastpageseen = $lastbranchtable->nextpageid;
3015 } else {
3016 // Next page ID did not exist prior to MDL-34006.
3017 $lastpageseen = $lastbranchtable->pageid;
3018 }
3019 }
3020 } else {
3021 // Has not answered any questions but has viewed a branch table.
3022 if (!empty($lastbranchtable->nextpageid)) {
3023 $lastpageseen = $lastbranchtable->nextpageid;
3024 } else {
3025 // Next page ID did not exist prior to MDL-34006.
3026 $lastpageseen = $lastbranchtable->pageid;
3027 }
3028 }
3029 }
3030 return $lastpageseen;
3031 }
3032
3033 /**
3034 * Return the number of retries in a lesson for a given user.
3035 *
3036 * @param int $userid the user id
3037 * @return int the retries count
3038 * @since Moodle 3.3
3039 */
3040 public function count_user_retries($userid) {
3041 global $DB;
3042
3043 return $DB->count_records('lesson_grades', array("lessonid" => $this->properties->id, "userid" => $userid));
3044 }
3045
3046 /**
3047 * Check if a user left a timed session.
3048 *
3049 * @param int $retriescount the number of retries for the lesson (the last retry number).
3050 * @return true if the user left the timed session
7d7a2a4e 3051 * @since Moodle 3.3
37029e46
JL
3052 */
3053 public function left_during_timed_session($retriescount) {
3054 global $DB, $USER;
3055
3056 $conditions = array('lessonid' => $this->properties->id, 'userid' => $USER->id, 'retry' => $retriescount);
3057 return $DB->count_records('lesson_attempts', $conditions) > 0 || $DB->count_records('lesson_branch', $conditions) > 0;
3058 }
7d7a2a4e
JL
3059
3060 /**
3061 * Trigger module viewed event and set the module viewed for completion.
3062 *
3063 * @since Moodle 3.3
3064 */
3065 public function set_module_viewed() {
3066 global $CFG;
3067 require_once($CFG->libdir . '/completionlib.php');
3068
3069 // Trigger module viewed event.
3070 $event = \mod_lesson\event\course_module_viewed::create(array(
3071 'objectid' => $this->properties->id,
3072 'context' => $this->get_context()
3073 ));
3074 $event->add_record_snapshot('course_modules', $this->get_cm());
3075 $event->add_record_snapshot('course', $this->get_courserecord());
3076 $event->trigger();
3077
3078 // Mark as viewed.
3079 $completion = new completion_info($this->get_courserecord());
3080 $completion->set_module_viewed($this->get_cm());
3081 }
592c94f3
JL
3082
3083 /**
3084 * Return the timers in the current lesson for the given user.
3085 *
3086 * @param int $userid the user id
3087 * @param string $sort an order to sort the results in (optional, a valid SQL ORDER BY parameter).
3088 * @param string $fields a comma separated list of fields to return
3089 * @param int $limitfrom return a subset of records, starting at this point (optional).
3090 * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set).
3091 * @return array list of timers for the given user in the lesson
3092 * @since Moodle 3.3
3093 */
3094 public function get_user_timers($userid = null, $sort = '', $fields = '*', $limitfrom = 0, $limitnum = 0) {
3095 global $DB, $USER;
3096
3097 if ($userid === null) {
3098 $userid = $USER->id;
3099 }
3100
3101 $params = array('lessonid' => $this->properties->id, 'userid' => $userid);
3102 return $DB->get_records('lesson_timer', $params, $sort, $fields, $limitfrom, $limitnum);
3103 }
dbba944e
JL
3104
3105 /**
3106 * Check if the user is out of time in a timed lesson.
3107 *
3108 * @param stdClass $timer timer object
3109 * @return bool True if the user is on time, false is the user ran out of time
3110 * @since Moodle 3.3
3111 */
3112 public function check_time($timer) {
3113 if ($this->properties->timelimit) {
3114 $timeleft = $timer->starttime + $this->properties->timelimit - time();
3115 if ($timeleft <= 0) {
3116 // Out of time.
3117 $this->add_message(get_string('eolstudentoutoftime', 'lesson'));
3118 return false;
3119 } else if ($timeleft < 60) {
3120 // One minute warning.
3121 $this->add_message(get_string('studentoneminwarning', 'lesson'));
3122 }
3123 }
3124 return true;
3125 }
3126
3127 /**
3128 * Add different informative messages to the given page.
3129 *
3130 * @param lesson_page $page page object
3131 * @param reviewmode $bool whether we are in review mode or not
3132 * @since Moodle 3.3
3133 */
3134 public function add_messages_on_page_view(lesson_page $page, $reviewmode) {
3135 global $DB, $USER;
3136
3137 if (!$this->can_manage()) {
3138 if ($page->qtype == LESSON_PAGE_BRANCHTABLE && $this->properties->minquestions) {
3139 // Tell student how many questions they have seen, how many are required and their grade.
3140 $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3141 $gradeinfo = lesson_grade($this, $ntries);
3142 if ($gradeinfo->attempts) {
3143 if ($gradeinfo->nquestions < $this->properties->minquestions) {
3144 $a = new stdClass;
3145 $a->nquestions = $gradeinfo->nquestions;
3146 $a->minquestions = $this->properties->minquestions;
3147 $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
3148 }
3149
2034f57f 3150 if (!$reviewmode && $this->properties->ongoing) {
dbba944e
JL
3151 $this->add_message(get_string("numberofcorrectanswers", "lesson", $gradeinfo->earned), 'notify');
3152 if ($this->properties->grade != GRADE_TYPE_NONE) {
3153 $a = new stdClass;
3154 $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
3155 $a->total = $this->properties->grade;
3156 $this->add_message(get_string('yourcurrentgradeisoutof', 'lesson', $a), 'notify');
3157 }
3158 }
3159 }
3160 }
3161 } else {
3162 if ($this->properties->timelimit) {
3163 $this->add_message(get_string('teachertimerwarning', 'lesson'));
3164 }
3165 if (lesson_display_teacher_warning($this)) {
3166 // This is the warning msg for teachers to inform them that cluster
3167 // and unseen does not work while logged in as a teacher.
3168 $warningvars = new stdClass();
3169 $warningvars->cluster = get_string('clusterjump', 'lesson');
3170 $warningvars->unseen = get_string('unseenpageinbranch', 'lesson');
3171 $this->add_message(get_string('teacherjumpwarning', 'lesson', $warningvars));
3172 }
3173 }
3174 }
66cd7b8e
JL
3175
3176 /**
3177 * Get the ongoing score message for the user (depending on the user permission and lesson settings).
3178 *
3179 * @return str the ongoing score message
3180 * @since Moodle 3.3
3181 */
3182 public function get_ongoing_score_message() {
3183 global $USER, $DB;
3184
3185 $context = $this->get_context();
3186
3187 if (has_capability('mod/lesson:manage', $context)) {
3188 return get_string('teacherongoingwarning', 'lesson');
3189 } else {
3190 $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3191 if (isset($USER->modattempts[$this->properties->id])) {
3192 $ntries--;
3193 }
3194 $gradeinfo = lesson_grade($this, $ntries);
3195 $a = new stdClass;
3196 if ($this->properties->custom) {
3197 $a->score = $gradeinfo->earned;
3198 $a->currenthigh = $gradeinfo->total;
3199 return get_string("ongoingcustom", "lesson", $a);
3200 } else {
3201 $a->correct = $gradeinfo->earned;
3202 $a->viewed = $gradeinfo->attempts;
3203 return get_string("ongoingnormal", "lesson", $a);
3204 }
3205 }
3206 }
3207
3208 /**
3209 * Calculate the progress of the current user in the lesson.
3210 *
3211 * @return int the progress (scale 0-100)
3212 * @since Moodle 3.3
3213 */
3214 public function calculate_progress() {
3215 global $USER, $DB;
3216
3217 // Check if the user is reviewing the attempt.
3218 if (isset($USER->modattempts[$this->properties->id])) {
3219 return 100;
3220 }
3221
3222 // All of the lesson pages.
3223 $pages = $this->load_all_pages();
3224 foreach ($pages as $page) {
3225 if ($page->prevpageid == 0) {
3226 $pageid = $page->id; // Find the first page id.
3227 break;
3228 }
3229 }
3230
3231 // Current attempt number.
3232 if (!$ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id))) {
3233 $ntries = 0; // May not be necessary.
3234 }
3235
3236 $viewedpageids = array();
3237 if ($attempts = $this->get_attempts($ntries, false)) {
3238 foreach ($attempts as $attempt) {
3239 $viewedpageids[$attempt->pageid] = $attempt;
3240 }
3241 }
3242
3243 $viewedbranches = array();
3244 // Collect all of the branch tables viewed.
3245 if ($branches = $this->get_content_pages_viewed($ntries, $USER->id, 'timeseen ASC', 'id, pageid')) {
3246 foreach ($branches as $branch) {
3247 $viewedbranches[$branch->pageid] = $branch;
3248 }
3249 $viewedpageids = array_merge($viewedpageids, $viewedbranches);
3250 }
3251
3252 // Filter out the following pages:
3253 // - End of Cluster
3254 // - End of Branch
3255 // - Pages found inside of Clusters
3256 // Do not filter out Cluster Page(s) because we count a cluster as one.
3257 // By keeping the cluster page, we get our 1.
3258 $validpages = array();
3259 while ($pageid != 0) {
3260 $pageid = $pages[$pageid]->valid_page_and_view($validpages, $viewedpageids);
3261 }
3262
3263 // Progress calculation as a percent.
16ca026c
JL
3264 $progress = round(count($viewedpageids) / count($validpages), 2) * 100;
3265 return (int) $progress;
66cd7b8e
JL
3266 }
3267
3268 /**
3269 * Calculate the correct page and prepare contents for a given page id (could be a page jump id).
3270 *
3271 * @param int $pageid the given page id
3272 * @param mod_lesson_renderer $lessonoutput the lesson output rendered
3273 * @param bool $reviewmode whether we are in review mode or not
0259109f 3274 * @param bool $redirect Optional, default to true. Set to false to avoid redirection and return the page to redirect.
66cd7b8e
JL
3275 * @return array the page object and contents
3276 * @throws moodle_exception
3277 * @since Moodle 3.3
3278 */
0259109f 3279 public function prepare_page_and_contents($pageid, $lessonoutput, $reviewmode, $redirect = true) {
66cd7b8e
JL
3280 global $USER, $CFG;
3281
3282 $page = $this->load_page($pageid);
3283 // Check if the page is of a special type and if so take any nessecary action.
0259109f
JL
3284 $newpageid = $page->callback_on_view($this->can_manage(), $redirect);
3285
3286 // Avoid redirections returning the jump to special page id.
3287 if (!$redirect && is_numeric($newpageid) && $newpageid < 0) {
3288 return array($newpageid, null, null);
3289 }
3290
66cd7b8e
JL
3291 if (is_numeric($newpageid)) {
3292 $page = $this->load_page($newpageid);
3293 }
3294
3295 // Add different informative messages to the given page.
3296 $this->add_messages_on_page_view($page, $reviewmode);
3297
3298 if (is_array($page->answers) && count($page->answers) > 0) {
3299 // This is for modattempts option. Find the users previous answer to this page,
3300 // and then display it below in answer processing.
3301 if (isset($USER->modattempts[$this->properties->id])) {
3302 $retries = $this->count_user_retries($USER->id);
3303 if (!$attempts = $this->get_attempts($retries - 1, false, $page->id)) {
3304 throw new moodle_exception('cannotfindpreattempt', 'lesson');
3305 }
3306 $attempt = end($attempts);
3307 $USER->modattempts[$this->properties->id] = $attempt;
3308 } else {
3309 $attempt = false;
3310 }
3311 $lessoncontent = $lessonoutput->display_page($this, $page, $attempt);
3312 } else {
3313 require_once($CFG->dirroot . '/mod/lesson/view_form.php');
3314 $data = new stdClass;
3315 $data->id = $this->get_cm()->id;
3316 $data->pageid = $page->id;
3317 $data->newpageid = $this->get_next_page($page->nextpageid);
3318
3319 $customdata = array(
3320 'title' => $page->title,
3321 'contents' => $page->get_contents()
3322 );
3323 $mform = new lesson_page_without_answers($CFG->wwwroot.'/mod/lesson/continue.php', $customdata);
3324 $mform->set_data($data);
3325 ob_start();
3326 $mform->display();
3327 $lessoncontent = ob_get_contents();
3328 ob_end_clean();
3329 }
3330
0259109f 3331 return array($page->id, $page, $lessoncontent);
66cd7b8e 3332 }
61b51764
JL
3333
3334 /**
707d50d1 3335 * This returns a real page id to jump to (or LESSON_EOL) after processing page responses.
61b51764 3336 *
707d50d1
JL
3337 * @param lesson_page $page lesson page
3338 * @param int $newpageid the new page id
3339 * @return int the real page to jump to (or end of lesson)
61b51764
JL
3340 * @since Moodle 3.3
3341 */
707d50d1 3342 public function calculate_new_page_on_jump(lesson_page $page, $newpageid) {
61b51764
JL
3343 global $USER, $DB;
3344
3345 $canmanage = $this->can_manage();
61b51764 3346
707d50d1 3347 if (isset($USER->modattempts[$this->properties->id])) {
61b51764 3348 // Make sure if the student is reviewing, that he/she sees the same pages/page path that he/she saw the first time.
e1f88fe7 3349 if ($USER->modattempts[$this->properties->id]->pageid == $page->id && $page->nextpageid == 0) {
61b51764 3350 // Remember, this session variable holds the pageid of the last page that the user saw.
707d50d1 3351 $newpageid = LESSON_EOL;
61b51764 3352 } else {
e1f88fe7 3353 $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
61b51764 3354 $nretakes--; // Make sure we are looking at the right try.
e1f88fe7 3355 $attempts = $DB->get_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes), "timeseen", "id, pageid");
61b51764
JL
3356 $found = false;
3357 $temppageid = 0;
3358 // Make sure that the newpageid always defaults to something valid.
707d50d1 3359 $newpageid = LESSON_EOL;
61b51764
JL
3360 foreach ($attempts as $attempt) {
3361 if ($found && $temppageid != $attempt->pageid) {
3362 // Now try to find the next page, make sure next few attempts do no belong to current page.
707d50d1 3363 $newpageid = $attempt->pageid;
61b51764
JL
3364 break;
3365 }
3366 if ($attempt->pageid == $page->id) {
3367 $found = true; // If found current page.
3368 $temppageid = $attempt->pageid;
3369 }
3370 }
3371 }
707d50d1 3372 } else if ($newpageid != LESSON_CLUSTERJUMP && $page->id != 0 && $newpageid > 0) {
61b51764
JL
3373 // Going to check to see if the page that the user is going to view next, is a cluster page.
3374 // If so, dont display, go into the cluster.
707d50d1
JL
3375 // The $newpageid > 0 is used to filter out all of the negative code jumps.
3376 $newpage = $this->load_page($newpageid);
3377 if ($overridenewpageid = $newpage->override_next_page($newpageid)) {
3378 $newpageid = $overridenewpageid;
61b51764 3379 }
707d50d1 3380 } else if ($newpageid == LESSON_UNSEENBRANCHPAGE) {
61b51764
JL
3381 if ($canmanage) {
3382 if ($page->nextpageid == 0) {
707d50d1 3383 $newpageid = LESSON_EOL;
61b51764 3384 } else {
707d50d1 3385 $newpageid = $page->nextpageid;
61b51764
JL
3386 }
3387 } else {
707d50d1 3388 $newpageid = lesson_unseen_question_jump($this, $USER->id, $page->id);
61b51764 3389 }
707d50d1
JL
3390 } else if ($newpageid == LESSON_PREVIOUSPAGE) {
3391 $newpageid = $page->prevpageid;
3392 } else if ($newpageid == LESSON_RANDOMPAGE) {
3393 $newpageid = lesson_random_question_jump($this, $page->id);
3394 } else if ($newpageid == LESSON_CLUSTERJUMP) {
61b51764
JL
3395 if ($canmanage) {
3396 if ($page->nextpageid == 0) { // If teacher, go to next page.
707d50d1 3397 $newpageid = LESSON_EOL;
61b51764 3398 } else {
707d50d1 3399 $newpageid = $page->nextpageid;
61b51764
JL
3400 }
3401 } else {
707d50d1 3402 $newpageid = $this->cluster_jump($page->id);
61b51764 3403 }
707d50d1
JL
3404 } else if ($newpageid == 0) {
3405 $newpageid = $page->id;
3406 } else if ($newpageid == LESSON_NEXTPAGE) {
3407 $newpageid = $this->get_next_page($page->nextpageid);
61b51764 3408 }
707d50d1
JL
3409
3410 return $newpageid;
3411 }
3412
3413 /**
3414 * Process page responses.
3415 *
3416 * @param lesson_page $page page object
3417 * @since Moodle 3.3
3418 */
3419 public function process_page_responses(lesson_page $page) {
3420 $context = $this->get_context();
3421
3422 // Check the page has answers [MDL-25632].
3423 if (count($page->answers) > 0) {
3424 $result = $page->record_attempt($context);
3425 } else {
3426 // The page has no answers so we will just progress to the next page in the
3427 // sequence (as set by newpageid).
3428 $result = new stdClass;
3429 $result->newpageid = optional_param('newpageid', $page->nextpageid, PARAM_INT);
3430 $result->nodefaultresponse = true;
e7a11c20 3431 $result->inmediatejump = false;
707d50d1
JL
3432 }
3433
3434 if ($result->inmediatejump) {
3435 return $result;
3436 }
3437
3438 $result->newpageid = $this->calculate_new_page_on_jump($page, $result->newpageid);
3439
61b51764
JL
3440 return $result;
3441 }
3442
3443 /**
3444 * Add different informative messages to the given page.
3445 *
3446 * @param lesson_page $page page object
3447 * @param stdClass $result the page processing result object
3448 * @param bool $reviewmode whether we are in review mode or not
3449 * @since Moodle 3.3
3450 */
3451 public function add_messages_on_page_process(lesson_page $page, $result, $reviewmode) {
3452
3453 if ($this->can_manage()) {
3454 // This is the warning msg for teachers to inform them that cluster and unseen does not work while logged in as a teacher.
3455 if (lesson_display_teacher_warning($this)) {
3456 $warningvars = new stdClass();
3457 $warningvars->cluster = get_string("clusterjump", "lesson");
3458 $warningvars->unseen = get_string("unseenpageinbranch", "lesson");
3459 $this->add_message(get_string("teacherjumpwarning", "lesson", $warningvars));
3460 }
3461 // Inform teacher that s/he will not see the timer.
3462 if ($this->properties->timelimit) {
d96d7295 3463 $this->add_message(get_string("teachertimerwarning", "lesson"));
61b51764
JL
3464 }
3465 }
3466 // Report attempts remaining.
3467 if ($result->attemptsremaining != 0 && $this->properties->review && !$reviewmode) {
3468 $this->add_message(get_string('attemptsremaining', 'lesson', $result->attemptsremaining));
3469 }
3470 }
dfcabd3b
JL
3471
3472 /**
3473 * Process and return all the information for the end of lesson page.
3474 *
3475 * @param string $outoftime used to check to see if the student ran out of time
3476 * @return stdclass an object with all the page data ready for rendering
3477 * @since Moodle 3.3
3478 */
3479 public function process_eol_page($outoftime) {
3480 global $DB, $USER;
3481
3482 $course = $this->get_courserecord();
3483 $cm = $this->get_cm();
3484 $canmanage = $this->can_manage();
3485
3486 // Init all the possible fields and values.
3487 $data = (object) array(
3488 'gradelesson' => true,
3489 'notenoughtimespent' => false,
3490 'numberofpagesviewed' => false,
3491 'youshouldview' => false,
3492 'numberofcorrectanswers' => false,
3493 'displayscorewithessays' => false,
3494 'displayscorewithoutessays' => false,
3495 'yourcurrentgradeisoutof' => false,
3496 'eolstudentoutoftimenoanswers' => false,
3497 'welldone' => false,
3498 'progressbar' => false,
3499 'displayofgrade' => false,
3500 'reviewlesson' => false,
3501 'modattemptsnoteacher' => false,
3502 'activitylink' => false,
f3d9512d 3503 'progresscompleted' => false,
dfcabd3b
JL
3504 );
3505
3506 $ntries = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id));
3507 if (isset($USER->modattempts[$this->properties->id])) {
3508 $ntries--; // Need to look at the old attempts :).
3509 }
3510
3511 $gradeinfo = lesson_grade($this, $ntries);
3512 $data->gradeinfo = $gradeinfo;
3513 if ($this->properties->custom && !$canmanage) {
3514 // Before we calculate the custom score make sure they answered the minimum
3515 // number of questions. We only need to do this for custom scoring as we can
3516 // not get the miniumum score the user should achieve. If we are not using
3517 // custom scoring (so all questions are valued as 1) then we simply check if
3518 // they answered more than the minimum questions, if not, we mark it out of the
3519 // number specified in the minimum questions setting - which is done in lesson_grade().
3520 // Get the number of answers given.
3521 if ($gradeinfo->nquestions < $this->properties->minquestions) {
3522 $data->gradelesson = false;
3523 $a = new stdClass;
3524 $a->nquestions = $gradeinfo->nquestions;
3525 $a->minquestions = $this->properties->minquestions;
3526 $this->add_message(get_string('numberofpagesviewednotice', 'lesson', $a));
3527 }
3528 }
3529
3530 if (!$canmanage) {
3531 if ($data->gradelesson) {
3532 // Store this now before any modifications to pages viewed.
3533 $progresscompleted = $this->calculate_progress();
3534
3535 // Update the clock / get time information for this user.
3536 $this->stop_timer();
3537
3538 // Update completion state.
3539 $completion = new completion_info($course);
3540 if ($completion->is_enabled($cm) && $this->properties->completionendreached) {
3541 $completion->update_state($cm, COMPLETION_COMPLETE);
3542 }
3543
3544 if ($this->properties->completiontimespent > 0) {
3545 $duration = $DB->get_field_sql(
3546 "SELECT SUM(lessontime - starttime)
3547 FROM {lesson_timer}
3548 WHERE lessonid = :lessonid
3549 AND userid = :userid",
3550 array('userid' => $USER->id, 'lessonid' => $this->properties->id));
3551 if (!$duration) {
3552 $duration = 0;
3553 }
3554
3555 // If student has not spend enough time in the lesson, display a message.
3556 if ($duration < $this->properties->completiontimespent) {
3557 $a = new stdClass;
3558 $a->timespentraw = $duration;
3559 $a->timespent = format_time($duration);
3560 $a->timerequiredraw = $this->properties->completiontimespent;
3561 $a->timerequired = format_time($this->properties->completiontimespent);
3562 $data->notenoughtimespent = $a;
3563 }
3564 }
3565
3566 if ($gradeinfo->attempts) {
3567 if (!$this->properties->custom) {
3568 $data->numberofpagesviewed = $gradeinfo->nquestions;
3569 if ($this->properties->minquestions) {
3570 if ($gradeinfo->nquestions < $this->properties->minquestions) {
3571 $data->youshouldview = $this->properties->minquestions;
3572 }
3573 }
3574 $data->numberofcorrectanswers = $gradeinfo->earned;
3575 }
3576 $a = new stdClass;
3577 $a->score = $gradeinfo->earned;
3578 $a->grade = $gradeinfo->total;
3579 if ($gradeinfo->nmanual) {
3580 $a->tempmaxgrade = $gradeinfo->total - $gradeinfo->manualpoints;
3581 $a->essayquestions = $gradeinfo->nmanual;
3582 $data->displayscorewithessays = $a;
3583 } else {
3584 $data->displayscorewithoutessays = $a;
3585 }
3586 if ($this->properties->grade != GRADE_TYPE_NONE) {
3587 $a = new stdClass;
3588 $a->grade = number_format($gradeinfo->grade * $this->properties->grade / 100, 1);
3589 $a->total = $this->properties->grade;
3590 $data->yourcurrentgradeisoutof = $a;
3591 }
3592
3593 $grade = new stdClass();