[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 ////////////////////////////////////////////////////////////////////////////////////// 65 /// Any other lesson functions go here. Each of them must have a name that 66 /// starts with lesson_ 67 68 /** 69 * Checks to see if a LESSON_CLUSTERJUMP or 70 * a LESSON_UNSEENBRANCHPAGE is used in a lesson. 71 * 72 * This function is only executed when a teacher is 73 * checking the navigation for a lesson. 74 * 75 * @param stdClass $lesson Id of the lesson that is to be checked. 76 * @return boolean True or false. 77 **/ 78 function lesson_display_teacher_warning($lesson) { 79 global $DB; 80 81 // get all of the lesson answers 82 $params = array ("lessonid" => $lesson->id); 83 if (!$lessonanswers = $DB->get_records_select("lesson_answers", "lessonid = :lessonid", $params)) { 84 // no answers, then not using cluster or unseen 85 return false; 86 } 87 // just check for the first one that fulfills the requirements 88 foreach ($lessonanswers as $lessonanswer) { 89 if ($lessonanswer->jumpto == LESSON_CLUSTERJUMP || $lessonanswer->jumpto == LESSON_UNSEENBRANCHPAGE) { 90 return true; 91 } 92 } 93 94 // if no answers use either of the two jumps 95 return false; 96 } 97 98 /** 99 * Interprets the LESSON_UNSEENBRANCHPAGE jump. 100 * 101 * will return the pageid of a random unseen page that is within a branch 102 * 103 * @param lesson $lesson 104 * @param int $userid Id of the user. 105 * @param int $pageid Id of the page from which we are jumping. 106 * @return int Id of the next page. 107 **/ 108 function lesson_unseen_question_jump($lesson, $user, $pageid) { 109 global $DB; 110 111 // get the number of retakes 112 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$user))) { 113 $retakes = 0; 114 } 115 116 // get all the lesson_attempts aka what the user has seen 117 if ($viewedpages = $DB->get_records("lesson_attempts", array("lessonid"=>$lesson->id, "userid"=>$user, "retry"=>$retakes), "timeseen DESC")) { 118 foreach($viewedpages as $viewed) { 119 $seenpages[] = $viewed->pageid; 120 } 121 } else { 122 $seenpages = array(); 123 } 124 125 // get the lesson pages 126 $lessonpages = $lesson->load_all_pages(); 127 128 if ($pageid == LESSON_UNSEENBRANCHPAGE) { // this only happens when a student leaves in the middle of an unseen question within a branch series 129 $pageid = $seenpages[0]; // just change the pageid to the last page viewed inside the branch table 130 } 131 132 // go up the pages till branch table 133 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page 134 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { 135 break; 136 } 137 $pageid = $lessonpages[$pageid]->prevpageid; 138 } 139 140 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); 141 142 // this foreach loop stores all the pages that are within the branch table but are not in the $seenpages array 143 $unseen = array(); 144 foreach($pagesinbranch as $page) { 145 if (!in_array($page->id, $seenpages)) { 146 $unseen[] = $page->id; 147 } 148 } 149 150 if(count($unseen) == 0) { 151 if(isset($pagesinbranch)) { 152 $temp = end($pagesinbranch); 153 $nextpage = $temp->nextpageid; // they have seen all the pages in the branch, so go to EOB/next branch table/EOL 154 } else { 155 // there are no pages inside the branch, so return the next page 156 $nextpage = $lessonpages[$pageid]->nextpageid; 157 } 158 if ($nextpage == 0) { 159 return LESSON_EOL; 160 } else { 161 return $nextpage; 162 } 163 } else { 164 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page 165 } 166 } 167 168 /** 169 * Handles the unseen branch table jump. 170 * 171 * @param lesson $lesson 172 * @param int $userid User id. 173 * @return int Will return the page id of a branch table or end of lesson 174 **/ 175 function lesson_unseen_branch_jump($lesson, $userid) { 176 global $DB; 177 178 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$lesson->id, "userid"=>$userid))) { 179 $retakes = 0; 180 } 181 182 $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $retakes); 183 if (!$seenbranches = $DB->get_records_select("lesson_branch", "lessonid = :lessonid AND userid = :userid AND retry = :retry", $params, 184 "timeseen DESC")) { 185 print_error('cannotfindrecords', 'lesson'); 186 } 187 188 // get the lesson pages 189 $lessonpages = $lesson->load_all_pages(); 190 191 // this loads all the viewed branch tables into $seen until it finds the branch table with the flag 192 // which is the branch table that starts the unseenbranch function 193 $seen = array(); 194 foreach ($seenbranches as $seenbranch) { 195 if (!$seenbranch->flag) { 196 $seen[$seenbranch->pageid] = $seenbranch->pageid; 197 } else { 198 $start = $seenbranch->pageid; 199 break; 200 } 201 } 202 // this function searches through the lesson pages to find all the branch tables 203 // that follow the flagged branch table 204 $pageid = $lessonpages[$start]->nextpageid; // move down from the flagged branch table 205 $branchtables = array(); 206 while ($pageid != 0) { // grab all of the branch table till eol 207 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { 208 $branchtables[] = $lessonpages[$pageid]->id; 209 } 210 $pageid = $lessonpages[$pageid]->nextpageid; 211 } 212 $unseen = array(); 213 foreach ($branchtables as $branchtable) { 214 // load all of the unseen branch tables into unseen 215 if (!array_key_exists($branchtable, $seen)) { 216 $unseen[] = $branchtable; 217 } 218 } 219 if (count($unseen) > 0) { 220 return $unseen[rand(0, count($unseen)-1)]; // returns a random page id for the next page 221 } else { 222 return LESSON_EOL; // has viewed all of the branch tables 223 } 224 } 225 226 /** 227 * Handles the random jump between a branch table and end of branch or end of lesson (LESSON_RANDOMPAGE). 228 * 229 * @param lesson $lesson 230 * @param int $pageid The id of the page that we are jumping from (?) 231 * @return int The pageid of a random page that is within a branch table 232 **/ 233 function lesson_random_question_jump($lesson, $pageid) { 234 global $DB; 235 236 // get the lesson pages 237 $params = array ("lessonid" => $lesson->id); 238 if (!$lessonpages = $DB->get_records_select("lesson_pages", "lessonid = :lessonid", $params)) { 239 print_error('cannotfindpages', 'lesson'); 240 } 241 242 // go up the pages till branch table 243 while ($pageid != 0) { // this condition should never be satisfied... only happens if there are no branch tables above this page 244 245 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_BRANCHTABLE) { 246 break; 247 } 248 $pageid = $lessonpages[$pageid]->prevpageid; 249 } 250 251 // get the pages within the branch 252 $pagesinbranch = $lesson->get_sub_pages_of($pageid, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); 253 254 if(count($pagesinbranch) == 0) { 255 // there are no pages inside the branch, so return the next page 256 return $lessonpages[$pageid]->nextpageid; 257 } else { 258 return $pagesinbranch[rand(0, count($pagesinbranch)-1)]->id; // returns a random page id for the next page 259 } 260 } 261 262 /** 263 * Calculates a user's grade for a lesson. 264 * 265 * @param object $lesson The lesson that the user is taking. 266 * @param int $retries The attempt number. 267 * @param int $userid Id of the user (optional, default current user). 268 * @return object { nquestions => number of questions answered 269 attempts => number of question attempts 270 total => max points possible 271 earned => points earned by student 272 grade => calculated percentage grade 273 nmanual => number of manually graded questions 274 manualpoints => point value for manually graded questions } 275 */ 276 function lesson_grade($lesson, $ntries, $userid = 0) { 277 global $USER, $DB; 278 279 if (empty($userid)) { 280 $userid = $USER->id; 281 } 282 283 // Zero out everything 284 $ncorrect = 0; 285 $nviewed = 0; 286 $score = 0; 287 $nmanual = 0; 288 $manualpoints = 0; 289 $thegrade = 0; 290 $nquestions = 0; 291 $total = 0; 292 $earned = 0; 293 294 $params = array ("lessonid" => $lesson->id, "userid" => $userid, "retry" => $ntries); 295 if ($useranswers = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND 296 userid = :userid AND retry = :retry", $params, "timeseen")) { 297 // group each try with its page 298 $attemptset = array(); 299 foreach ($useranswers as $useranswer) { 300 $attemptset[$useranswer->pageid][] = $useranswer; 301 } 302 303 // Drop all attempts that go beyond max attempts for the lesson 304 foreach ($attemptset as $key => $set) { 305 $attemptset[$key] = array_slice($set, 0, $lesson->maxattempts); 306 } 307 308 // get only the pages and their answers that the user answered 309 list($usql, $parameters) = $DB->get_in_or_equal(array_keys($attemptset)); 310 array_unshift($parameters, $lesson->id); 311 $pages = $DB->get_records_select("lesson_pages", "lessonid = ? AND id $usql", $parameters); 312 $answers = $DB->get_records_select("lesson_answers", "lessonid = ? AND pageid $usql", $parameters); 313 314 // Number of pages answered 315 $nquestions = count($pages); 316 317 foreach ($attemptset as $attempts) { 318 $page = lesson_page::load($pages[end($attempts)->pageid], $lesson); 319 if ($lesson->custom) { 320 $attempt = end($attempts); 321 // If essay question, handle it, otherwise add to score 322 if ($page->requires_manual_grading()) { 323 $useranswerobj = unserialize($attempt->useranswer); 324 if (isset($useranswerobj->score)) { 325 $earned += $useranswerobj->score; 326 } 327 $nmanual++; 328 $manualpoints += $answers[$attempt->answerid]->score; 329 } else if (!empty($attempt->answerid)) { 330 $earned += $page->earned_score($answers, $attempt); 331 } 332 } else { 333 foreach ($attempts as $attempt) { 334 $earned += $attempt->correct; 335 } 336 $attempt = end($attempts); // doesn't matter which one 337 // If essay question, increase numbers 338 if ($page->requires_manual_grading()) { 339 $nmanual++; 340 $manualpoints++; 341 } 342 } 343 // Number of times answered 344 $nviewed += count($attempts); 345 } 346 347 if ($lesson->custom) { 348 $bestscores = array(); 349 // Find the highest possible score per page to get our total 350 foreach ($answers as $answer) { 351 if(!isset($bestscores[$answer->pageid])) { 352 $bestscores[$answer->pageid] = $answer->score; 353 } else if ($bestscores[$answer->pageid] < $answer->score) { 354 $bestscores[$answer->pageid] = $answer->score; 355 } 356 } 357 $total = array_sum($bestscores); 358 } else { 359 // Check to make sure the student has answered the minimum questions 360 if ($lesson->minquestions and $nquestions < $lesson->minquestions) { 361 // Nope, increase number viewed by the amount of unanswered questions 362 $total = $nviewed + ($lesson->minquestions - $nquestions); 363 } else { 364 $total = $nviewed; 365 } 366 } 367 } 368 369 if ($total) { // not zero 370 $thegrade = round(100 * $earned / $total, 5); 371 } 372 373 // Build the grade information object 374 $gradeinfo = new stdClass; 375 $gradeinfo->nquestions = $nquestions; 376 $gradeinfo->attempts = $nviewed; 377 $gradeinfo->total = $total; 378 $gradeinfo->earned = $earned; 379 $gradeinfo->grade = $thegrade; 380 $gradeinfo->nmanual = $nmanual; 381 $gradeinfo->manualpoints = $manualpoints; 382 383 return $gradeinfo; 384 } 385 386 /** 387 * Determines if a user can view the left menu. The determining factor 388 * is whether a user has a grade greater than or equal to the lesson setting 389 * of displayleftif 390 * 391 * @param object $lesson Lesson object of the current lesson 392 * @return boolean 0 if the user cannot see, or $lesson->displayleft to keep displayleft unchanged 393 **/ 394 function lesson_displayleftif($lesson) { 395 global $CFG, $USER, $DB; 396 397 if (!empty($lesson->displayleftif)) { 398 // get the current user's max grade for this lesson 399 $params = array ("userid" => $USER->id, "lessonid" => $lesson->id); 400 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)) { 401 if ($maxgrade->maxgrade < $lesson->displayleftif) { 402 return 0; // turn off the displayleft 403 } 404 } else { 405 return 0; // no grades 406 } 407 } 408 409 // if we get to here, keep the original state of displayleft lesson setting 410 return $lesson->displayleft; 411 } 412 413 /** 414 * 415 * @param $cm 416 * @param $lesson 417 * @param $page 418 * @return unknown_type 419 */ 420 function lesson_add_fake_blocks($page, $cm, $lesson, $timer = null) { 421 $bc = lesson_menu_block_contents($cm->id, $lesson); 422 if (!empty($bc)) { 423 $regions = $page->blocks->get_regions(); 424 $firstregion = reset($regions); 425 $page->blocks->add_fake_block($bc, $firstregion); 426 } 427 428 $bc = lesson_mediafile_block_contents($cm->id, $lesson); 429 if (!empty($bc)) { 430 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region()); 431 } 432 433 if (!empty($timer)) { 434 $bc = lesson_clock_block_contents($cm->id, $lesson, $timer, $page); 435 if (!empty($bc)) { 436 $page->blocks->add_fake_block($bc, $page->blocks->get_default_region()); 437 } 438 } 439 } 440 441 /** 442 * If there is a media file associated with this 443 * lesson, return a block_contents that displays it. 444 * 445 * @param int $cmid Course Module ID for this lesson 446 * @param object $lesson Full lesson record object 447 * @return block_contents 448 **/ 449 function lesson_mediafile_block_contents($cmid, $lesson) { 450 global $OUTPUT; 451 if (empty($lesson->mediafile)) { 452 return null; 453 } 454 455 $options = array(); 456 $options['menubar'] = 0; 457 $options['location'] = 0; 458 $options['left'] = 5; 459 $options['top'] = 5; 460 $options['scrollbars'] = 1; 461 $options['resizable'] = 1; 462 $options['width'] = $lesson->mediawidth; 463 $options['height'] = $lesson->mediaheight; 464 465 $link = new moodle_url('/mod/lesson/mediafile.php?id='.$cmid); 466 $action = new popup_action('click', $link, 'lessonmediafile', $options); 467 $content = $OUTPUT->action_link($link, get_string('mediafilepopup', 'lesson'), $action, array('title'=>get_string('mediafilepopup', 'lesson'))); 468 469 $bc = new block_contents(); 470 $bc->title = get_string('linkedmedia', 'lesson'); 471 $bc->attributes['class'] = 'mediafile block'; 472 $bc->content = $content; 473 474 return $bc; 475 } 476 477 /** 478 * If a timed lesson and not a teacher, then 479 * return a block_contents containing the clock. 480 * 481 * @param int $cmid Course Module ID for this lesson 482 * @param object $lesson Full lesson record object 483 * @param object $timer Full timer record object 484 * @return block_contents 485 **/ 486 function lesson_clock_block_contents($cmid, $lesson, $timer, $page) { 487 // Display for timed lessons and for students only 488 $context = context_module::instance($cmid); 489 if ($lesson->timelimit == 0 || has_capability('mod/lesson:manage', $context)) { 490 return null; 491 } 492 493 $content = '<div id="lesson-timer">'; 494 $content .= $lesson->time_remaining($timer->starttime); 495 $content .= '</div>'; 496 497 $clocksettings = array('starttime' => $timer->starttime, 'servertime' => time(), 'testlength' => $lesson->timelimit); 498 $page->requires->data_for_js('clocksettings', $clocksettings, true); 499 $page->requires->strings_for_js(array('timeisup'), 'lesson'); 500 $page->requires->js('/mod/lesson/timer.js'); 501 $page->requires->js_init_call('show_clock'); 502 503 $bc = new block_contents(); 504 $bc->title = get_string('timeremaining', 'lesson'); 505 $bc->attributes['class'] = 'clock block'; 506 $bc->content = $content; 507 508 return $bc; 509 } 510 511 /** 512 * If left menu is turned on, then this will 513 * print the menu in a block 514 * 515 * @param int $cmid Course Module ID for this lesson 516 * @param lesson $lesson Full lesson record object 517 * @return void 518 **/ 519 function lesson_menu_block_contents($cmid, $lesson) { 520 global $CFG, $DB; 521 522 if (!$lesson->displayleft) { 523 return null; 524 } 525 526 $pages = $lesson->load_all_pages(); 527 foreach ($pages as $page) { 528 if ((int)$page->prevpageid === 0) { 529 $pageid = $page->id; 530 break; 531 } 532 } 533 $currentpageid = optional_param('pageid', $pageid, PARAM_INT); 534 535 if (!$pageid || !$pages) { 536 return null; 537 } 538 539 $content = '<a href="#maincontent" class="skip">'.get_string('skip', 'lesson')."</a>\n<div class=\"menuwrapper\">\n<ul>\n"; 540 541 while ($pageid != 0) { 542 $page = $pages[$pageid]; 543 544 // Only process branch tables with display turned on 545 if ($page->displayinmenublock && $page->display) { 546 if ($page->id == $currentpageid) { 547 $content .= '<li class="selected">'.format_string($page->title,true)."</li>\n"; 548 } else { 549 $content .= "<li class=\"notselected\"><a href=\"$CFG->wwwroot/mod/lesson/view.php?id=$cmid&pageid=$page->id\">".format_string($page->title,true)."</a></li>\n"; 550 } 551 552 } 553 $pageid = $page->nextpageid; 554 } 555 $content .= "</ul>\n</div>\n"; 556 557 $bc = new block_contents(); 558 $bc->title = get_string('lessonmenu', 'lesson'); 559 $bc->attributes['class'] = 'menu block'; 560 $bc->content = $content; 561 562 return $bc; 563 } 564 565 /** 566 * Adds header buttons to the page for the lesson 567 * 568 * @param object $cm 569 * @param object $context 570 * @param bool $extraeditbuttons 571 * @param int $lessonpageid 572 */ 573 function lesson_add_header_buttons($cm, $context, $extraeditbuttons=false, $lessonpageid=null) { 574 global $CFG, $PAGE, $OUTPUT; 575 if (has_capability('mod/lesson:edit', $context) && $extraeditbuttons) { 576 if ($lessonpageid === null) { 577 print_error('invalidpageid', 'lesson'); 578 } 579 if (!empty($lessonpageid) && $lessonpageid != LESSON_EOL) { 580 $url = new moodle_url('/mod/lesson/editpage.php', array( 581 'id' => $cm->id, 582 'pageid' => $lessonpageid, 583 'edit' => 1, 584 'returnto' => $PAGE->url->out(false) 585 )); 586 $PAGE->set_button($OUTPUT->single_button($url, get_string('editpagecontent', 'lesson'))); 587 } 588 } 589 } 590 591 /** 592 * This is a function used to detect media types and generate html code. 593 * 594 * @global object $CFG 595 * @global object $PAGE 596 * @param object $lesson 597 * @param object $context 598 * @return string $code the html code of media 599 */ 600 function lesson_get_media_html($lesson, $context) { 601 global $CFG, $PAGE, $OUTPUT; 602 require_once("$CFG->libdir/resourcelib.php"); 603 604 // get the media file link 605 if (strpos($lesson->mediafile, '://') !== false) { 606 $url = new moodle_url($lesson->mediafile); 607 } else { 608 // the timemodified is used to prevent caching problems, instead of '/' we should better read from files table and use sortorder 609 $url = moodle_url::make_pluginfile_url($context->id, 'mod_lesson', 'mediafile', $lesson->timemodified, '/', ltrim($lesson->mediafile, '/')); 610 } 611 $title = $lesson->mediafile; 612 613 $clicktoopen = html_writer::link($url, get_string('download')); 614 615 $mimetype = resourcelib_guess_url_mimetype($url); 616 617 $extension = resourcelib_get_extension($url->out(false)); 618 619 $mediarenderer = $PAGE->get_renderer('core', 'media'); 620 $embedoptions = array( 621 core_media::OPTION_TRUSTED => true, 622 core_media::OPTION_BLOCK => true 623 ); 624 625 // find the correct type and print it out 626 if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image 627 $code = resourcelib_embed_image($url, $title); 628 629 } else if ($mediarenderer->can_embed_url($url, $embedoptions)) { 630 // Media (audio/video) file. 631 $code = $mediarenderer->embed_url($url, $title, 0, 0, $embedoptions); 632 633 } else { 634 // anything else - just try object tag enlarged as much as possible 635 $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype); 636 } 637 638 return $code; 639 } 640 641 /** 642 * Logic to happen when a/some group(s) has/have been deleted in a course. 643 * 644 * @param int $courseid The course ID. 645 * @param int $groupid The group id if it is known 646 * @return void 647 */ 648 function lesson_process_group_deleted_in_course($courseid, $groupid = null) { 649 global $DB; 650 651 $params = array('courseid' => $courseid); 652 if ($groupid) { 653 $params['groupid'] = $groupid; 654 // We just update the group that was deleted. 655 $sql = "SELECT o.id, o.lessonid 656 FROM {lesson_overrides} o 657 JOIN {lesson} lesson ON lesson.id = o.lessonid 658 WHERE lesson.course = :courseid 659 AND o.groupid = :groupid"; 660 } else { 661 // No groupid, we update all orphaned group overrides for all lessons in course. 662 $sql = "SELECT o.id, o.lessonid 663 FROM {lesson_overrides} o 664 JOIN {lesson} lesson ON lesson.id = o.lessonid 665 LEFT JOIN {groups} grp ON grp.id = o.groupid 666 WHERE lesson.course = :courseid 667 AND o.groupid IS NOT NULL 668 AND grp.id IS NULL"; 669 } 670 $records = $DB->get_records_sql_menu($sql, $params); 671 if (!$records) { 672 return; // Nothing to do. 673 } 674 $DB->delete_records_list('lesson_overrides', 'id', array_keys($records)); 675 } 676 677 /** 678 * Abstract class that page type's MUST inherit from. 679 * 680 * This is the abstract class that ALL add page type forms must extend. 681 * You will notice that all but two of the methods this class contains are final. 682 * Essentially the only thing that extending classes can do is extend custom_definition. 683 * OR if it has a special requirement on creation it can extend construction_override 684 * 685 * @abstract 686 * @copyright 2009 Sam Hemelryk 687 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 688 */ 689 abstract class lesson_add_page_form_base extends moodleform { 690 691 /** 692 * This is the classic define that is used to identify this pagetype. 693 * Will be one of LESSON_* 694 * @var int 695 */ 696 public $qtype; 697 698 /** 699 * The simple string that describes the page type e.g. truefalse, multichoice 700 * @var string 701 */ 702 public $qtypestring; 703 704 /** 705 * An array of options used in the htmleditor 706 * @var array 707 */ 708 protected $editoroptions = array(); 709 710 /** 711 * True if this is a standard page of false if it does something special. 712 * Questions are standard pages, branch tables are not 713 * @var bool 714 */ 715 protected $standard = true; 716 717 /** 718 * Answer format supported by question type. 719 */ 720 protected $answerformat = ''; 721 722 /** 723 * Response format supported by question type. 724 */ 725 protected $responseformat = ''; 726 727 /** 728 * Each page type can and should override this to add any custom elements to 729 * the basic form that they want 730 */ 731 public function custom_definition() {} 732 733 /** 734 * Returns answer format used by question type. 735 */ 736 public function get_answer_format() { 737 return $this->answerformat; 738 } 739 740 /** 741 * Returns response format used by question type. 742 */ 743 public function get_response_format() { 744 return $this->responseformat; 745 } 746 747 /** 748 * Used to determine if this is a standard page or a special page 749 * @return bool 750 */ 751 public final function is_standard() { 752 return (bool)$this->standard; 753 } 754 755 /** 756 * Add the required basic elements to the form. 757 * 758 * This method adds the basic elements to the form including title and contents 759 * and then calls custom_definition(); 760 */ 761 public final function definition() { 762 $mform = $this->_form; 763 $editoroptions = $this->_customdata['editoroptions']; 764 765 $mform->addElement('header', 'qtypeheading', get_string('createaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson'))); 766 767 if (!empty($this->_customdata['returnto'])) { 768 $mform->addElement('hidden', 'returnto', $this->_customdata['returnto']); 769 $mform->setType('returnto', PARAM_URL); 770 } 771 772 $mform->addElement('hidden', 'id'); 773 $mform->setType('id', PARAM_INT); 774 775 $mform->addElement('hidden', 'pageid'); 776 $mform->setType('pageid', PARAM_INT); 777 778 if ($this->standard === true) { 779 $mform->addElement('hidden', 'qtype'); 780 $mform->setType('qtype', PARAM_INT); 781 782 $mform->addElement('text', 'title', get_string('pagetitle', 'lesson'), array('size'=>70)); 783 $mform->setType('title', PARAM_TEXT); 784 $mform->addRule('title', get_string('required'), 'required', null, 'client'); 785 786 $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']); 787 $mform->addElement('editor', 'contents_editor', get_string('pagecontents', 'lesson'), null, $this->editoroptions); 788 $mform->setType('contents_editor', PARAM_RAW); 789 $mform->addRule('contents_editor', get_string('required'), 'required', null, 'client'); 790 } 791 792 $this->custom_definition(); 793 794 if ($this->_customdata['edit'] === true) { 795 $mform->addElement('hidden', 'edit', 1); 796 $mform->setType('edit', PARAM_BOOL); 797 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson')); 798 } else if ($this->qtype === 'questiontype') { 799 $this->add_action_buttons(get_string('cancel'), get_string('addaquestionpage', 'lesson')); 800 } else { 801 $this->add_action_buttons(get_string('cancel'), get_string('savepage', 'lesson')); 802 } 803 } 804 805 /** 806 * Convenience function: Adds a jumpto select element 807 * 808 * @param string $name 809 * @param string|null $label 810 * @param int $selected The page to select by default 811 */ 812 protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) { 813 $title = get_string("jump", "lesson"); 814 if ($label === null) { 815 $label = $title; 816 } 817 if (is_int($name)) { 818 $name = "jumpto[$name]"; 819 } 820 $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']); 821 $this->_form->setDefault($name, $selected); 822 $this->_form->addHelpButton($name, 'jumps', 'lesson'); 823 } 824 825 /** 826 * Convenience function: Adds a score input element 827 * 828 * @param string $name 829 * @param string|null $label 830 * @param mixed $value The default value 831 */ 832 protected final function add_score($name, $label=null, $value=null) { 833 if ($label === null) { 834 $label = get_string("score", "lesson"); 835 } 836 837 if (is_int($name)) { 838 $name = "score[$name]"; 839 } 840 $this->_form->addElement('text', $name, $label, array('size'=>5)); 841 $this->_form->setType($name, PARAM_INT); 842 if ($value !== null) { 843 $this->_form->setDefault($name, $value); 844 } 845 $this->_form->addHelpButton($name, 'score', 'lesson'); 846 847 // Score is only used for custom scoring. Disable the element when not in use to stop some confusion. 848 if (!$this->_customdata['lesson']->custom) { 849 $this->_form->freeze($name); 850 } 851 } 852 853 /** 854 * Convenience function: Adds an answer editor 855 * 856 * @param int $count The count of the element to add 857 * @param string $label, null means default 858 * @param bool $required 859 * @param string $format 860 * @return void 861 */ 862 protected final function add_answer($count, $label = null, $required = false, $format= '') { 863 if ($label === null) { 864 $label = get_string('answer', 'lesson'); 865 } 866 867 if ($format == LESSON_ANSWER_HTML) { 868 $this->_form->addElement('editor', 'answer_editor['.$count.']', $label, 869 array('rows' => '4', 'columns' => '80'), 870 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes'])); 871 $this->_form->setType('answer_editor['.$count.']', PARAM_RAW); 872 $this->_form->setDefault('answer_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML)); 873 } else { 874 $this->_form->addElement('text', 'answer_editor['.$count.']', $label, 875 array('size' => '50', 'maxlength' => '200')); 876 $this->_form->setType('answer_editor['.$count.']', PARAM_TEXT); 877 } 878 879 if ($required) { 880 $this->_form->addRule('answer_editor['.$count.']', get_string('required'), 'required', null, 'client'); 881 } 882 } 883 /** 884 * Convenience function: Adds an response editor 885 * 886 * @param int $count The count of the element to add 887 * @param string $label, null means default 888 * @param bool $required 889 * @return void 890 */ 891 protected final function add_response($count, $label = null, $required = false) { 892 if ($label === null) { 893 $label = get_string('response', 'lesson'); 894 } 895 $this->_form->addElement('editor', 'response_editor['.$count.']', $label, 896 array('rows' => '4', 'columns' => '80'), 897 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $this->_customdata['maxbytes'])); 898 $this->_form->setType('response_editor['.$count.']', PARAM_RAW); 899 $this->_form->setDefault('response_editor['.$count.']', array('text' => '', 'format' => FORMAT_HTML)); 900 901 if ($required) { 902 $this->_form->addRule('response_editor['.$count.']', get_string('required'), 'required', null, 'client'); 903 } 904 } 905 906 /** 907 * A function that gets called upon init of this object by the calling script. 908 * 909 * This can be used to process an immediate action if required. Currently it 910 * is only used in special cases by non-standard page types. 911 * 912 * @return bool 913 */ 914 public function construction_override($pageid, lesson $lesson) { 915 return true; 916 } 917 } 918 919 920 921 /** 922 * Class representation of a lesson 923 * 924 * This class is used the interact with, and manage a lesson once instantiated. 925 * If you need to fetch a lesson object you can do so by calling 926 * 927 * <code> 928 * lesson::load($lessonid); 929 * // or 930 * $lessonrecord = $DB->get_record('lesson', $lessonid); 931 * $lesson = new lesson($lessonrecord); 932 * </code> 933 * 934 * The class itself extends lesson_base as all classes within the lesson module should 935 * 936 * These properties are from the database 937 * @property int $id The id of this lesson 938 * @property int $course The ID of the course this lesson belongs to 939 * @property string $name The name of this lesson 940 * @property int $practice Flag to toggle this as a practice lesson 941 * @property int $modattempts Toggle to allow the user to go back and review answers 942 * @property int $usepassword Toggle the use of a password for entry 943 * @property string $password The password to require users to enter 944 * @property int $dependency ID of another lesson this lesson is dependent on 945 * @property string $conditions Conditions of the lesson dependency 946 * @property int $grade The maximum grade a user can achieve (%) 947 * @property int $custom Toggle custom scoring on or off 948 * @property int $ongoing Toggle display of an ongoing score 949 * @property int $usemaxgrade How retakes are handled (max=1, mean=0) 950 * @property int $maxanswers The max number of answers or branches 951 * @property int $maxattempts The maximum number of attempts a user can record 952 * @property int $review Toggle use or wrong answer review button 953 * @property int $nextpagedefault Override the default next page 954 * @property int $feedback Toggles display of default feedback 955 * @property int $minquestions Sets a minimum value of pages seen when calculating grades 956 * @property int $maxpages Maximum number of pages this lesson can contain 957 * @property int $retake Flag to allow users to retake a lesson 958 * @property int $activitylink Relate this lesson to another lesson 959 * @property string $mediafile File to pop up to or webpage to display 960 * @property int $mediaheight Sets the height of the media file popup 961 * @property int $mediawidth Sets the width of the media file popup 962 * @property int $mediaclose Toggle display of a media close button 963 * @property int $slideshow Flag for whether branch pages should be shown as slideshows 964 * @property int $width Width of slideshow 965 * @property int $height Height of slideshow 966 * @property string $bgcolor Background colour of slideshow 967 * @property int $displayleft Display a left menu 968 * @property int $displayleftif Sets the condition on which the left menu is displayed 969 * @property int $progressbar Flag to toggle display of a lesson progress bar 970 * @property int $available Timestamp of when this lesson becomes available 971 * @property int $deadline Timestamp of when this lesson is no longer available 972 * @property int $timemodified Timestamp when lesson was last modified 973 * 974 * These properties are calculated 975 * @property int $firstpageid Id of the first page of this lesson (prevpageid=0) 976 * @property int $lastpageid Id of the last page of this lesson (nextpageid=0) 977 * 978 * @copyright 2009 Sam Hemelryk 979 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 980 */ 981 class lesson extends lesson_base { 982 983 /** 984 * The id of the first page (where prevpageid = 0) gets set and retrieved by 985 * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code> 986 * @var int 987 */ 988 protected $firstpageid = null; 989 /** 990 * The id of the last page (where nextpageid = 0) gets set and retrieved by 991 * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code> 992 * @var int 993 */ 994 protected $lastpageid = null; 995 /** 996 * An array used to cache the pages associated with this lesson after the first 997 * time they have been loaded. 998 * A note to developers: If you are going to be working with MORE than one or 999 * two pages from a lesson you should probably call {@see $lesson->load_all_pages()} 1000 * in order to save excess database queries. 1001 * @var array An array of lesson_page objects 1002 */ 1003 protected $pages = array(); 1004 /** 1005 * Flag that gets set to true once all of the pages associated with the lesson 1006 * have been loaded. 1007 * @var bool 1008 */ 1009 protected $loadedallpages = false; 1010 1011 /** 1012 * Simply generates a lesson object given an array/object of properties 1013 * Overrides {@see lesson_base->create()} 1014 * @static 1015 * @param object|array $properties 1016 * @return lesson 1017 */ 1018 public static function create($properties) { 1019 return new lesson($properties); 1020 } 1021 1022 /** 1023 * Generates a lesson object from the database given its id 1024 * @static 1025 * @param int $lessonid 1026 * @return lesson 1027 */ 1028 public static function load($lessonid) { 1029 global $DB; 1030 1031 if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) { 1032 print_error('invalidcoursemodule'); 1033 } 1034 return new lesson($lesson); 1035 } 1036 1037 /** 1038 * Deletes this lesson from the database 1039 */ 1040 public function delete() { 1041 global $CFG, $DB; 1042 require_once($CFG->libdir.'/gradelib.php'); 1043 require_once($CFG->dirroot.'/calendar/lib.php'); 1044 1045 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); 1046 $context = context_module::instance($cm->id); 1047 1048 $this->delete_all_overrides(); 1049 1050 $DB->delete_records("lesson", array("id"=>$this->properties->id)); 1051 $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id)); 1052 $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id)); 1053 $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id)); 1054 $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id)); 1055 $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id)); 1056 $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id)); 1057 if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) { 1058 foreach($events as $event) { 1059 $event = calendar_event::load($event); 1060 $event->delete(); 1061 } 1062 } 1063 1064 // Delete files associated with this module. 1065 $fs = get_file_storage(); 1066 $fs->delete_area_files($context->id); 1067 1068 grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, null, array('deleted'=>1)); 1069 return true; 1070 } 1071 1072 /** 1073 * Deletes a lesson override from the database and clears any corresponding calendar events 1074 * 1075 * @param int $overrideid The id of the override being deleted 1076 * @return bool true on success 1077 */ 1078 public function delete_override($overrideid) { 1079 global $CFG, $DB; 1080 1081 require_once($CFG->dirroot . '/calendar/lib.php'); 1082 1083 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); 1084 1085 $override = $DB->get_record('lesson_overrides', array('id' => $overrideid), '*', MUST_EXIST); 1086 1087 // Delete the events. 1088 $conds = array('modulename' => 'lesson', 1089 'instance' => $this->properties->id); 1090 if (isset($override->userid)) { 1091 $conds['userid'] = $override->userid; 1092 } else { 1093 $conds['groupid'] = $override->groupid; 1094 } 1095 $events = $DB->get_records('event', $conds); 1096 foreach ($events as $event) { 1097 $eventold = calendar_event::load($event); 1098 $eventold->delete(); 1099 } 1100 1101 $DB->delete_records('lesson_overrides', array('id' => $overrideid)); 1102 1103 // Set the common parameters for one of the events we will be triggering. 1104 $params = array( 1105 'objectid' => $override->id, 1106 'context' => context_module::instance($cm->id), 1107 'other' => array( 1108 'lessonid' => $override->lessonid 1109 ) 1110 ); 1111 // Determine which override deleted event to fire. 1112 if (!empty($override->userid)) { 1113 $params['relateduserid'] = $override->userid; 1114 $event = \mod_lesson\event\user_override_deleted::create($params); 1115 } else { 1116 $params['other']['groupid'] = $override->groupid; 1117 $event = \mod_lesson\event\group_override_deleted::create($params); 1118 } 1119 1120 // Trigger the override deleted event. 1121 $event->add_record_snapshot('lesson_overrides', $override); 1122 $event->trigger(); 1123 1124 return true; 1125 } 1126 1127 /** 1128 * Deletes all lesson overrides from the database and clears any corresponding calendar events 1129 */ 1130 public function delete_all_overrides() { 1131 global $DB; 1132 1133 $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $this->properties->id), 'id'); 1134 foreach ($overrides as $override) { 1135 $this->delete_override($override->id); 1136 } 1137 } 1138 1139 /** 1140 * Updates the lesson properties with override information for a user. 1141 * 1142 * Algorithm: For each lesson setting, if there is a matching user-specific override, 1143 * then use that otherwise, if there are group-specific overrides, return the most 1144 * lenient combination of them. If neither applies, leave the quiz setting unchanged. 1145 * 1146 * Special case: if there is more than one password that applies to the user, then 1147 * lesson->extrapasswords will contain an array of strings giving the remaining 1148 * passwords. 1149 * 1150 * @param int $userid The userid. 1151 */ 1152 public function update_effective_access($userid) { 1153 global $DB; 1154 1155 // Check for user override. 1156 $override = $DB->get_record('lesson_overrides', array('lessonid' => $this->properties->id, 'userid' => $userid)); 1157 1158 if (!$override) { 1159 $override = new stdClass(); 1160 $override->available = null; 1161 $override->deadline = null; 1162 $override->timelimit = null; 1163 $override->review = null; 1164 $override->maxattempts = null; 1165 $override->retake = null; 1166 $override->password = null; 1167 } 1168 1169 // Check for group overrides. 1170 $groupings = groups_get_user_groups($this->properties->course, $userid); 1171 1172 if (!empty($groupings[0])) { 1173 // Select all overrides that apply to the User's groups. 1174 list($extra, $params) = $DB->get_in_or_equal(array_values($groupings[0])); 1175 $sql = "SELECT * FROM {lesson_overrides} 1176 WHERE groupid $extra AND lessonid = ?"; 1177 $params[] = $this->properties->id; 1178 $records = $DB->get_records_sql($sql, $params); 1179 1180 // Combine the overrides. 1181 $availables = array(); 1182 $deadlines = array(); 1183 $timelimits = array(); 1184 $reviews = array(); 1185 $attempts = array(); 1186 $retakes = array(); 1187 $passwords = array(); 1188 1189 foreach ($records as $gpoverride) { 1190 if (isset($gpoverride->available)) { 1191 $availables[] = $gpoverride->available; 1192 } 1193 if (isset($gpoverride->deadline)) { 1194 $deadlines[] = $gpoverride->deadline; 1195 } 1196 if (isset($gpoverride->timelimit)) { 1197 $timelimits[] = $gpoverride->timelimit; 1198 } 1199 if (isset($gpoverride->review)) { 1200 $reviews[] = $gpoverride->review; 1201 } 1202 if (isset($gpoverride->maxattempts)) { 1203 $attempts[] = $gpoverride->maxattempts; 1204 } 1205 if (isset($gpoverride->retake)) { 1206 $retakes[] = $gpoverride->retake; 1207 } 1208 if (isset($gpoverride->password)) { 1209 $passwords[] = $gpoverride->password; 1210 } 1211 } 1212 // If there is a user override for a setting, ignore the group override. 1213 if (is_null($override->available) && count($availables)) { 1214 $override->available = min($availables); 1215 } 1216 if (is_null($override->deadline) && count($deadlines)) { 1217 if (in_array(0, $deadlines)) { 1218 $override->deadline = 0; 1219 } else { 1220 $override->deadline = max($deadlines); 1221 } 1222 } 1223 if (is_null($override->timelimit) && count($timelimits)) { 1224 if (in_array(0, $timelimits)) { 1225 $override->timelimit = 0; 1226 } else { 1227 $override->timelimit = max($timelimits); 1228 } 1229 } 1230 if (is_null($override->review) && count($reviews)) { 1231 $override->review = max($reviews); 1232 } 1233 if (is_null($override->maxattempts) && count($attempts)) { 1234 $override->maxattempts = max($attempts); 1235 } 1236 if (is_null($override->retake) && count($retakes)) { 1237 $override->retake = max($retakes); 1238 } 1239 if (is_null($override->password) && count($passwords)) { 1240 $override->password = array_shift($passwords); 1241 if (count($passwords)) { 1242 $override->extrapasswords = $passwords; 1243 } 1244 } 1245 1246 } 1247 1248 // Merge with lesson defaults. 1249 $keys = array('available', 'deadline', 'timelimit', 'maxattempts', 'review', 'retake'); 1250 foreach ($keys as $key) { 1251 if (isset($override->{$key})) { 1252 $this->properties->{$key} = $override->{$key}; 1253 } 1254 } 1255 1256 // Special handling of lesson usepassword and password. 1257 if (isset($override->password)) { 1258 if ($override->password == '') { 1259 $this->properties->usepassword = 0; 1260 } else { 1261 $this->properties->usepassword = 1; 1262 $this->properties->password = $override->password; 1263 if (isset($override->extrapasswords)) { 1264 $this->properties->extrapasswords = $override->extrapasswords; 1265 } 1266 } 1267 } 1268 } 1269 1270 /** 1271 * Fetches messages from the session that may have been set in previous page 1272 * actions. 1273 * 1274 * <code> 1275 * // Do not call this method directly instead use 1276 * $lesson->messages; 1277 * </code> 1278 * 1279 * @return array 1280 */ 1281 protected function get_messages() { 1282 global $SESSION; 1283 1284 $messages = array(); 1285 if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) { 1286 $messages = $SESSION->lesson_messages[$this->properties->id]; 1287 unset($SESSION->lesson_messages[$this->properties->id]); 1288 } 1289 1290 return $messages; 1291 } 1292 1293 /** 1294 * Get all of the attempts for the current user. 1295 * 1296 * @param int $retries 1297 * @param bool $correct Optional: only fetch correct attempts 1298 * @param int $pageid Optional: only fetch attempts at the given page 1299 * @param int $userid Optional: defaults to the current user if not set 1300 * @return array|false 1301 */ 1302 public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) { 1303 global $USER, $DB; 1304 $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries); 1305 if ($correct) { 1306 $params['correct'] = 1; 1307 } 1308 if ($pageid !== null) { 1309 $params['pageid'] = $pageid; 1310 } 1311 if ($userid === null) { 1312 $params['userid'] = $USER->id; 1313 } 1314 return $DB->get_records('lesson_attempts', $params, 'timeseen ASC'); 1315 } 1316 1317 /** 1318 * Returns the first page for the lesson or false if there isn't one. 1319 * 1320 * This method should be called via the magic method __get(); 1321 * <code> 1322 * $firstpage = $lesson->firstpage; 1323 * </code> 1324 * 1325 * @return lesson_page|bool Returns the lesson_page specialised object or false 1326 */ 1327 protected function get_firstpage() { 1328 $pages = $this->load_all_pages(); 1329 if (count($pages) > 0) { 1330 foreach ($pages as $page) { 1331 if ((int)$page->prevpageid === 0) { 1332 return $page; 1333 } 1334 } 1335 } 1336 return false; 1337 } 1338 1339 /** 1340 * Returns the last page for the lesson or false if there isn't one. 1341 * 1342 * This method should be called via the magic method __get(); 1343 * <code> 1344 * $lastpage = $lesson->lastpage; 1345 * </code> 1346 * 1347 * @return lesson_page|bool Returns the lesson_page specialised object or false 1348 */ 1349 protected function get_lastpage() { 1350 $pages = $this->load_all_pages(); 1351 if (count($pages) > 0) { 1352 foreach ($pages as $page) { 1353 if ((int)$page->nextpageid === 0) { 1354 return $page; 1355 } 1356 } 1357 } 1358 return false; 1359 } 1360 1361 /** 1362 * Returns the id of the first page of this lesson. (prevpageid = 0) 1363 * @return int 1364 */ 1365 protected function get_firstpageid() { 1366 global $DB; 1367 if ($this->firstpageid == null) { 1368 if (!$this->loadedallpages) { 1369 $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0)); 1370 if (!$firstpageid) { 1371 print_error('cannotfindfirstpage', 'lesson'); 1372 } 1373 $this->firstpageid = $firstpageid; 1374 } else { 1375 $firstpage = $this->get_firstpage(); 1376 $this->firstpageid = $firstpage->id; 1377 } 1378 } 1379 return $this->firstpageid; 1380 } 1381 1382 /** 1383 * Returns the id of the last page of this lesson. (nextpageid = 0) 1384 * @return int 1385 */ 1386 public function get_lastpageid() { 1387 global $DB; 1388 if ($this->lastpageid == null) { 1389 if (!$this->loadedallpages) { 1390 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0)); 1391 if (!$lastpageid) { 1392 print_error('cannotfindlastpage', 'lesson'); 1393 } 1394 $this->lastpageid = $lastpageid; 1395 } else { 1396 $lastpageid = $this->get_lastpage(); 1397 $this->lastpageid = $lastpageid->id; 1398 } 1399 } 1400 1401 return $this->lastpageid; 1402 } 1403 1404 /** 1405 * Gets the next page id to display after the one that is provided. 1406 * @param int $nextpageid 1407 * @return bool 1408 */ 1409 public function get_next_page($nextpageid) { 1410 global $USER, $DB; 1411 $allpages = $this->load_all_pages(); 1412 if ($this->properties->nextpagedefault) { 1413 // in Flash Card mode...first get number of retakes 1414 $nretakes = $DB->count_records("lesson_grades", array("lessonid" => $this->properties->id, "userid" => $USER->id)); 1415 shuffle($allpages); 1416 $found = false; 1417 if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) { 1418 foreach ($allpages as $nextpage) { 1419 if (!$DB->count_records("lesson_attempts", array("pageid" => $nextpage->id, "userid" => $USER->id, "retry" => $nretakes))) { 1420 $found = true; 1421 break; 1422 } 1423 } 1424 } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) { 1425 foreach ($allpages as $nextpage) { 1426 if (!$DB->count_records("lesson_attempts", array('pageid' => $nextpage->id, 'userid' => $USER->id, 'correct' => 1, 'retry' => $nretakes))) { 1427 $found = true; 1428 break; 1429 } 1430 } 1431 } 1432 if ($found) { 1433 if ($this->properties->maxpages) { 1434 // check number of pages viewed (in the lesson) 1435 if ($DB->count_records("lesson_attempts", array("lessonid" => $this->properties->id, "userid" => $USER->id, "retry" => $nretakes)) >= $this->properties->maxpages) { 1436 return LESSON_EOL; 1437 } 1438 } 1439 return $nextpage->id; 1440 } 1441 } 1442 // In a normal lesson mode 1443 foreach ($allpages as $nextpage) { 1444 if ((int)$nextpage->id === (int)$nextpageid) { 1445 return $nextpage->id; 1446 } 1447 } 1448 return LESSON_EOL; 1449 } 1450 1451 /** 1452 * Sets a message against the session for this lesson that will displayed next 1453 * time the lesson processes messages 1454 * 1455 * @param string $message 1456 * @param string $class 1457 * @param string $align 1458 * @return bool 1459 */ 1460 public function add_message($message, $class="notifyproblem", $align='center') { 1461 global $SESSION; 1462 1463 if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) { 1464 $SESSION->lesson_messages = array(); 1465 $SESSION->lesson_messages[$this->properties->id] = array(); 1466 } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) { 1467 $SESSION->lesson_messages[$this->properties->id] = array(); 1468 } 1469 1470 $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align); 1471 1472 return true; 1473 } 1474 1475 /** 1476 * Check if the lesson is accessible at the present time 1477 * @return bool True if the lesson is accessible, false otherwise 1478 */ 1479 public function is_accessible() { 1480 $available = $this->properties->available; 1481 $deadline = $this->properties->deadline; 1482 return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline)); 1483 } 1484 1485 /** 1486 * Starts the lesson time for the current user 1487 * @return bool Returns true 1488 */ 1489 public function start_timer() { 1490 global $USER, $DB; 1491 1492 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, 1493 false, MUST_EXIST); 1494 1495 // Trigger lesson started event. 1496 $event = \mod_lesson\event\lesson_started::create(array( 1497 'objectid' => $this->properties()->id, 1498 'context' => context_module::instance($cm->id), 1499 'courseid' => $this->properties()->course 1500 )); 1501 $event->trigger(); 1502 1503 $USER->startlesson[$this->properties->id] = true; 1504 $startlesson = new stdClass; 1505 $startlesson->lessonid = $this->properties->id; 1506 $startlesson->userid = $USER->id; 1507 $startlesson->starttime = time(); 1508 $startlesson->lessontime = time(); 1509 $DB->insert_record('lesson_timer', $startlesson); 1510 if ($this->properties->timelimit) { 1511 $this->add_message(get_string('timelimitwarning', 'lesson', format_time($this->properties->timelimit)), 'center'); 1512 } 1513 return true; 1514 } 1515 1516 /** 1517 * Updates the timer to the current time and returns the new timer object 1518 * @param bool $restart If set to true the timer is restarted 1519 * @param bool $continue If set to true AND $restart=true then the timer 1520 * will continue from a previous attempt 1521 * @return stdClass The new timer 1522 */ 1523 public function update_timer($restart=false, $continue=false, $endreached =false) { 1524 global $USER, $DB; 1525 1526 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); 1527 1528 // clock code 1529 // get time information for this user 1530 $params = array("lessonid" => $this->properties->id, "userid" => $USER->id); 1531 if (!$timer = $DB->get_records('lesson_timer', $params, 'starttime DESC', '*', 0, 1)) { 1532 $this->start_timer(); 1533 $timer = $DB->get_records('lesson_timer', $params, 'starttime DESC', '*', 0, 1); 1534 } 1535 $timer = current($timer); // This will get the latest start time record. 1536 1537 if ($restart) { 1538 if ($continue) { 1539 // continue a previous test, need to update the clock (think this option is disabled atm) 1540 $timer->starttime = time() - ($timer->lessontime - $timer->starttime); 1541 1542 // Trigger lesson resumed event. 1543 $event = \mod_lesson\event\lesson_resumed::create(array( 1544 'objectid' => $this->properties->id, 1545 'context' => context_module::instance($cm->id), 1546 'courseid' => $this->properties->course 1547 )); 1548 $event->trigger(); 1549 1550 } else { 1551 // starting over, so reset the clock 1552 $timer->starttime = time(); 1553 1554 // Trigger lesson restarted event. 1555 $event = \mod_lesson\event\lesson_restarted::create(array( 1556 'objectid' => $this->properties->id, 1557 'context' => context_module::instance($cm->id), 1558 'courseid' => $this->properties->course 1559 )); 1560 $event->trigger(); 1561 1562 } 1563 } 1564 1565 $timer->lessontime = time(); 1566 $timer->completed = $endreached; 1567 $DB->update_record('lesson_timer', $timer); 1568 1569 // Update completion state. 1570 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, 1571 false, MUST_EXIST); 1572 $course = get_course($cm->course); 1573 $completion = new completion_info($course); 1574 if ($completion->is_enabled($cm) && $this->properties()->completiontimespent > 0) { 1575 $completion->update_state($cm, COMPLETION_COMPLETE); 1576 } 1577 return $timer; 1578 } 1579 1580 /** 1581 * Updates the timer to the current time then stops it by unsetting the user var 1582 * @return bool Returns true 1583 */ 1584 public function stop_timer() { 1585 global $USER, $DB; 1586 unset($USER->startlesson[$this->properties->id]); 1587 1588 $cm = get_coursemodule_from_instance('lesson', $this->properties()->id, $this->properties()->course, 1589 false, MUST_EXIST); 1590 1591 // Trigger lesson ended event. 1592 $event = \mod_lesson\event\lesson_ended::create(array( 1593 'objectid' => $this->properties()->id, 1594 'context' => context_module::instance($cm->id), 1595 'courseid' => $this->properties()->course 1596 )); 1597 $event->trigger(); 1598 1599 return $this->update_timer(false, false, true); 1600 } 1601 1602 /** 1603 * Checks to see if the lesson has pages 1604 */ 1605 public function has_pages() { 1606 global $DB; 1607 $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id)); 1608 return ($pagecount>0); 1609 } 1610 1611 /** 1612 * Returns the link for the related activity 1613 * @return array|false 1614 */ 1615 public function link_for_activitylink() { 1616 global $DB; 1617 $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink)); 1618 if ($module) { 1619 $modname = $DB->get_field('modules', 'name', array('id' => $module->module)); 1620 if ($modname) { 1621 $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance)); 1622 if ($instancename) { 1623 return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php', array('id'=>$this->properties->activitylink)), 1624 get_string('activitylinkname', 'lesson', $instancename), 1625 array('class'=>'centerpadded lessonbutton standardbutton')); 1626 } 1627 } 1628 } 1629 return ''; 1630 } 1631 1632 /** 1633 * Loads the requested page. 1634 * 1635 * This function will return the requested page id as either a specialised 1636 * lesson_page object OR as a generic lesson_page. 1637 * If the page has been loaded previously it will be returned from the pages 1638 * array, otherwise it will be loaded from the database first 1639 * 1640 * @param int $pageid 1641 * @return lesson_page A lesson_page object or an object that extends it 1642 */ 1643 public function load_page($pageid) { 1644 if (!array_key_exists($pageid, $this->pages)) { 1645 $manager = lesson_page_type_manager::get($this); 1646 $this->pages[$pageid] = $manager->load_page($pageid, $this); 1647 } 1648 return $this->pages[$pageid]; 1649 } 1650 1651 /** 1652 * Loads ALL of the pages for this lesson 1653 * 1654 * @return array An array containing all pages from this lesson 1655 */ 1656 public function load_all_pages() { 1657 if (!$this->loadedallpages) { 1658 $manager = lesson_page_type_manager::get($this); 1659 $this->pages = $manager->load_all_pages($this); 1660 $this->loadedallpages = true; 1661 } 1662 return $this->pages; 1663 } 1664 1665 /** 1666 * Determines if a jumpto value is correct or not. 1667 * 1668 * returns true if jumpto page is (logically) after the pageid page or 1669 * if the jumpto value is a special value. Returns false in all other cases. 1670 * 1671 * @param int $pageid Id of the page from which you are jumping from. 1672 * @param int $jumpto The jumpto number. 1673 * @return boolean True or false after a series of tests. 1674 **/ 1675 public function jumpto_is_correct($pageid, $jumpto) { 1676 global $DB; 1677 1678 // first test the special values 1679 if (!$jumpto) { 1680 // same page 1681 return false; 1682 } elseif ($jumpto == LESSON_NEXTPAGE) { 1683 return true; 1684 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) { 1685 return true; 1686 } elseif ($jumpto == LESSON_RANDOMPAGE) { 1687 return true; 1688 } elseif ($jumpto == LESSON_CLUSTERJUMP) { 1689 return true; 1690 } elseif ($jumpto == LESSON_EOL) { 1691 return true; 1692 } 1693 1694 $pages = $this->load_all_pages(); 1695 $apageid = $pages[$pageid]->nextpageid; 1696 while ($apageid != 0) { 1697 if ($jumpto == $apageid) { 1698 return true; 1699 } 1700 $apageid = $pages[$apageid]->nextpageid; 1701 } 1702 return false; 1703 } 1704 1705 /** 1706 * Returns the time a user has remaining on this lesson 1707 * @param int $starttime Starttime timestamp 1708 * @return string 1709 */ 1710 public function time_remaining($starttime) { 1711 $timeleft = $starttime + $this->properties->timelimit - time(); 1712 $hours = floor($timeleft/3600); 1713 $timeleft = $timeleft - ($hours * 3600); 1714 $minutes = floor($timeleft/60); 1715 $secs = $timeleft - ($minutes * 60); 1716 1717 if ($minutes < 10) { 1718 $minutes = "0$minutes"; 1719 } 1720 if ($secs < 10) { 1721 $secs = "0$secs"; 1722 } 1723 $output = array(); 1724 $output[] = $hours; 1725 $output[] = $minutes; 1726 $output[] = $secs; 1727 $output = implode(':', $output); 1728 return $output; 1729 } 1730 1731 /** 1732 * Interprets LESSON_CLUSTERJUMP jumpto value. 1733 * 1734 * This will select a page randomly 1735 * and the page selected will be inbetween a cluster page and end of clutter or end of lesson 1736 * and the page selected will be a page that has not been viewed already 1737 * and if any pages are within a branch table or end of branch then only 1 page within 1738 * the branch table or end of branch will be randomly selected (sub clustering). 1739 * 1740 * @param int $pageid Id of the current page from which we are jumping from. 1741 * @param int $userid Id of the user. 1742 * @return int The id of the next page. 1743 **/ 1744 public function cluster_jump($pageid, $userid=null) { 1745 global $DB, $USER; 1746 1747 if ($userid===null) { 1748 $userid = $USER->id; 1749 } 1750 // get the number of retakes 1751 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) { 1752 $retakes = 0; 1753 } 1754 // get all the lesson_attempts aka what the user has seen 1755 $seenpages = array(); 1756 if ($attempts = $this->get_attempts($retakes)) { 1757 foreach ($attempts as $attempt) { 1758 $seenpages[$attempt->pageid] = $attempt->pageid; 1759 } 1760 1761 } 1762 1763 // get the lesson pages 1764 $lessonpages = $this->load_all_pages(); 1765 // find the start of the cluster 1766 while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page 1767 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) { 1768 break; 1769 } 1770 $pageid = $lessonpages[$pageid]->prevpageid; 1771 } 1772 1773 $clusterpages = array(); 1774 $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER)); 1775 $unseen = array(); 1776 foreach ($clusterpages as $key=>$cluster) { 1777 // Remove the page if it is in a branch table or is an endofbranch. 1778 if ($this->is_sub_page_of_type($cluster->id, 1779 array(LESSON_PAGE_BRANCHTABLE), array(LESSON_PAGE_ENDOFBRANCH, LESSON_PAGE_CLUSTER)) 1780 || $cluster->qtype == LESSON_PAGE_ENDOFBRANCH) { 1781 unset($clusterpages[$key]); 1782 } else if ($cluster->qtype == LESSON_PAGE_BRANCHTABLE) { 1783 // If branchtable, check to see if any pages inside have been viewed. 1784 $branchpages = $this->get_sub_pages_of($cluster->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); 1785 $flag = true; 1786 foreach ($branchpages as $branchpage) { 1787 if (array_key_exists($branchpage->id, $seenpages)) { // Check if any of the pages have been viewed. 1788 $flag = false; 1789 } 1790 } 1791 if ($flag && count($branchpages) > 0) { 1792 // Add branch table. 1793 $unseen[] = $cluster; 1794 } 1795 } elseif ($cluster->is_unseen($seenpages)) { 1796 $unseen[] = $cluster; 1797 } 1798 } 1799 1800 if (count($unseen) > 0) { 1801 // it does not contain elements, then use exitjump, otherwise find out next page/branch 1802 $nextpage = $unseen[rand(0, count($unseen)-1)]; 1803 if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) { 1804 // if branch table, then pick a random page inside of it 1805 $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH)); 1806 return $branchpages[rand(0, count($branchpages)-1)]->id; 1807 } else { // otherwise, return the page's id 1808 return $nextpage->id; 1809 } 1810 } else { 1811 // seen all there is to see, leave the cluster 1812 if (end($clusterpages)->nextpageid == 0) { 1813 return LESSON_EOL; 1814 } else { 1815 $clusterendid = $pageid; 1816 while ($clusterendid != 0) { // This condition should not be satisfied... should be an end of cluster page. 1817 if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_ENDOFCLUSTER) { 1818 break; 1819 } 1820 $clusterendid = $lessonpages[$clusterendid]->nextpageid; 1821 } 1822 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id)); 1823 if ($exitjump == LESSON_NEXTPAGE) { 1824 $exitjump = $lessonpages[$clusterendid]->nextpageid; 1825 } 1826 if ($exitjump == 0) { 1827 return LESSON_EOL; 1828 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) { 1829 return $exitjump; 1830 } else { 1831 if (!array_key_exists($exitjump, $lessonpages)) { 1832 $found = false; 1833 foreach ($lessonpages as $page) { 1834 if ($page->id === $clusterendid) { 1835 $found = true; 1836 } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) { 1837 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id)); 1838 if ($exitjump == LESSON_NEXTPAGE) { 1839 $exitjump = $lessonpages[$page->id]->nextpageid; 1840 } 1841 break; 1842 } 1843 } 1844 } 1845 if (!array_key_exists($exitjump, $lessonpages)) { 1846 return LESSON_EOL; 1847 } 1848 return $exitjump; 1849 } 1850 } 1851 } 1852 } 1853 1854 /** 1855 * Finds all pages that appear to be a subtype of the provided pageid until 1856 * an end point specified within $ends is encountered or no more pages exist 1857 * 1858 * @param int $pageid 1859 * @param array $ends An array of LESSON_PAGE_* types that signify an end of 1860 * the subtype 1861 * @return array An array of specialised lesson_page objects 1862 */ 1863 public function get_sub_pages_of($pageid, array $ends) { 1864 $lessonpages = $this->load_all_pages(); 1865 $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table 1866 $pages = array(); 1867 1868 while (true) { 1869 if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) { 1870 break; 1871 } 1872 $pages[] = $lessonpages[$pageid]; 1873 $pageid = $lessonpages[$pageid]->nextpageid; 1874 } 1875 1876 return $pages; 1877 } 1878 1879 /** 1880 * Checks to see if the specified page[id] is a subpage of a type specified in 1881 * the $types array, until either there are no more pages of we find a type 1882 * corresponding to that of a type specified in $ends 1883 * 1884 * @param int $pageid The id of the page to check 1885 * @param array $types An array of types that would signify this page was a subpage 1886 * @param array $ends An array of types that mean this is not a subpage 1887 * @return bool 1888 */ 1889 public function is_sub_page_of_type($pageid, array $types, array $ends) { 1890 $pages = $this->load_all_pages(); 1891 $pageid = $pages[$pageid]->prevpageid; // move up one 1892 1893 array_unshift($ends, 0); 1894 // go up the pages till branch table 1895 while (true) { 1896 if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) { 1897 return false; 1898 } else if (in_array($pages[$pageid]->qtype, $types)) { 1899 return true; 1900 } 1901 $pageid = $pages[$pageid]->prevpageid; 1902 } 1903 } 1904 1905 /** 1906 * Move a page resorting all other pages. 1907 * 1908 * @param int $pageid 1909 * @param int $after 1910 * @return void 1911 */ 1912 public function resort_pages($pageid, $after) { 1913 global $CFG; 1914 1915 $cm = get_coursemodule_from_instance('lesson', $this->properties->id, $this->properties->course); 1916 $context = context_module::instance($cm->id); 1917 1918 $pages = $this->load_all_pages(); 1919 1920 if (!array_key_exists($pageid, $pages) || ($after!=0 && !array_key_exists($after, $pages))) { 1921 print_error('cannotfindpages', 'lesson', "$CFG->wwwroot/mod/lesson/edit.php?id=$cm->id"); 1922 } 1923 1924 $pagetomove = clone($pages[$pageid]); 1925 unset($pages[$pageid]); 1926 1927 $pageids = array(); 1928 if ($after === 0) { 1929 $pageids['p0'] = $pageid; 1930 } 1931 foreach ($pages as $page) { 1932 $pageids[] = $page->id; 1933 if ($page->id == $after) { 1934 $pageids[] = $pageid; 1935 } 1936 } 1937 1938 $pageidsref = $pageids; 1939 reset($pageidsref); 1940 $prev = 0; 1941 $next = next($pageidsref); 1942 foreach ($pageids as $pid) { 1943 if ($pid === $pageid) { 1944 $page = $pagetomove; 1945 } else { 1946 $page = $pages[$pid]; 1947 } 1948 if ($page->prevpageid != $prev || $page->nextpageid != $next) { 1949 $page->move($next, $prev); 1950 1951 if ($pid === $pageid) { 1952 // We will trigger an event. 1953 $pageupdated = array('next' => $next, 'prev' => $prev); 1954 } 1955 } 1956 1957 $prev = $page->id; 1958 $next = next($pageidsref); 1959 if (!$next) { 1960 $next = 0; 1961 } 1962 } 1963 1964 // Trigger an event: page moved. 1965 if (!empty($pageupdated)) { 1966 $eventparams = array( 1967 'context' => $context, 1968 'objectid' => $pageid, 1969 'other' => array( 1970 'pagetype' => $page->get_typestring(), 1971 'prevpageid' => $pageupdated['prev'], 1972 'nextpageid' => $pageupdated['next'] 1973 ) 1974 ); 1975 $event = \mod_lesson\event\page_moved::create($eventparams); 1976 $event->trigger(); 1977 } 1978 1979 } 1980 } 1981 1982 1983 /** 1984 * Abstract class to provide a core functions to the all lesson classes 1985 * 1986 * This class should be abstracted by ALL classes with the lesson module to ensure 1987 * that all classes within this module can be interacted with in the same way. 1988 * 1989 * This class provides the user with a basic properties array that can be fetched 1990 * or set via magic methods, or alternatively by defining methods get_blah() or 1991 * set_blah() within the extending object. 1992 * 1993 * @copyright 2009 Sam Hemelryk 1994 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 1995 */ 1996 abstract class lesson_base { 1997 1998 /** 1999 * An object containing properties 2000 * @var stdClass 2001 */ 2002 protected $properties; 2003 2004 /** 2005 * The constructor 2006 * @param stdClass $properties 2007 */ 2008 public function __construct($properties) { 2009 $this->properties = (object)$properties; 2010 } 2011 2012 /** 2013 * Magic property method 2014 * 2015 * Attempts to call a set_$key method if one exists otherwise falls back 2016 * to simply set the property 2017 * 2018 * @param string $key 2019 * @param mixed $value 2020 */ 2021 public function __set($key, $value) { 2022 if (method_exists($this, 'set_'.$key)) { 2023 $this->{'set_'.$key}($value); 2024 } 2025 $this->properties->{$key} = $value; 2026 } 2027 2028 /** 2029 * Magic get method 2030 * 2031 * Attempts to call a get_$key method to return the property and ralls over 2032 * to return the raw property 2033 * 2034 * @param str $key 2035 * @return mixed 2036 */ 2037 public function __get($key) { 2038 if (method_exists($this, 'get_'.$key)) { 2039 return $this->{'get_'.$key}(); 2040 } 2041 return $this->properties->{$key}; 2042 } 2043 2044 /** 2045 * Stupid PHP needs an isset magic method if you use the get magic method and 2046 * still want empty calls to work.... blah ~! 2047 * 2048 * @param string $key 2049 * @return bool 2050 */ 2051 public function __isset($key) { 2052 if (method_exists($this, 'get_'.$key)) { 2053 $val = $this->{'get_'.$key}(); 2054 return !empty($val); 2055 } 2056 return !empty($this->properties->{$key}); 2057 } 2058 2059 //NOTE: E_STRICT does not allow to change function signature! 2060 2061 /** 2062 * If implemented should create a new instance, save it in the DB and return it 2063 */ 2064 //public static function create() {} 2065 /** 2066 * If implemented should load an instance from the DB and return it 2067 */ 2068 //public static function load() {} 2069 /** 2070 * Fetches all of the properties of the object 2071 * @return stdClass 2072 */ 2073 public function properties() { 2074 return $this->properties; 2075 } 2076 } 2077 2078 2079 /** 2080 * Abstract class representation of a page associated with a lesson. 2081 * 2082 * This class should MUST be extended by all specialised page types defined in 2083 * mod/lesson/pagetypes/. 2084 * There are a handful of abstract methods that need to be defined as well as 2085 * severl methods that can optionally be defined in order to make the page type 2086 * operate in the desired way 2087 * 2088 * Database properties 2089 * @property int $id The id of this lesson page 2090 * @property int $lessonid The id of the lesson this page belongs to 2091 * @property int $prevpageid The id of the page before this one 2092 * @property int $nextpageid The id of the next page in the page sequence 2093 * @property int $qtype Identifies the page type of this page 2094 * @property int $qoption Used to record page type specific options 2095 * @property int $layout Used to record page specific layout selections 2096 * @property int $display Used to record page specific display selections 2097 * @property int $timecreated Timestamp for when the page was created 2098 * @property int $timemodified Timestamp for when the page was last modified 2099 * @property string $title The title of this page 2100 * @property string $contents The rich content shown to describe the page 2101 * @property int $contentsformat The format of the contents field 2102 * 2103 * Calculated properties 2104 * @property-read array $answers An array of answers for this page 2105 * @property-read bool $displayinmenublock Toggles display in the left menu block 2106 * @property-read array $jumps An array containing all the jumps this page uses 2107 * @property-read lesson $lesson The lesson this page belongs to 2108 * @property-read int $type The type of the page [question | structure] 2109 * @property-read typeid The unique identifier for the page type 2110 * @property-read typestring The string that describes this page type 2111 * 2112 * @abstract 2113 * @copyright 2009 Sam Hemelryk 2114 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2115 */ 2116 abstract class lesson_page extends lesson_base { 2117 2118 /** 2119 * A reference to the lesson this page belongs to 2120 * @var lesson 2121 */ 2122 protected $lesson = null; 2123 /** 2124 * Contains the answers to this lesson_page once loaded 2125 * @var null|array 2126 */ 2127 protected $answers = null; 2128 /** 2129 * This sets the type of the page, can be one of the constants defined below 2130 * @var int 2131 */ 2132 protected $type = 0; 2133 2134 /** 2135 * Constants used to identify the type of the page 2136 */ 2137 const TYPE_QUESTION = 0; 2138 const TYPE_STRUCTURE = 1; 2139 2140 /** 2141 * This method should return the integer used to identify the page type within 2142 * the database and throughout code. This maps back to the defines used in 1.x 2143 * @abstract 2144 * @return int 2145 */ 2146 abstract protected function get_typeid(); 2147 /** 2148 * This method should return the string that describes the pagetype 2149 * @abstract 2150 * @return string 2151 */ 2152 abstract protected function get_typestring(); 2153 2154 /** 2155 * This method gets called to display the page to the user taking the lesson 2156 * @abstract 2157 * @param object $renderer 2158 * @param object $attempt 2159 * @return string 2160 */ 2161 abstract public function display($renderer, $attempt); 2162 2163 /** 2164 * Creates a new lesson_page within the database and returns the correct pagetype 2165 * object to use to interact with the new lesson 2166 * 2167 * @final 2168 * @static 2169 * @param object $properties 2170 * @param lesson $lesson 2171 * @return lesson_page Specialised object that extends lesson_page 2172 */ 2173 final public static function create($properties, lesson $lesson, $context, $maxbytes) { 2174 global $DB; 2175 $newpage = new stdClass; 2176 $newpage->title = $properties->title; 2177 $newpage->contents = $properties->contents_editor['text']; 2178 $newpage->contentsformat = $properties->contents_editor['format']; 2179 $newpage->lessonid = $lesson->id; 2180 $newpage->timecreated = time(); 2181 $newpage->qtype = $properties->qtype; 2182 $newpage->qoption = (isset($properties->qoption))?1:0; 2183 $newpage->layout = (isset($properties->layout))?1:0; 2184 $newpage->display = (isset($properties->display))?1:0; 2185 $newpage->prevpageid = 0; // this is a first page 2186 $newpage->nextpageid = 0; // this is the only page 2187 2188 if ($properties->pageid) { 2189 $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid'); 2190 if (!$prevpage) { 2191 print_error('cannotfindpages', 'lesson'); 2192 } 2193 $newpage->prevpageid = $prevpage->id; 2194 $newpage->nextpageid = $prevpage->nextpageid; 2195 } else { 2196 $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id'); 2197 if ($nextpage) { 2198 // This is the first page, there are existing pages put this at the start 2199 $newpage->nextpageid = $nextpage->id; 2200 } 2201 } 2202 2203 $newpage->id = $DB->insert_record("lesson_pages", $newpage); 2204 2205 $editor = new stdClass; 2206 $editor->id = $newpage->id; 2207 $editor->contents_editor = $properties->contents_editor; 2208 $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id); 2209 $DB->update_record("lesson_pages", $editor); 2210 2211 if ($newpage->prevpageid > 0) { 2212 $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid)); 2213 } 2214 if ($newpage->nextpageid > 0) { 2215 $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid)); 2216 } 2217 2218 $page = lesson_page::load($newpage, $lesson); 2219 $page->create_answers($properties); 2220 2221 // Trigger an event: page created. 2222 $eventparams = array( 2223 'context' => $context, 2224 'objectid' => $newpage->id, 2225 'other' => array( 2226 'pagetype' => $page->get_typestring() 2227 ) 2228 ); 2229 $event = \mod_lesson\event\page_created::create($eventparams); 2230 $snapshot = clone($newpage); 2231 $snapshot->timemodified = 0; 2232 $event->add_record_snapshot('lesson_pages', $snapshot); 2233 $event->trigger(); 2234 2235 $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess'); 2236 2237 return $page; 2238 } 2239 2240 /** 2241 * This method loads a page object from the database and returns it as a 2242 * specialised object that extends lesson_page 2243 * 2244 * @final 2245 * @static 2246 * @param int $id 2247 * @param lesson $lesson 2248 * @return lesson_page Specialised lesson_page object 2249 */ 2250 final public static function load($id, lesson $lesson) { 2251 global $DB; 2252 2253 if (is_object($id) && !empty($id->qtype)) { 2254 $page = $id; 2255 } else { 2256 $page = $DB->get_record("lesson_pages", array("id" => $id)); 2257 if (!$page) { 2258 print_error('cannotfindpages', 'lesson'); 2259 } 2260 } 2261 $manager = lesson_page_type_manager::get($lesson); 2262 2263 $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype); 2264 if (!class_exists($class)) { 2265 $class = 'lesson_page'; 2266 } 2267 2268 return new $class($page, $lesson); 2269 } 2270 2271 /** 2272 * Deletes a lesson_page from the database as well as any associated records. 2273 * @final 2274 * @return bool 2275 */ 2276 final public function delete() { 2277 global $DB; 2278 2279 $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course); 2280 $context = context_module::instance($cm->id); 2281 2282 // Delete files associated with attempts. 2283 $fs = get_file_storage(); 2284 if ($attempts = $DB->get_records('lesson_attempts', array("pageid" => $this->properties->id))) { 2285 foreach ($attempts as $attempt) { 2286 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses', $attempt->id); 2287 } 2288 } 2289 2290 // Then delete all the associated records... 2291 $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id)); 2292 2293 $DB->delete_records("lesson_branch", array("pageid" => $this->properties->id)); 2294 // ...now delete the answers... 2295 $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id)); 2296 // ..and the page itself 2297 $DB->delete_records("lesson_pages", array("id" => $this->properties->id)); 2298 2299 // Trigger an event: page deleted. 2300 $eventparams = array( 2301 'context' => $context, 2302 'objectid' => $this->properties->id, 2303 'other' => array( 2304 'pagetype' => $this->get_typestring() 2305 ) 2306 ); 2307 $event = \mod_lesson\event\page_deleted::create($eventparams); 2308 $event->add_record_snapshot('lesson_pages', $this->properties); 2309 $event->trigger(); 2310 2311 // Delete files associated with this page. 2312 $fs->delete_area_files($context->id, 'mod_lesson', 'page_contents', $this->properties->id); 2313 $fs->delete_area_files($context->id, 'mod_lesson', 'page_answers', $this->properties->id); 2314 $fs->delete_area_files($context->id, 'mod_lesson', 'page_responses', $this->properties->id); 2315 2316 // repair the hole in the linkage 2317 if (!$this->properties->prevpageid && !$this->properties->nextpageid) { 2318 //This is the only page, no repair needed 2319 } elseif (!$this->properties->prevpageid) { 2320 // this is the first page... 2321 $page = $this->lesson->load_page($this->properties->nextpageid); 2322 $page->move(null, 0); 2323 } elseif (!$this->properties->nextpageid) { 2324 // this is the last page... 2325 $page = $this->lesson->load_page($this->properties->prevpageid); 2326 $page->move(0); 2327 } else { 2328 // page is in the middle... 2329 $prevpage = $this->lesson->load_page($this->properties->prevpageid); 2330 $nextpage = $this->lesson->load_page($this->properties->nextpageid); 2331 2332 $prevpage->move($nextpage->id); 2333 $nextpage->move(null, $prevpage->id); 2334 } 2335 return true; 2336 } 2337 2338 /** 2339 * Moves a page by updating its nextpageid and prevpageid values within 2340 * the database 2341 * 2342 * @final 2343 * @param int $nextpageid 2344 * @param int $prevpageid 2345 */ 2346 final public function move($nextpageid=null, $prevpageid=null) { 2347 global $DB; 2348 if ($nextpageid === null) { 2349 $nextpageid = $this->properties->nextpageid; 2350 } 2351 if ($prevpageid === null) { 2352 $prevpageid = $this->properties->prevpageid; 2353 } 2354 $obj = new stdClass; 2355 $obj->id = $this->properties->id; 2356 $obj->prevpageid = $prevpageid; 2357 $obj->nextpageid = $nextpageid; 2358 $DB->update_record('lesson_pages', $obj); 2359 } 2360 2361 /** 2362 * Returns the answers that are associated with this page in the database 2363 * 2364 * @final 2365 * @return array 2366 */ 2367 final public function get_answers() { 2368 global $DB; 2369 if ($this->answers === null) { 2370 $this->answers = array(); 2371 $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id'); 2372 if (!$answers) { 2373 // It is possible that a lesson upgraded from Moodle 1.9 still 2374 // contains questions without any answers [MDL-25632]. 2375 // debugging(get_string('cannotfindanswer', 'lesson')); 2376 return array(); 2377 } 2378 foreach ($answers as $answer) { 2379 $this->answers[count($this->answers)] = new lesson_page_answer($answer); 2380 } 2381 } 2382 return $this->answers; 2383 } 2384 2385 /** 2386 * Returns the lesson this page is associated with 2387 * @final 2388 * @return lesson 2389 */ 2390 final protected function get_lesson() { 2391 return $this->lesson; 2392 } 2393 2394 /** 2395 * Returns the type of page this is. Not to be confused with page type 2396 * @final 2397 * @return int 2398 */ 2399 final protected function get_type() { 2400 return $this->type; 2401 } 2402 2403 /** 2404 * Records an attempt at this page 2405 * 2406 * @final 2407 * @global moodle_database $DB 2408 * @param stdClass $context 2409 * @return stdClass Returns the result of the attempt 2410 */ 2411 final public function record_attempt($context) { 2412 global $DB, $USER, $OUTPUT, $PAGE; 2413 2414 /** 2415 * This should be overridden by each page type to actually check the response 2416 * against what ever custom criteria they have defined 2417 */ 2418 $result = $this->check_answer(); 2419 2420 $result->attemptsremaining = 0; 2421 $result->maxattemptsreached = false; 2422 2423 if ($result->noanswer) { 2424 $result->newpageid = $this->properties->id; // display same page again 2425 $result->feedback = get_string('noanswer', 'lesson'); 2426 } else { 2427 if (!has_capability('mod/lesson:manage', $context)) { 2428 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id)); 2429 2430 // Get the number of attempts that have been made on this question for this student and retake, 2431 $nattempts = $DB->count_records('lesson_attempts', array('lessonid' => $this->lesson->id, 2432 'userid' => $USER->id, 'pageid' => $this->properties->id, 'retry' => $nretakes)); 2433 2434 // Check if they have reached (or exceeded) the maximum number of attempts allowed. 2435 if ($nattempts >= $this->lesson->maxattempts) { 2436 $result->maxattemptsreached = true; 2437 $result->feedback = get_string('maximumnumberofattemptsreached', 'lesson'); 2438 $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid); 2439 return $result; 2440 } 2441 2442 // record student's attempt 2443 $attempt = new stdClass; 2444 $attempt->lessonid = $this->lesson->id; 2445 $attempt->pageid = $this->properties->id; 2446 $attempt->userid = $USER->id; 2447 $attempt->answerid = $result->answerid; 2448 $attempt->retry = $nretakes; 2449 $attempt->correct = $result->correctanswer; 2450 if($result->userresponse !== null) { 2451 $attempt->useranswer = $result->userresponse; 2452 } 2453 2454 $attempt->timeseen = time(); 2455 // if allow modattempts, then update the old attempt record, otherwise, insert new answer record 2456 $userisreviewing = false; 2457 if (isset($USER->modattempts[$this->lesson->id])) { 2458 $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high 2459 $userisreviewing = true; 2460 } 2461 2462 // Only insert a record if we are not reviewing the lesson. 2463 if (!$userisreviewing) { 2464 if ($this->lesson->retake || (!$this->lesson->retake && $nretakes == 0)) { 2465 $attempt->id = $DB->insert_record("lesson_attempts", $attempt); 2466 // Trigger an event: question answered. 2467 $eventparams = array( 2468 'context' => context_module::instance($PAGE->cm->id), 2469 'objectid' => $this->properties->id, 2470 'other' => array( 2471 'pagetype' => $this->get_typestring() 2472 ) 2473 ); 2474 $event = \mod_lesson\event\question_answered::create($eventparams); 2475 $event->add_record_snapshot('lesson_attempts', $attempt); 2476 $event->trigger(); 2477 2478 // Increase the number of attempts made. 2479 $nattempts++; 2480 } 2481 } 2482 // "number of attempts remaining" message if $this->lesson->maxattempts > 1 2483 // displaying of message(s) is at the end of page for more ergonomic display 2484 if (!$result->correctanswer && ($result->newpageid == 0)) { 2485 // retreive the number of attempts left counter for displaying at bottom of feedback page 2486 if ($nattempts >= $this->lesson->maxattempts) { 2487 if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt 2488 $result->maxattemptsreached = true; 2489 } 2490 $result->newpageid = LESSON_NEXTPAGE; 2491 } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt 2492 $result->attemptsremaining = $this->lesson->maxattempts - $nattempts; 2493 } 2494 } 2495 } 2496 // TODO: merge this code with the jump code below. Convert jumpto page into a proper page id 2497 if ($result->newpageid == 0) { 2498 $result->newpageid = $this->properties->id; 2499 } elseif ($result->newpageid == LESSON_NEXTPAGE) { 2500 $result->newpageid = $this->lesson->get_next_page($this->properties->nextpageid); 2501 } 2502 2503 // Determine default feedback if necessary 2504 if (empty($result->response)) { 2505 if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) { 2506 // These conditions have been met: 2507 // 1. The lesson manager has not supplied feedback to the student 2508 // 2. Not displaying default feedback 2509 // 3. The user did provide an answer 2510 // 4. We are not reviewing with an incorrect answer (and not reviewing an essay question) 2511 2512 $result->nodefaultresponse = true; // This will cause a redirect below 2513 } else if ($result->isessayquestion) { 2514 $result->response = get_string('defaultessayresponse', 'lesson'); 2515 } else if ($result->correctanswer) { 2516 $result->response = get_string('thatsthecorrectanswer', 'lesson'); 2517 } else { 2518 $result->response = get_string('thatsthewronganswer', 'lesson'); 2519 } 2520 } 2521 2522 if ($result->response) { 2523 if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) { 2524 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id)); 2525 $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id)); 2526 if ($qattempts == 1) { 2527 $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback'); 2528 } else { 2529 $result->feedback = $OUTPUT->box(get_string("secondpluswrong", "lesson"), 'feedback'); 2530 } 2531 } else { 2532 $result->feedback = ''; 2533 } 2534 $class = 'response'; 2535 if ($result->correctanswer) { 2536 $class .= ' correct'; // CSS over-ride this if they exist (!important). 2537 } else if (!$result->isessayquestion) { 2538 $class .= ' incorrect'; // CSS over-ride this if they exist (!important). 2539 } 2540 $options = new stdClass; 2541 $options->noclean = true; 2542 $options->para = true; 2543 $options->overflowdiv = true; 2544 $options->context = $context; 2545 2546 $result->feedback .= $OUTPUT->box(format_text($this->get_contents(), $this->properties->contentsformat, $options), 2547 'generalbox boxaligncenter'); 2548 if (isset($result->studentanswerformat)) { 2549 // This is the student's answer so it should be cleaned. 2550 $studentanswer = format_text($result->studentanswer, $result->studentanswerformat, 2551 array('context' => $context, 'para' => true)); 2552 } else { 2553 $studentanswer = format_string($result->studentanswer); 2554 } 2555 $result->feedback .= '<div class="correctanswer generalbox"><em>' 2556 . get_string("youranswer", "lesson").'</em> : ' . $studentanswer; 2557 if (isset($result->responseformat)) { 2558 $result->response = file_rewrite_pluginfile_urls($result->response, 'pluginfile.php', $context->id, 2559 'mod_lesson', 'page_responses', $result->answerid); 2560 $result->feedback .= $OUTPUT->box(format_text($result->response, $result->responseformat, $options) 2561 , $class); 2562 } else { 2563 $result->feedback .= $OUTPUT->box($result->response, $class); 2564 } 2565 $result->feedback .= '</div>'; 2566 } 2567 } 2568 2569 return $result; 2570 } 2571 2572 /** 2573 * Returns the string for a jump name 2574 * 2575 * @final 2576 * @param int $jumpto Jump code or page ID 2577 * @return string 2578 **/ 2579 final protected function get_jump_name($jumpto) { 2580 global $DB; 2581 static $jumpnames = array(); 2582 2583 if (!array_key_exists($jumpto, $jumpnames)) { 2584 if ($jumpto == LESSON_THISPAGE) { 2585 $jumptitle = get_string('thispage', 'lesson'); 2586 } elseif ($jumpto == LESSON_NEXTPAGE) { 2587 $jumptitle = get_string('nextpage', 'lesson'); 2588 } elseif ($jumpto == LESSON_EOL) { 2589 $jumptitle = get_string('endoflesson', 'lesson'); 2590 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) { 2591 $jumptitle = get_string('unseenpageinbranch', 'lesson'); 2592 } elseif ($jumpto == LESSON_PREVIOUSPAGE) { 2593 $jumptitle = get_string('previouspage', 'lesson'); 2594 } elseif ($jumpto == LESSON_RANDOMPAGE) { 2595 $jumptitle = get_string('randompageinbranch', 'lesson'); 2596 } elseif ($jumpto == LESSON_RANDOMBRANCH) { 2597 $jumptitle = get_string('randombranch', 'lesson'); 2598 } elseif ($jumpto == LESSON_CLUSTERJUMP) { 2599 $jumptitle = get_string('clusterjump', 'lesson'); 2600 } else { 2601 if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) { 2602 $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>'; 2603 } 2604 } 2605 $jumpnames[$jumpto] = format_string($jumptitle,true); 2606 } 2607 2608 return $jumpnames[$jumpto]; 2609 } 2610 2611 /** 2612 * Constructor method 2613 * @param object $properties 2614 * @param lesson $lesson 2615 */ 2616 public function __construct($properties, lesson $lesson) { 2617 parent::__construct($properties); 2618 $this->lesson = $lesson; 2619 } 2620 2621 /** 2622 * Returns the score for the attempt 2623 * This may be overridden by page types that require manual grading 2624 * @param array $answers 2625 * @param object $attempt 2626 * @return int 2627 */ 2628 public function earned_score($answers, $attempt) { 2629 return $answers[$attempt->answerid]->score; 2630 } 2631 2632 /** 2633 * This is a callback method that can be override and gets called when ever a page 2634 * is viewed 2635 * 2636 * @param bool $canmanage True if the user has the manage cap 2637 * @return mixed 2638 */ 2639 public function callback_on_view($canmanage) { 2640 return true; 2641 } 2642 2643 /** 2644 * save editor answers files and update answer record 2645 * 2646 * @param object $context 2647 * @param int $maxbytes 2648 * @param object $answer 2649 * @param object $answereditor 2650 * @param object $responseeditor 2651 */ 2652 public function save_answers_files($context, $maxbytes, &$answer, $answereditor = '', $responseeditor = '') { 2653 global $DB; 2654 if (isset($answereditor['itemid'])) { 2655 $answer->answer = file_save_draft_area_files($answereditor['itemid'], 2656 $context->id, 'mod_lesson', 'page_answers', $answer->id, 2657 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes), 2658 $answer->answer, null); 2659 $DB->set_field('lesson_answers', 'answer', $answer->answer, array('id' => $answer->id)); 2660 } 2661 if (isset($responseeditor['itemid'])) { 2662 $answer->response = file_save_draft_area_files($responseeditor['itemid'], 2663 $context->id, 'mod_lesson', 'page_responses', $answer->id, 2664 array('noclean' => true, 'maxfiles' => EDITOR_UNLIMITED_FILES, 'maxbytes' => $maxbytes), 2665 $answer->response, null); 2666 $DB->set_field('lesson_answers', 'response', $answer->response, array('id' => $answer->id)); 2667 } 2668 } 2669 2670 /** 2671 * Rewrite urls in response and optionality answer of a question answer 2672 * 2673 * @param object $answer 2674 * @param bool $rewriteanswer must rewrite answer 2675 * @return object answer with rewritten urls 2676 */ 2677 public static function rewrite_answers_urls($answer, $rewriteanswer = true) { 2678 global $PAGE; 2679 2680 $context = context_module::instance($PAGE->cm->id); 2681 if ($rewriteanswer) { 2682 $answer->answer = file_rewrite_pluginfile_urls($answer->answer, 'pluginfile.php', $context->id, 2683 'mod_lesson', 'page_answers', $answer->id); 2684 } 2685 $answer->response = file_rewrite_pluginfile_urls($answer->response, 'pluginfile.php', $context->id, 2686 'mod_lesson', 'page_responses', $answer->id); 2687 2688 return $answer; 2689 } 2690 2691 /** 2692 * Updates a lesson page and its answers within the database 2693 * 2694 * @param object $properties 2695 * @return bool 2696 */ 2697 public function update($properties, $context = null, $maxbytes = null) { 2698 global $DB, $PAGE; 2699 $answers = $this->get_answers(); 2700 $properties->id = $this->properties->id; 2701 $properties->lessonid = $this->lesson->id; 2702 if (empty($properties->qoption)) { 2703 $properties->qoption = '0'; 2704 } 2705 if (empty($context)) { 2706 $context = $PAGE->context; 2707 } 2708 if ($maxbytes === null) { 2709 $maxbytes = get_user_max_upload_file_size($context); 2710 } 2711 $properties->timemodified = time(); 2712 $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id); 2713 $DB->update_record("lesson_pages", $properties); 2714 2715 // Trigger an event: page updated. 2716 \mod_lesson\event\page_updated::create_from_lesson_page($this, $context)->trigger(); 2717 2718 if ($this->type == self::TYPE_STRUCTURE && $this->get_typeid() != LESSON_PAGE_BRANCHTABLE) { 2719 // These page types have only one answer to save the jump and score. 2720 if (count($answers) > 1) { 2721 $answer = array_shift($answers); 2722 foreach ($answers as $a) { 2723 $DB->delete_record('lesson_answers', array('id' => $a->id)); 2724 } 2725 } else if (count($answers) == 1) { 2726 $answer = array_shift($answers); 2727 } else { 2728 $answer = new stdClass; 2729 $answer->lessonid = $properties->lessonid; 2730 $answer->pageid = $properties->id; 2731 $answer->timecreated = time(); 2732 } 2733 2734 $answer->timemodified = time(); 2735 if (isset($properties->jumpto[0])) { 2736 $answer->jumpto = $properties->jumpto[0]; 2737 } 2738 if (isset($properties->score[0])) { 2739 $answer->score = $properties->score[0]; 2740 } 2741 if (!empty($answer->id)) { 2742 $DB->update_record("lesson_answers", $answer->properties()); 2743 } else { 2744 $DB->insert_record("lesson_answers", $answer); 2745 } 2746 } else { 2747 for ($i = 0; $i < $this->lesson->maxanswers; $i++) { 2748 if (!array_key_exists($i, $this->answers)) { 2749 $this->answers[$i] = new stdClass; 2750 $this->answers[$i]->lessonid = $this->lesson->id; 2751 $this->answers[$i]->pageid = $this->id; 2752 $this->answers[$i]->timecreated = $this->timecreated; 2753 } 2754 2755 if (isset($properties->answer_editor[$i])) { 2756 if (is_array($properties->answer_editor[$i])) { 2757 // Multichoice and true/false pages have an HTML editor. 2758 $this->answers[$i]->answer = $properties->answer_editor[$i]['text']; 2759 $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format']; 2760 } else { 2761 // Branch tables, shortanswer and mumerical pages have only a text field. 2762 $this->answers[$i]->answer = $properties->answer_editor[$i]; 2763 $this->answers[$i]->answerformat = FORMAT_MOODLE; 2764 } 2765 } 2766 2767 if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) { 2768 $this->answers[$i]->response = $properties->response_editor[$i]['text']; 2769 $this->answers[$i]->responseformat = $properties->response_editor[$i]['format']; 2770 } 2771 2772 if (isset($this->answers[$i]->answer) && $this->answers[$i]->answer != '') { 2773 if (isset($properties->jumpto[$i])) { 2774 $this->answers[$i]->jumpto = $properties->jumpto[$i]; 2775 } 2776 if ($this->lesson->custom && isset($properties->score[$i])) { 2777 $this->answers[$i]->score = $properties->score[$i]; 2778 } 2779 if (!isset($this->answers[$i]->id)) { 2780 $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]); 2781 } else { 2782 $DB->update_record("lesson_answers", $this->answers[$i]->properties()); 2783 } 2784 2785 // Save files in answers and responses. 2786 if (isset($properties->response_editor[$i])) { 2787 $this->save_answers_files($context, $maxbytes, $this->answers[$i], 2788 $properties->answer_editor[$i], $properties->response_editor[$i]); 2789 } else { 2790 $this->save_answers_files($context, $maxbytes, $this->answers[$i], 2791 $properties->answer_editor[$i]); 2792 } 2793 2794 } else if (isset($this->answers[$i]->id)) { 2795 $DB->delete_records('lesson_answers', array('id' => $this->answers[$i]->id)); 2796 unset($this->answers[$i]); 2797 } 2798 } 2799 } 2800 return true; 2801 } 2802 2803 /** 2804 * Can be set to true if the page requires a static link to create a new instance 2805 * instead of simply being included in the dropdown 2806 * @param int $previd 2807 * @return bool 2808 */ 2809 public function add_page_link($previd) { 2810 return false; 2811 } 2812 2813 /** 2814 * Returns true if a page has been viewed before 2815 * 2816 * @param array|int $param Either an array of pages that have been seen or the 2817 * number of retakes a user has had 2818 * @return bool 2819 */ 2820 public function is_unseen($param) { 2821 global $USER, $DB; 2822 if (is_array($param)) { 2823 $seenpages = $param; 2824 return (!array_key_exists($this->properties->id, $seenpages)); 2825 } else { 2826 $nretakes = $param; 2827 if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) { 2828 return true; 2829 } 2830 } 2831 return false; 2832 } 2833 2834 /** 2835 * Checks to see if a page has been answered previously 2836 * @param int $nretakes 2837 * @return bool 2838 */ 2839 public function is_unanswered($nretakes) { 2840 global $DB, $USER; 2841 if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) { 2842 return true; 2843 } 2844 return false; 2845 } 2846 2847 /** 2848 * Creates answers within the database for this lesson_page. Usually only ever 2849 * called when creating a new page instance 2850 * @param object $properties 2851 * @return array 2852 */ 2853 public function create_answers($properties) { 2854 global $DB, $PAGE; 2855 // now add the answers 2856 $newanswer = new stdClass; 2857 $newanswer->lessonid = $this->lesson->id; 2858 $newanswer->pageid = $this->properties->id; 2859 $newanswer->timecreated = $this->properties->timecreated; 2860 2861 $cm = get_coursemodule_from_instance('lesson', $this->lesson->id, $this->lesson->course); 2862 $context = context_module::instance($cm->id); 2863 2864 $answers = array(); 2865 2866 for ($i = 0; $i < $this->lesson->maxanswers; $i++) { 2867 $answer = clone($newanswer); 2868 2869 if (!empty($properties->answer_editor[$i])) { 2870 if (is_array($properties->answer_editor[$i])) { 2871 // Multichoice and true/false pages have an HTML editor. 2872 $answer->answer = $properties->answer_editor[$i]['text']; 2873 $answer->answerformat = $properties->answer_editor[$i]['format']; 2874 } else { 2875 // Branch tables, shortanswer and mumerical pages have only a text field. 2876 $answer->answer = $properties->answer_editor[$i]; 2877 $answer->answerformat = FORMAT_MOODLE; 2878 } 2879 } 2880 if (!empty($properties->response_editor[$i]) && is_array($properties->response_editor[$i])) { 2881 $answer->response = $properties->response_editor[$i]['text']; 2882 $answer->responseformat = $properties->response_editor[$i]['format']; 2883 } 2884 2885 if (isset($answer->answer) && $answer->answer != '') { 2886 if (isset($properties->jumpto[$i])) { 2887 $answer->jumpto = $properties->jumpto[$i]; 2888 } 2889 if ($this->lesson->custom && isset($properties->score[$i])) { 2890 $answer->score = $properties->score[$i]; 2891 } 2892 $answer->id = $DB->insert_record("lesson_answers", $answer); 2893 if (isset($properties->response_editor[$i])) { 2894 $this->save_answers_files($context, $PAGE->course->maxbytes, $answer, 2895 $properties->answer_editor[$i], $properties->response_editor[$i]); 2896 } else { 2897 $this->save_answers_files($context, $PAGE->course->maxbytes, $answer, 2898 $properties->answer_editor[$i]); 2899 } 2900 $answers[$answer->id] = new lesson_page_answer($answer); 2901 } else { 2902 break; 2903 } 2904 } 2905 2906 $this->answers = $answers; 2907 return $answers; 2908 } 2909 2910 /** 2911 * This method MUST be overridden by all question page types, or page types that 2912 * wish to score a page. 2913 * 2914 * The structure of result should always be the same so it is a good idea when 2915 * overriding this method on a page type to call 2916 * <code> 2917 * $result = parent::check_answer(); 2918 * </code> 2919 * before modifying it as required. 2920 * 2921 * @return stdClass 2922 */ 2923 public function check_answer() { 2924 $result = new stdClass; 2925 $result->answerid = 0; 2926 $result->noanswer = false; 2927 $result->correctanswer = false; 2928 $result->isessayquestion = false; // use this to turn off review button on essay questions 2929 $result->response = ''; 2930 $result->newpageid = 0; // stay on the page 2931 $result->studentanswer = ''; // use this to store student's answer(s) in order to display it on feedback page 2932 $result->userresponse = null; 2933 $result->feedback = ''; 2934 $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off 2935 return $result; 2936 } 2937 2938 /** 2939 * True if the page uses a custom option 2940 * 2941 * Should be override and set to true if the page uses a custom option. 2942 * 2943 * @return bool 2944 */ 2945 public function has_option() { 2946 return false; 2947 } 2948 2949 /** 2950 * Returns the maximum number of answers for this page given the maximum number 2951 * of answers permitted by the lesson. 2952 * 2953 * @param int $default 2954 * @return int 2955 */ 2956 public function max_answers($default) { 2957 return $default; 2958 } 2959 2960 /** 2961 * Returns the properties of this lesson page as an object 2962 * @return stdClass; 2963 */ 2964 public function properties() { 2965 $properties = clone($this->properties); 2966 if ($this->answers === null) { 2967 $this->get_answers(); 2968 } 2969 if (count($this->answers)>0) { 2970 $count = 0; 2971 $qtype = $properties->qtype; 2972 foreach ($this->answers as $answer) { 2973 $properties->{'answer_editor['.$count.']'} = array('text' => $answer->answer, 'format' => $answer->answerformat); 2974 if ($qtype != LESSON_PAGE_MATCHING) { 2975 $properties->{'response_editor['.$count.']'} = array('text' => $answer->response, 'format' => $answer->responseformat); 2976 } else { 2977 $properties->{'response_editor['.$count.']'} = $answer->response; 2978 } 2979 $properties->{'jumpto['.$count.']'} = $answer->jumpto; 2980 $properties->{'score['.$count.']'} = $answer->score; 2981 $count++; 2982 } 2983 } 2984 return $properties; 2985 } 2986 2987 /** 2988 * Returns an array of options to display when choosing the jumpto for a page/answer 2989 * @static 2990 * @param int $pageid 2991 * @param lesson $lesson 2992 * @return array 2993 */ 2994 public static function get_jumptooptions($pageid, lesson $lesson) { 2995 global $DB; 2996 $jump = array(); 2997 $jump[0] = get_string("thispage", "lesson"); 2998 $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson"); 2999 $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson"); 3000 $jump[LESSON_EOL] = get_string("endoflesson", "lesson"); 3001 3002 if ($pageid == 0) { 3003 return $jump; 3004 } 3005 3006 $pages = $lesson->load_all_pages(); 3007 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))) { 3008 $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson"); 3009 $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson"); 3010 } 3011 if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) { 3012 $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson"); 3013 } 3014 if (!optional_param('firstpage', 0, PARAM_INT)) { 3015 $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0)); 3016 while (true) { 3017 if ($apageid) { 3018 $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid)); 3019 $jump[$apageid] = strip_tags(format_string($title,true)); 3020 $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid)); 3021 } else { 3022 // last page reached 3023 break; 3024 } 3025 } 3026 } 3027 return $jump; 3028 } 3029 /** 3030 * Returns the contents field for the page properly formatted and with plugin 3031 * file url's converted 3032 * @return string 3033 */ 3034 public function get_contents() { 3035 global $PAGE; 3036 if (!empty($this->properties->contents)) { 3037 if (!isset($this->properties->contentsformat)) { 3038 $this->properties->contentsformat = FORMAT_HTML; 3039 } 3040 $context = context_module::instance($PAGE->cm->id); 3041 $contents = file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson', 3042 'page_contents', $this->properties->id); // Must do this BEFORE format_text()! 3043 return format_text($contents, $this->properties->contentsformat, 3044 array('context' => $context, 'noclean' => true, 3045 'overflowdiv' => true)); // Page edit is marked with XSS, we want all content here. 3046 } else { 3047 return ''; 3048 } 3049 } 3050 3051 /** 3052 * Set to true if this page should display in the menu block 3053 * @return bool 3054 */ 3055 protected function get_displayinmenublock() { 3056 return false; 3057 } 3058 3059 /** 3060 * Get the string that describes the options of this page type 3061 * @return string 3062 */ 3063 public function option_description_string() { 3064 return ''; 3065 } 3066 3067 /** 3068 * Updates a table with the answers for this page 3069 * @param html_table $table 3070 * @return html_table 3071 */ 3072 public function display_answers(html_table $table) { 3073 $answers = $this->get_answers(); 3074 $i = 1; 3075 foreach ($answers as $answer) { 3076 $cells = array(); 3077 $cells[] = "<span class=\"label\">".get_string("jump", "lesson")." $i<span>: "; 3078 $cells[] = $this->get_jump_name($answer->jumpto); 3079 $table->data[] = new html_table_row($cells); 3080 if ($i === 1){ 3081 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;'; 3082 } 3083 $i++; 3084 } 3085 return $table; 3086 } 3087 3088 /** 3089 * Determines if this page should be grayed out on the management/report screens 3090 * @return int 0 or 1 3091 */ 3092 protected function get_grayout() { 3093 return 0; 3094 } 3095 3096 /** 3097 * Adds stats for this page to the &pagestats object. This should be defined 3098 * for all page types that grade 3099 * @param array $pagestats 3100 * @param int $tries 3101 * @return bool 3102 */ 3103 public function stats(array &$pagestats, $tries) { 3104 return true; 3105 } 3106 3107 /** 3108 * Formats the answers of this page for a report 3109 * 3110 * @param object $answerpage 3111 * @param object $answerdata 3112 * @param object $useranswer 3113 * @param array $pagestats 3114 * @param int $i Count of first level answers 3115 * @param int $n Count of second level answers 3116 * @return object The answer page for this 3117 */ 3118 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) { 3119 $answers = $this->get_answers(); 3120 $formattextdefoptions = new stdClass; 3121 $formattextdefoptions->para = false; //I'll use it widely in this page 3122 foreach ($answers as $answer) { 3123 $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto)); 3124 $answerdata->answers[] = array($data, ""); 3125 $answerpage->answerdata = $answerdata; 3126 } 3127 return $answerpage; 3128 } 3129 3130 /** 3131 * Gets an array of the jumps used by the answers of this page 3132 * 3133 * @return array 3134 */ 3135 public function get_jumps() { 3136 global $DB; 3137 $jumps = array(); 3138 $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id); 3139 if ($answers = $this->get_answers()) { 3140 foreach ($answers as $answer) { 3141 $jumps[] = $this->get_jump_name($answer->jumpto); 3142 } 3143 } else { 3144 $jumps[] = $this->get_jump_name($this->properties->nextpageid); 3145 } 3146 return $jumps; 3147 } 3148 /** 3149 * Informs whether this page type require manual grading or not 3150 * @return bool 3151 */ 3152 public function requires_manual_grading() { 3153 return false; 3154 } 3155 3156 /** 3157 * A callback method that allows a page to override the next page a user will 3158 * see during when this page is being completed. 3159 * @return false|int 3160 */ 3161 public function override_next_page() { 3162 return false; 3163 } 3164 3165 /** 3166 * This method is used to determine if this page is a valid page 3167 * 3168 * @param array $validpages 3169 * @param array $pageviews 3170 * @return int The next page id to check 3171 */ 3172 public function valid_page_and_view(&$validpages, &$pageviews) { 3173 $validpages[$this->properties->id] = 1; 3174 return $this->properties->nextpageid; 3175 } 3176 } 3177 3178 3179 3180 /** 3181 * Class used to represent an answer to a page 3182 * 3183 * @property int $id The ID of this answer in the database 3184 * @property int $lessonid The ID of the lesson this answer belongs to 3185 * @property int $pageid The ID of the page this answer belongs to 3186 * @property int $jumpto Identifies where the user goes upon completing a page with this answer 3187 * @property int $grade The grade this answer is worth 3188 * @property int $score The score this answer will give 3189 * @property int $flags Used to store options for the answer 3190 * @property int $timecreated A timestamp of when the answer was created 3191 * @property int $timemodified A timestamp of when the answer was modified 3192 * @property string $answer The answer itself 3193 * @property string $response The response the user sees if selecting this answer 3194 * 3195 * @copyright 2009 Sam Hemelryk 3196 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 3197 */ 3198 class lesson_page_answer extends lesson_base { 3199 3200 /** 3201 * Loads an page answer from the DB 3202 * 3203 * @param int $id 3204 * @return lesson_page_answer 3205 */ 3206 public static function load($id) { 3207 global $DB; 3208 $answer = $DB->get_record("lesson_answers", array("id" => $id)); 3209 return new lesson_page_answer($answer); 3210 } 3211 3212 /** 3213 * Given an object of properties and a page created answer(s) and saves them 3214 * in the database. 3215 * 3216 * @param stdClass $properties 3217 * @param lesson_page $page 3218 * @return array 3219 */ 3220 public static function create($properties, lesson_page $page) { 3221 return $page->create_answers($properties); 3222 } 3223 3224 } 3225 3226 /** 3227 * A management class for page types 3228 * 3229 * This class is responsible for managing the different pages. A manager object can 3230 * be retrieved by calling the following line of code: 3231 * <code> 3232 * $manager = lesson_page_type_manager::get($lesson); 3233 * </code> 3234 * The first time the page type manager is retrieved the it includes all of the 3235 * different page types located in mod/lesson/pagetypes. 3236 * 3237 * @copyright 2009 Sam Hemelryk 3238 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 3239 */ 3240 class lesson_page_type_manager { 3241 3242 /** 3243 * An array of different page type classes 3244 * @var array 3245 */ 3246 protected $types = array(); 3247 3248 /** 3249 * Retrieves the lesson page type manager object 3250 * 3251 * If the object hasn't yet been created it is created here. 3252 * 3253 * @staticvar lesson_page_type_manager $pagetypemanager 3254 * @param lesson $lesson 3255 * @return lesson_page_type_manager 3256 */ 3257 public static function get(lesson $lesson) { 3258 static $pagetypemanager; 3259 if (!($pagetypemanager instanceof lesson_page_type_manager)) { 3260 $pagetypemanager = new lesson_page_type_manager(); 3261 $pagetypemanager->load_lesson_types($lesson); 3262 } 3263 return $pagetypemanager; 3264 } 3265 3266 /** 3267 * Finds and loads all lesson page types in mod/lesson/pagetypes 3268 * 3269 * @param lesson $lesson 3270 */ 3271 public function load_lesson_types(lesson $lesson) { 3272 global $CFG; 3273 $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/'; 3274 $dir = dir($basedir); 3275 while (false !== ($entry = $dir->read())) { 3276 if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) { 3277 continue; 3278 } 3279 require_once($basedir.$entry); 3280 $class = 'lesson_page_type_'.strtok($entry,'.'); 3281 if (class_exists($class)) { 3282 $pagetype = new $class(new stdClass, $lesson); 3283 $this->types[$pagetype->typeid] = $pagetype; 3284 } 3285 } 3286 3287 } 3288 3289 /** 3290 * Returns an array of strings to describe the loaded page types 3291 * 3292 * @param int $type Can be used to return JUST the string for the requested type 3293 * @return array 3294 */ 3295 public function get_page_type_strings($type=null, $special=true) { 3296 $types = array(); 3297 foreach ($this->types as $pagetype) { 3298 if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) { 3299 $types[$pagetype->typeid] = $pagetype->typestring; 3300 } 3301 } 3302 return $types; 3303 } 3304 3305 /** 3306 * Returns the basic string used to identify a page type provided with an id 3307 * 3308 * This string can be used to instantiate or identify the page type class. 3309 * If the page type id is unknown then 'unknown' is returned 3310 * 3311 * @param int $id 3312 * @return string 3313 */ 3314 public function get_page_type_idstring($id) { 3315 foreach ($this->types as $pagetype) { 3316 if ((int)$pagetype->typeid === (int)$id) { 3317 return $pagetype->idstring; 3318 } 3319 } 3320 return 'unknown'; 3321 } 3322 3323 /** 3324 * Loads a page for the provided lesson given it's id 3325 * 3326 * This function loads a page from the lesson when given both the lesson it belongs 3327 * to as well as the page's id. 3328 * If the page doesn't exist an error is thrown 3329 * 3330 * @param int $pageid The id of the page to load 3331 * @param lesson $lesson The lesson the page belongs to 3332 * @return lesson_page A class that extends lesson_page 3333 */ 3334 public function load_page($pageid, lesson $lesson) { 3335 global $DB; 3336 if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) { 3337 print_error('cannotfindpages', 'lesson'); 3338 } 3339 $pagetype = get_class($this->types[$page->qtype]); 3340 $page = new $pagetype($page, $lesson); 3341 return $page; 3342 } 3343 3344 /** 3345 * This function detects errors in the ordering between 2 pages and updates the page records. 3346 * 3347 * @param stdClass $page1 Either the first of 2 pages or null if the $page2 param is the first in the list. 3348 * @param stdClass $page1 Either the second of 2 pages or null if the $page1 param is the last in the list. 3349 */ 3350 protected function check_page_order($page1, $page2) { 3351 global $DB; 3352 if (empty($page1)) { 3353 if ($page2->prevpageid != 0) { 3354 debugging("***prevpageid of page " . $page2->id . " set to 0***"); 3355 $page2->prevpageid = 0; 3356 $DB->set_field("lesson_pages", "prevpageid", 0, array("id" => $page2->id)); 3357 } 3358 } else if (empty($page2)) { 3359 if ($page1->nextpageid != 0) { 3360 debugging("***nextpageid of page " . $page1->id . " set to 0***"); 3361 $page1->nextpageid = 0; 3362 $DB->set_field("lesson_pages", "nextpageid", 0, array("id" => $page1->id)); 3363 } 3364 } else { 3365 if ($page1->nextpageid != $page2->id) { 3366 debugging("***nextpageid of page " . $page1->id . " set to " . $page2->id . "***"); 3367 $page1->nextpageid = $page2->id; 3368 $DB->set_field("lesson_pages", "nextpageid", $page2->id, array("id" => $page1->id)); 3369 } 3370 if ($page2->prevpageid != $page1->id) { 3371 debugging("***prevpageid of page " . $page2->id . " set to " . $page1->id . "***"); 3372 $page2->prevpageid = $page1->id; 3373 $DB->set_field("lesson_pages", "prevpageid", $page1->id, array("id" => $page2->id)); 3374 } 3375 } 3376 } 3377 3378 /** 3379 * This function loads ALL pages that belong to the lesson. 3380 * 3381 * @param lesson $lesson 3382 * @return array An array of lesson_page_type_* 3383 */ 3384 public function load_all_pages(lesson $lesson) { 3385 global $DB; 3386 if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) { 3387 return array(); // Records returned empty. 3388 } 3389 foreach ($pages as $key=>$page) { 3390 $pagetype = get_class($this->types[$page->qtype]); 3391 $pages[$key] = new $pagetype($page, $lesson); 3392 } 3393 3394 $orderedpages = array(); 3395 $lastpageid = 0; 3396 $morepages = true; 3397 while ($morepages) { 3398 $morepages = false; 3399 foreach ($pages as $page) { 3400 if ((int)$page->prevpageid === (int)$lastpageid) { 3401 // Check for errors in page ordering and fix them on the fly. 3402 $prevpage = null; 3403 if ($lastpageid !== 0) { 3404 $prevpage = $orderedpages[$lastpageid]; 3405 } 3406 $this->check_page_order($prevpage, $page); 3407 $morepages = true; 3408 $orderedpages[$page->id] = $page; 3409 unset($pages[$page->id]); 3410 $lastpageid = $page->id; 3411 if ((int)$page->nextpageid===0) { 3412 break 2; 3413 } else { 3414 break 1; 3415 } 3416 } 3417 } 3418 } 3419 3420 // Add remaining pages and fix the nextpageid links for each page. 3421 foreach ($pages as $page) { 3422 // Check for errors in page ordering and fix them on the fly. 3423 $prevpage = null; 3424 if ($lastpageid !== 0) { 3425 $prevpage = $orderedpages[$lastpageid]; 3426 } 3427 $this->check_page_order($prevpage, $page); 3428 $orderedpages[$page->id] = $page; 3429 unset($pages[$page->id]); 3430 $lastpageid = $page->id; 3431 } 3432 3433 if ($lastpageid !== 0) { 3434 $this->check_page_order($orderedpages[$lastpageid], null); 3435 } 3436 3437 return $orderedpages; 3438 } 3439 3440 /** 3441 * Fetches an mform that can be used to create/edit an page 3442 * 3443 * @param int $type The id for the page type 3444 * @param array $arguments Any arguments to pass to the mform 3445 * @return lesson_add_page_form_base 3446 */ 3447 public function get_page_form($type, $arguments) { 3448 $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type); 3449 if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') { 3450 debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER); 3451 $class = 'lesson_add_page_form_selection'; 3452 } else if ($class === 'lesson_add_page_form_unknown') { 3453 $class = 'lesson_add_page_form_selection'; 3454 } 3455 return new $class(null, $arguments); 3456 } 3457 3458 /** 3459 * Returns an array of links to use as add page links 3460 * @param int $previd The id of the previous page 3461 * @return array 3462 */ 3463 public function get_add_page_type_links($previd) { 3464 global $OUTPUT; 3465 3466 $links = array(); 3467 3468 foreach ($this->types as $key=>$type) { 3469 if ($link = $type->add_page_link($previd)) { 3470 $links[$key] = $link; 3471 } 3472 } 3473 3474 return $links; 3475 } 3476 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |