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