See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]
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 * Standard library of functions and constants for lesson 20 * 21 * @package mod_lesson 22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 **/ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 // Event types. 29 define('LESSON_EVENT_TYPE_OPEN', 'open'); 30 define('LESSON_EVENT_TYPE_CLOSE', 'close'); 31 32 require_once (__DIR__ . '/deprecatedlib.php'); 33 /* Do not include any libraries here! */ 34 35 /** 36 * Given an object containing all the necessary data, 37 * (defined by the form in mod_form.php) this function 38 * will create a new instance and return the id number 39 * of the new instance. 40 * 41 * @global object 42 * @global object 43 * @param object $lesson Lesson post data from the form 44 * @return int 45 **/ 46 function lesson_add_instance($data, $mform) { 47 global $DB; 48 49 $cmid = $data->coursemodule; 50 $draftitemid = $data->mediafile; 51 $context = context_module::instance($cmid); 52 53 lesson_process_pre_save($data); 54 55 unset($data->mediafile); 56 $lessonid = $DB->insert_record("lesson", $data); 57 $data->id = $lessonid; 58 59 lesson_update_media_file($lessonid, $context, $draftitemid); 60 61 lesson_process_post_save($data); 62 63 lesson_grade_item_update($data); 64 65 return $lessonid; 66 } 67 68 /** 69 * Given an object containing all the necessary data, 70 * (defined by the form in mod_form.php) this function 71 * will update an existing instance with new data. 72 * 73 * @global object 74 * @param object $lesson Lesson post data from the form 75 * @return boolean 76 **/ 77 function lesson_update_instance($data, $mform) { 78 global $DB; 79 80 $data->id = $data->instance; 81 $cmid = $data->coursemodule; 82 $draftitemid = $data->mediafile; 83 $context = context_module::instance($cmid); 84 85 lesson_process_pre_save($data); 86 87 unset($data->mediafile); 88 $DB->update_record("lesson", $data); 89 90 lesson_update_media_file($data->id, $context, $draftitemid); 91 92 lesson_process_post_save($data); 93 94 // update grade item definition 95 lesson_grade_item_update($data); 96 97 // update grades - TODO: do it only when grading style changes 98 lesson_update_grades($data, 0, false); 99 100 return true; 101 } 102 103 /** 104 * This function updates the events associated to the lesson. 105 * If $override is non-zero, then it updates only the events 106 * associated with the specified override. 107 * 108 * @uses LESSON_MAX_EVENT_LENGTH 109 * @param object $lesson the lesson object. 110 * @param object $override (optional) limit to a specific override 111 */ 112 function lesson_update_events($lesson, $override = null) { 113 global $CFG, $DB; 114 115 require_once($CFG->dirroot . '/mod/lesson/locallib.php'); 116 require_once($CFG->dirroot . '/calendar/lib.php'); 117 118 // Load the old events relating to this lesson. 119 $conds = array('modulename' => 'lesson', 120 'instance' => $lesson->id); 121 if (!empty($override)) { 122 // Only load events for this override. 123 if (isset($override->userid)) { 124 $conds['userid'] = $override->userid; 125 } else { 126 $conds['groupid'] = $override->groupid; 127 } 128 } 129 $oldevents = $DB->get_records('event', $conds, 'id ASC'); 130 131 // Now make a to-do list of all that needs to be updated. 132 if (empty($override)) { 133 // We are updating the primary settings for the lesson, so we need to add all the overrides. 134 $overrides = $DB->get_records('lesson_overrides', array('lessonid' => $lesson->id), 'id ASC'); 135 // It is necessary to add an empty stdClass to the beginning of the array as the $oldevents 136 // list contains the original (non-override) event for the module. If this is not included 137 // the logic below will end up updating the wrong row when we try to reconcile this $overrides 138 // list against the $oldevents list. 139 array_unshift($overrides, new stdClass()); 140 } else { 141 // Just do the one override. 142 $overrides = array($override); 143 } 144 145 // Get group override priorities. 146 $grouppriorities = lesson_get_group_override_priorities($lesson->id); 147 148 foreach ($overrides as $current) { 149 $groupid = isset($current->groupid) ? $current->groupid : 0; 150 $userid = isset($current->userid) ? $current->userid : 0; 151 $available = isset($current->available) ? $current->available : $lesson->available; 152 $deadline = isset($current->deadline) ? $current->deadline : $lesson->deadline; 153 154 // Only add open/close events for an override if they differ from the lesson default. 155 $addopen = empty($current->id) || !empty($current->available); 156 $addclose = empty($current->id) || !empty($current->deadline); 157 158 if (!empty($lesson->coursemodule)) { 159 $cmid = $lesson->coursemodule; 160 } else { 161 $cmid = get_coursemodule_from_instance('lesson', $lesson->id, $lesson->course)->id; 162 } 163 164 $event = new stdClass(); 165 $event->type = !$deadline ? CALENDAR_EVENT_TYPE_ACTION : CALENDAR_EVENT_TYPE_STANDARD; 166 $event->description = format_module_intro('lesson', $lesson, $cmid, false); 167 $event->format = FORMAT_HTML; 168 // Events module won't show user events when the courseid is nonzero. 169 $event->courseid = ($userid) ? 0 : $lesson->course; 170 $event->groupid = $groupid; 171 $event->userid = $userid; 172 $event->modulename = 'lesson'; 173 $event->instance = $lesson->id; 174 $event->timestart = $available; 175 $event->timeduration = max($deadline - $available, 0); 176 $event->timesort = $available; 177 $event->visible = instance_is_visible('lesson', $lesson); 178 $event->eventtype = LESSON_EVENT_TYPE_OPEN; 179 $event->priority = null; 180 181 // Determine the event name and priority. 182 if ($groupid) { 183 // Group override event. 184 $params = new stdClass(); 185 $params->lesson = $lesson->name; 186 $params->group = groups_get_group_name($groupid); 187 if ($params->group === false) { 188 // Group doesn't exist, just skip it. 189 continue; 190 } 191 $eventname = get_string('overridegroupeventname', 'lesson', $params); 192 // Set group override priority. 193 if ($grouppriorities !== null) { 194 $openpriorities = $grouppriorities['open']; 195 if (isset($openpriorities[$available])) { 196 $event->priority = $openpriorities[$available]; 197 } 198 } 199 } else if ($userid) { 200 // User override event. 201 $params = new stdClass(); 202 $params->lesson = $lesson->name; 203 $eventname = get_string('overrideusereventname', 'lesson', $params); 204 // Set user override priority. 205 $event->priority = CALENDAR_EVENT_USER_OVERRIDE_PRIORITY; 206 } else { 207 // The parent event. 208 $eventname = $lesson->name; 209 } 210 211 if ($addopen or $addclose) { 212 // Separate start and end events. 213 $event->timeduration = 0; 214 if ($available && $addopen) { 215 if ($oldevent = array_shift($oldevents)) { 216 $event->id = $oldevent->id; 217 } else { 218 unset($event->id); 219 } 220 $event->name = get_string('lessoneventopens', 'lesson', $eventname); 221 // The method calendar_event::create will reuse a db record if the id field is set. 222 calendar_event::create($event, false); 223 } 224 if ($deadline && $addclose) { 225 if ($oldevent = array_shift($oldevents)) { 226 $event->id = $oldevent->id; 227 } else { 228 unset($event->id); 229 } 230 $event->type = CALENDAR_EVENT_TYPE_ACTION; 231 $event->name = get_string('lessoneventcloses', 'lesson', $eventname); 232 $event->timestart = $deadline; 233 $event->timesort = $deadline; 234 $event->eventtype = LESSON_EVENT_TYPE_CLOSE; 235 if ($groupid && $grouppriorities !== null) { 236 $closepriorities = $grouppriorities['close']; 237 if (isset($closepriorities[$deadline])) { 238 $event->priority = $closepriorities[$deadline]; 239 } 240 } 241 calendar_event::create($event, false); 242 } 243 } 244 } 245 246 // Delete any leftover events. 247 foreach ($oldevents as $badevent) { 248 $badevent = calendar_event::load($badevent); 249 $badevent->delete(); 250 } 251 } 252 253 /** 254 * Calculates the priorities of timeopen and timeclose values for group overrides for a lesson. 255 * 256 * @param int $lessonid The lesson ID. 257 * @return array|null Array of group override priorities for open and close times. Null if there are no group overrides. 258 */ 259 function lesson_get_group_override_priorities($lessonid) { 260 global $DB; 261 262 // Fetch group overrides. 263 $where = 'lessonid = :lessonid AND groupid IS NOT NULL'; 264 $params = ['lessonid' => $lessonid]; 265 $overrides = $DB->get_records_select('lesson_overrides', $where, $params, '', 'id, groupid, available, deadline'); 266 if (!$overrides) { 267 return null; 268 } 269 270 $grouptimeopen = []; 271 $grouptimeclose = []; 272 foreach ($overrides as $override) { 273 if ($override->available !== null && !in_array($override->available, $grouptimeopen)) { 274 $grouptimeopen[] = $override->available; 275 } 276 if ($override->deadline !== null && !in_array($override->deadline, $grouptimeclose)) { 277 $grouptimeclose[] = $override->deadline; 278 } 279 } 280 281 // Sort open times in ascending manner. The earlier open time gets higher priority. 282 sort($grouptimeopen); 283 // Set priorities. 284 $opengrouppriorities = []; 285 $openpriority = 1; 286 foreach ($grouptimeopen as $timeopen) { 287 $opengrouppriorities[$timeopen] = $openpriority++; 288 } 289 290 // Sort close times in descending manner. The later close time gets higher priority. 291 rsort($grouptimeclose); 292 // Set priorities. 293 $closegrouppriorities = []; 294 $closepriority = 1; 295 foreach ($grouptimeclose as $timeclose) { 296 $closegrouppriorities[$timeclose] = $closepriority++; 297 } 298 299 return [ 300 'open' => $opengrouppriorities, 301 'close' => $closegrouppriorities 302 ]; 303 } 304 305 /** 306 * This standard function will check all instances of this module 307 * and make sure there are up-to-date events created for each of them. 308 * If courseid = 0, then every lesson event in the site is checked, else 309 * only lesson events belonging to the course specified are checked. 310 * This function is used, in its new format, by restore_refresh_events() 311 * 312 * @param int $courseid 313 * @param int|stdClass $instance Lesson module instance or ID. 314 * @param int|stdClass $cm Course module object or ID (not used in this module). 315 * @return bool 316 */ 317 function lesson_refresh_events($courseid = 0, $instance = null, $cm = null) { 318 global $DB; 319 320 // If we have instance information then we can just update the one event instead of updating all events. 321 if (isset($instance)) { 322 if (!is_object($instance)) { 323 $instance = $DB->get_record('lesson', array('id' => $instance), '*', MUST_EXIST); 324 } 325 lesson_update_events($instance); 326 return true; 327 } 328 329 if ($courseid == 0) { 330 if (!$lessons = $DB->get_records('lesson')) { 331 return true; 332 } 333 } else { 334 if (!$lessons = $DB->get_records('lesson', array('course' => $courseid))) { 335 return true; 336 } 337 } 338 339 foreach ($lessons as $lesson) { 340 lesson_update_events($lesson); 341 } 342 343 return true; 344 } 345 346 /** 347 * Given an ID of an instance of this module, 348 * this function will permanently delete the instance 349 * and any data that depends on it. 350 * 351 * @global object 352 * @param int $id 353 * @return bool 354 */ 355 function lesson_delete_instance($id) { 356 global $DB, $CFG; 357 require_once($CFG->dirroot . '/mod/lesson/locallib.php'); 358 359 $lesson = $DB->get_record("lesson", array("id"=>$id), '*', MUST_EXIST); 360 $lesson = new lesson($lesson); 361 return $lesson->delete(); 362 } 363 364 /** 365 * Return a small object with summary information about what a 366 * user has done with a given particular instance of this module 367 * Used for user activity reports. 368 * $return->time = the time they did it 369 * $return->info = a short text description 370 * 371 * @global object 372 * @param object $course 373 * @param object $user 374 * @param object $mod 375 * @param object $lesson 376 * @return object 377 */ 378 function lesson_user_outline($course, $user, $mod, $lesson) { 379 global $CFG, $DB; 380 381 require_once("$CFG->libdir/gradelib.php"); 382 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id); 383 $return = new stdClass(); 384 385 if (empty($grades->items[0]->grades)) { 386 $return->info = get_string("nolessonattempts", "lesson"); 387 } else { 388 $grade = reset($grades->items[0]->grades); 389 if (empty($grade->grade)) { 390 391 // Check to see if it an ungraded / incomplete attempt. 392 $sql = "SELECT * 393 FROM {lesson_timer} 394 WHERE lessonid = :lessonid 395 AND userid = :userid 396 ORDER BY starttime DESC"; 397 $params = array('lessonid' => $lesson->id, 'userid' => $user->id); 398 399 if ($attempts = $DB->get_records_sql($sql, $params, 0, 1)) { 400 $attempt = reset($attempts); 401 if ($attempt->completed) { 402 $return->info = get_string("completed", "lesson"); 403 } else { 404 $return->info = get_string("notyetcompleted", "lesson"); 405 } 406 $return->time = $attempt->lessontime; 407 } else { 408 $return->info = get_string("nolessonattempts", "lesson"); 409 } 410 } else { 411 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 412 $return->info = get_string('gradenoun') . ': ' . $grade->str_long_grade; 413 } else { 414 $return->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); 415 } 416 417 $return->time = grade_get_date_for_user_grade($grade, $user); 418 } 419 } 420 return $return; 421 } 422 423 /** 424 * Print a detailed representation of what a user has done with 425 * a given particular instance of this module, for user activity reports. 426 * 427 * @global object 428 * @param object $course 429 * @param object $user 430 * @param object $mod 431 * @param object $lesson 432 * @return bool 433 */ 434 function lesson_user_complete($course, $user, $mod, $lesson) { 435 global $DB, $OUTPUT, $CFG; 436 437 require_once("$CFG->libdir/gradelib.php"); 438 439 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id); 440 441 // Display the grade and feedback. 442 if (empty($grades->items[0]->grades)) { 443 echo $OUTPUT->container(get_string("nolessonattempts", "lesson")); 444 } else { 445 $grade = reset($grades->items[0]->grades); 446 if (empty($grade->grade)) { 447 // Check to see if it an ungraded / incomplete attempt. 448 $sql = "SELECT * 449 FROM {lesson_timer} 450 WHERE lessonid = :lessonid 451 AND userid = :userid 452 ORDER by starttime desc"; 453 $params = array('lessonid' => $lesson->id, 'userid' => $user->id); 454 455 if ($attempt = $DB->get_record_sql($sql, $params, IGNORE_MULTIPLE)) { 456 if ($attempt->completed) { 457 $status = get_string("completed", "lesson"); 458 } else { 459 $status = get_string("notyetcompleted", "lesson"); 460 } 461 } else { 462 $status = get_string("nolessonattempts", "lesson"); 463 } 464 } else { 465 if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) { 466 $status = get_string('gradenoun') . ': ' . $grade->str_long_grade; 467 } else { 468 $status = get_string('gradenoun') . ': ' . get_string('hidden', 'grades'); 469 } 470 } 471 472 // Display the grade or lesson status if there isn't one. 473 echo $OUTPUT->container($status); 474 475 if ($grade->str_feedback && 476 (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id)))) { 477 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback); 478 } 479 } 480 481 // Display the lesson progress. 482 // Attempt, pages viewed, questions answered, correct answers, time. 483 $params = array ("lessonid" => $lesson->id, "userid" => $user->id); 484 $attempts = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND userid = :userid", $params, "retry, timeseen"); 485 $branches = $DB->get_records_select("lesson_branch", "lessonid = :lessonid AND userid = :userid", $params, "retry, timeseen"); 486 if (!empty($attempts) or !empty($branches)) { 487 echo $OUTPUT->box_start(); 488 $table = new html_table(); 489 // Table Headings. 490 $table->head = array (get_string("attemptheader", "lesson"), 491 get_string("totalpagesviewedheader", "lesson"), 492 get_string("numberofpagesviewedheader", "lesson"), 493 get_string("numberofcorrectanswersheader", "lesson"), 494 get_string("time")); 495 $table->width = "100%"; 496 $table->align = array ("center", "center", "center", "center", "center"); 497 $table->size = array ("*", "*", "*", "*", "*"); 498 $table->cellpadding = 2; 499 $table->cellspacing = 0; 500 501 $retry = 0; 502 $nquestions = 0; 503 $npages = 0; 504 $ncorrect = 0; 505 506 // Filter question pages (from lesson_attempts). 507 foreach ($attempts as $attempt) { 508 if ($attempt->retry == $retry) { 509 $npages++; 510 $nquestions++; 511 if ($attempt->correct) { 512 $ncorrect++; 513 } 514 $timeseen = $attempt->timeseen; 515 } else { 516 $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen)); 517 $retry++; 518 $nquestions = 1; 519 $npages = 1; 520 if ($attempt->correct) { 521 $ncorrect = 1; 522 } else { 523 $ncorrect = 0; 524 } 525 } 526 } 527 528 // Filter content pages (from lesson_branch). 529 foreach ($branches as $branch) { 530 if ($branch->retry == $retry) { 531 $npages++; 532 533 $timeseen = $branch->timeseen; 534 } else { 535 $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen)); 536 $retry++; 537 $npages = 1; 538 } 539 } 540 if ($npages > 0) { 541 $table->data[] = array($retry + 1, $npages, $nquestions, $ncorrect, userdate($timeseen)); 542 } 543 echo html_writer::table($table); 544 echo $OUTPUT->box_end(); 545 } 546 547 return true; 548 } 549 550 /** 551 * @deprecated since Moodle 3.3, when the block_course_overview block was removed. 552 */ 553 function lesson_print_overview() { 554 throw new coding_exception('lesson_print_overview() can not be used any more and is obsolete.'); 555 } 556 557 /** 558 * Function to be run periodically according to the moodle cron 559 * This function searches for things that need to be done, such 560 * as sending out mail, toggling flags etc ... 561 * @global stdClass 562 * @return bool true 563 */ 564 function lesson_cron () { 565 global $CFG; 566 567 return true; 568 } 569 570 /** 571 * Return grade for given user or all users. 572 * 573 * @global stdClass 574 * @global object 575 * @param int $lessonid id of lesson 576 * @param int $userid optional user id, 0 means all users 577 * @return array array of grades, false if none 578 */ 579 function lesson_get_user_grades($lesson, $userid=0) { 580 global $CFG, $DB; 581 582 $params = array("lessonid" => $lesson->id,"lessonid2" => $lesson->id); 583 584 if (!empty($userid)) { 585 $params["userid"] = $userid; 586 $params["userid2"] = $userid; 587 $user = "AND u.id = :userid"; 588 $fuser = "AND uu.id = :userid2"; 589 } 590 else { 591 $user=""; 592 $fuser=""; 593 } 594 595 if ($lesson->retake) { 596 if ($lesson->usemaxgrade) { 597 $sql = "SELECT u.id, u.id AS userid, MAX(g.grade) AS rawgrade 598 FROM {user} u, {lesson_grades} g 599 WHERE u.id = g.userid AND g.lessonid = :lessonid 600 $user 601 GROUP BY u.id"; 602 } else { 603 $sql = "SELECT u.id, u.id AS userid, AVG(g.grade) AS rawgrade 604 FROM {user} u, {lesson_grades} g 605 WHERE u.id = g.userid AND g.lessonid = :lessonid 606 $user 607 GROUP BY u.id"; 608 } 609 unset($params['lessonid2']); 610 unset($params['userid2']); 611 } else { 612 // use only first attempts (with lowest id in lesson_grades table) 613 $firstonly = "SELECT uu.id AS userid, MIN(gg.id) AS firstcompleted 614 FROM {user} uu, {lesson_grades} gg 615 WHERE uu.id = gg.userid AND gg.lessonid = :lessonid2 616 $fuser 617 GROUP BY uu.id"; 618 619 $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade 620 FROM {user} u, {lesson_grades} g, ($firstonly) f 621 WHERE u.id = g.userid AND g.lessonid = :lessonid 622 AND g.id = f.firstcompleted AND g.userid=f.userid 623 $user"; 624 } 625 626 return $DB->get_records_sql($sql, $params); 627 } 628 629 /** 630 * Update grades in central gradebook 631 * 632 * @category grade 633 * @param object $lesson 634 * @param int $userid specific user only, 0 means all 635 * @param bool $nullifnone 636 */ 637 function lesson_update_grades($lesson, $userid=0, $nullifnone=true) { 638 global $CFG, $DB; 639 require_once($CFG->libdir.'/gradelib.php'); 640 641 if ($lesson->grade == 0 || $lesson->practice) { 642 lesson_grade_item_update($lesson); 643 644 } else if ($grades = lesson_get_user_grades($lesson, $userid)) { 645 lesson_grade_item_update($lesson, $grades); 646 647 } else if ($userid and $nullifnone) { 648 $grade = new stdClass(); 649 $grade->userid = $userid; 650 $grade->rawgrade = null; 651 lesson_grade_item_update($lesson, $grade); 652 653 } else { 654 lesson_grade_item_update($lesson); 655 } 656 } 657 658 /** 659 * Create grade item for given lesson 660 * 661 * @category grade 662 * @uses GRADE_TYPE_VALUE 663 * @uses GRADE_TYPE_NONE 664 * @param object $lesson object with extra cmidnumber 665 * @param array|object $grades optional array/object of grade(s); 'reset' means reset grades in gradebook 666 * @return int 0 if ok, error code otherwise 667 */ 668 function lesson_grade_item_update($lesson, $grades=null) { 669 global $CFG; 670 if (!function_exists('grade_update')) { //workaround for buggy PHP versions 671 require_once($CFG->libdir.'/gradelib.php'); 672 } 673 674 if (property_exists($lesson, 'cmidnumber')) { //it may not be always present 675 $params = array('itemname'=>$lesson->name, 'idnumber'=>$lesson->cmidnumber); 676 } else { 677 $params = array('itemname'=>$lesson->name); 678 } 679 680 if (!$lesson->practice and $lesson->grade > 0) { 681 $params['gradetype'] = GRADE_TYPE_VALUE; 682 $params['grademax'] = $lesson->grade; 683 $params['grademin'] = 0; 684 } else if (!$lesson->practice and $lesson->grade < 0) { 685 $params['gradetype'] = GRADE_TYPE_SCALE; 686 $params['scaleid'] = -$lesson->grade; 687 688 // Make sure current grade fetched correctly from $grades 689 $currentgrade = null; 690 if (!empty($grades)) { 691 if (is_array($grades)) { 692 $currentgrade = reset($grades); 693 } else { 694 $currentgrade = $grades; 695 } 696 } 697 698 // When converting a score to a scale, use scale's grade maximum to calculate it. 699 if (!empty($currentgrade) && $currentgrade->rawgrade !== null) { 700 $grade = grade_get_grades($lesson->course, 'mod', 'lesson', $lesson->id, $currentgrade->userid); 701 $params['grademax'] = reset($grade->items)->grademax; 702 } 703 } else { 704 $params['gradetype'] = GRADE_TYPE_NONE; 705 } 706 707 if ($grades === 'reset') { 708 $params['reset'] = true; 709 $grades = null; 710 } else if (!empty($grades)) { 711 // Need to calculate raw grade (Note: $grades has many forms) 712 if (is_object($grades)) { 713 $grades = array($grades->userid => $grades); 714 } else if (array_key_exists('userid', $grades)) { 715 $grades = array($grades['userid'] => $grades); 716 } 717 foreach ($grades as $key => $grade) { 718 if (!is_array($grade)) { 719 $grades[$key] = $grade = (array) $grade; 720 } 721 //check raw grade isnt null otherwise we erroneously insert a grade of 0 722 if ($grade['rawgrade'] !== null) { 723 $grades[$key]['rawgrade'] = ($grade['rawgrade'] * $params['grademax'] / 100); 724 } else { 725 //setting rawgrade to null just in case user is deleting a grade 726 $grades[$key]['rawgrade'] = null; 727 } 728 } 729 } 730 731 return grade_update('mod/lesson', $lesson->course, 'mod', 'lesson', $lesson->id, 0, $grades, $params); 732 } 733 734 /** 735 * List the actions that correspond to a view of this module. 736 * This is used by the participation report. 737 * 738 * Note: This is not used by new logging system. Event with 739 * crud = 'r' and edulevel = LEVEL_PARTICIPATING will 740 * be considered as view action. 741 * 742 * @return array 743 */ 744 function lesson_get_view_actions() { 745 return array('view','view all'); 746 } 747 748 /** 749 * List the actions that correspond to a post of this module. 750 * This is used by the participation report. 751 * 752 * Note: This is not used by new logging system. Event with 753 * crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING 754 * will be considered as post action. 755 * 756 * @return array 757 */ 758 function lesson_get_post_actions() { 759 return array('end','start'); 760 } 761 762 /** 763 * Runs any processes that must run before 764 * a lesson insert/update 765 * 766 * @global object 767 * @param object $lesson Lesson form data 768 * @return void 769 **/ 770 function lesson_process_pre_save(&$lesson) { 771 global $DB; 772 773 $lesson->timemodified = time(); 774 775 if (empty($lesson->timelimit)) { 776 $lesson->timelimit = 0; 777 } 778 if (empty($lesson->timespent) or !is_numeric($lesson->timespent) or $lesson->timespent < 0) { 779 $lesson->timespent = 0; 780 } 781 if (!isset($lesson->completed)) { 782 $lesson->completed = 0; 783 } 784 if (empty($lesson->gradebetterthan) or !is_numeric($lesson->gradebetterthan) or $lesson->gradebetterthan < 0) { 785 $lesson->gradebetterthan = 0; 786 } else if ($lesson->gradebetterthan > 100) { 787 $lesson->gradebetterthan = 100; 788 } 789 790 if (empty($lesson->width)) { 791 $lesson->width = 640; 792 } 793 if (empty($lesson->height)) { 794 $lesson->height = 480; 795 } 796 if (empty($lesson->bgcolor)) { 797 $lesson->bgcolor = '#FFFFFF'; 798 } 799 800 // Conditions for dependency 801 $conditions = new stdClass; 802 $conditions->timespent = $lesson->timespent; 803 $conditions->completed = $lesson->completed; 804 $conditions->gradebetterthan = $lesson->gradebetterthan; 805 $lesson->conditions = serialize($conditions); 806 unset($lesson->timespent); 807 unset($lesson->completed); 808 unset($lesson->gradebetterthan); 809 810 if (empty($lesson->password)) { 811 unset($lesson->password); 812 } 813 } 814 815 /** 816 * Runs any processes that must be run 817 * after a lesson insert/update 818 * 819 * @global object 820 * @param object $lesson Lesson form data 821 * @return void 822 **/ 823 function lesson_process_post_save(&$lesson) { 824 // Update the events relating to this lesson. 825 lesson_update_events($lesson); 826 $completionexpected = (!empty($lesson->completionexpected)) ? $lesson->completionexpected : null; 827 \core_completion\api::update_completion_date_event($lesson->coursemodule, 'lesson', $lesson, $completionexpected); 828 } 829 830 831 /** 832 * Implementation of the function for printing the form elements that control 833 * whether the course reset functionality affects the lesson. 834 * 835 * @param $mform form passed by reference 836 */ 837 function lesson_reset_course_form_definition(&$mform) { 838 $mform->addElement('header', 'lessonheader', get_string('modulenameplural', 'lesson')); 839 $mform->addElement('advcheckbox', 'reset_lesson', get_string('deleteallattempts','lesson')); 840 $mform->addElement('advcheckbox', 'reset_lesson_user_overrides', 841 get_string('removealluseroverrides', 'lesson')); 842 $mform->addElement('advcheckbox', 'reset_lesson_group_overrides', 843 get_string('removeallgroupoverrides', 'lesson')); 844 } 845 846 /** 847 * Course reset form defaults. 848 * @param object $course 849 * @return array 850 */ 851 function lesson_reset_course_form_defaults($course) { 852 return array('reset_lesson' => 1, 853 'reset_lesson_group_overrides' => 1, 854 'reset_lesson_user_overrides' => 1); 855 } 856 857 /** 858 * Removes all grades from gradebook 859 * 860 * @global stdClass 861 * @global object 862 * @param int $courseid 863 * @param string optional type 864 */ 865 function lesson_reset_gradebook($courseid, $type='') { 866 global $CFG, $DB; 867 868 $sql = "SELECT l.*, cm.idnumber as cmidnumber, l.course as courseid 869 FROM {lesson} l, {course_modules} cm, {modules} m 870 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id AND l.course=:course"; 871 $params = array ("course" => $courseid); 872 if ($lessons = $DB->get_records_sql($sql,$params)) { 873 foreach ($lessons as $lesson) { 874 lesson_grade_item_update($lesson, 'reset'); 875 } 876 } 877 } 878 879 /** 880 * Actual implementation of the reset course functionality, delete all the 881 * lesson attempts for course $data->courseid. 882 * 883 * @global stdClass 884 * @global object 885 * @param object $data the data submitted from the reset course. 886 * @return array status array 887 */ 888 function lesson_reset_userdata($data) { 889 global $CFG, $DB; 890 891 $componentstr = get_string('modulenameplural', 'lesson'); 892 $status = array(); 893 894 if (!empty($data->reset_lesson)) { 895 $lessonssql = "SELECT l.id 896 FROM {lesson} l 897 WHERE l.course=:course"; 898 899 $params = array ("course" => $data->courseid); 900 $lessons = $DB->get_records_sql($lessonssql, $params); 901 902 // Get rid of attempts files. 903 $fs = get_file_storage(); 904 if ($lessons) { 905 foreach ($lessons as $lessonid => $unused) { 906 if (!$cm = get_coursemodule_from_instance('lesson', $lessonid)) { 907 continue; 908 } 909 $context = context_module::instance($cm->id); 910 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_responses'); 911 $fs->delete_area_files($context->id, 'mod_lesson', 'essay_answers'); 912 } 913 } 914 915 $DB->delete_records_select('lesson_timer', "lessonid IN ($lessonssql)", $params); 916 $DB->delete_records_select('lesson_grades', "lessonid IN ($lessonssql)", $params); 917 $DB->delete_records_select('lesson_attempts', "lessonid IN ($lessonssql)", $params); 918 $DB->delete_records_select('lesson_branch', "lessonid IN ($lessonssql)", $params); 919 920 // remove all grades from gradebook 921 if (empty($data->reset_gradebook_grades)) { 922 lesson_reset_gradebook($data->courseid); 923 } 924 925 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false); 926 } 927 928 $purgeoverrides = false; 929 930 // Remove user overrides. 931 if (!empty($data->reset_lesson_user_overrides)) { 932 $DB->delete_records_select('lesson_overrides', 933 'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND userid IS NOT NULL', array($data->courseid)); 934 $status[] = array( 935 'component' => $componentstr, 936 'item' => get_string('useroverridesdeleted', 'lesson'), 937 'error' => false); 938 $purgeoverrides = true; 939 } 940 // Remove group overrides. 941 if (!empty($data->reset_lesson_group_overrides)) { 942 $DB->delete_records_select('lesson_overrides', 943 'lessonid IN (SELECT id FROM {lesson} WHERE course = ?) AND groupid IS NOT NULL', array($data->courseid)); 944 $status[] = array( 945 'component' => $componentstr, 946 'item' => get_string('groupoverridesdeleted', 'lesson'), 947 'error' => false); 948 $purgeoverrides = true; 949 } 950 /// updating dates - shift may be negative too 951 if ($data->timeshift) { 952 $DB->execute("UPDATE {lesson_overrides} 953 SET available = available + ? 954 WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?) 955 AND available <> 0", array($data->timeshift, $data->courseid)); 956 $DB->execute("UPDATE {lesson_overrides} 957 SET deadline = deadline + ? 958 WHERE lessonid IN (SELECT id FROM {lesson} WHERE course = ?) 959 AND deadline <> 0", array($data->timeshift, $data->courseid)); 960 961 $purgeoverrides = true; 962 963 // Any changes to the list of dates that needs to be rolled should be same during course restore and course reset. 964 // See MDL-9367. 965 shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid); 966 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false); 967 } 968 969 if ($purgeoverrides) { 970 cache::make('mod_lesson', 'overrides')->purge(); 971 } 972 973 return $status; 974 } 975 976 /** 977 * @uses FEATURE_GROUPS 978 * @uses FEATURE_GROUPINGS 979 * @uses FEATURE_MOD_INTRO 980 * @uses FEATURE_COMPLETION_TRACKS_VIEWS 981 * @uses FEATURE_GRADE_HAS_GRADE 982 * @uses FEATURE_GRADE_OUTCOMES 983 * @param string $feature FEATURE_xx constant for requested feature 984 * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. 985 */ 986 function lesson_supports($feature) { 987 switch($feature) { 988 case FEATURE_GROUPS: 989 return true; 990 case FEATURE_GROUPINGS: 991 return true; 992 case FEATURE_MOD_INTRO: 993 return true; 994 case FEATURE_COMPLETION_TRACKS_VIEWS: 995 return true; 996 case FEATURE_GRADE_HAS_GRADE: 997 return true; 998 case FEATURE_COMPLETION_HAS_RULES: 999 return true; 1000 case FEATURE_GRADE_OUTCOMES: 1001 return true; 1002 case FEATURE_BACKUP_MOODLE2: 1003 return true; 1004 case FEATURE_SHOW_DESCRIPTION: 1005 return true; 1006 case FEATURE_MOD_PURPOSE: 1007 return MOD_PURPOSE_CONTENT; 1008 default: 1009 return null; 1010 } 1011 } 1012 1013 /** 1014 * This function extends the settings navigation block for the site. 1015 * 1016 * It is safe to rely on PAGE here as we will only ever be within the module 1017 * context when this is called 1018 * 1019 * @param settings_navigation $settings 1020 * @param navigation_node $lessonnode 1021 */ 1022 function lesson_extend_settings_navigation(settings_navigation $settings, navigation_node $lessonnode) { 1023 // We want to add these new nodes after the Edit settings node, and before the 1024 // Locally assigned roles node. Of course, both of those are controlled by capabilities. 1025 $keys = $lessonnode->get_children_key_list(); 1026 $beforekey = null; 1027 $i = array_search('modedit', $keys); 1028 if ($i === false and array_key_exists(0, $keys)) { 1029 $beforekey = $keys[0]; 1030 } else if (array_key_exists($i + 1, $keys)) { 1031 $beforekey = $keys[$i + 1]; 1032 } 1033 1034 if (has_capability('mod/lesson:manageoverrides', $settings->get_page()->cm->context)) { 1035 $url = new moodle_url('/mod/lesson/overrides.php', ['cmid' => $settings->get_page()->cm->id, 'mode' => 'user']); 1036 $node = navigation_node::create(get_string('overrides', 'lesson'), $url, 1037 navigation_node::TYPE_SETTING, null, 'mod_lesson_useroverrides'); 1038 $lessonnode->add_node($node, $beforekey); 1039 } 1040 1041 if (has_capability('mod/lesson:viewreports', $settings->get_page()->cm->context)) { 1042 $reportsnode = $lessonnode->add( 1043 get_string('reports', 'lesson'), 1044 new moodle_url('/mod/lesson/report.php', ['id' => $settings->get_page()->cm->id, 1045 'action' => 'reportoverview']) 1046 ); 1047 } 1048 } 1049 1050 /** 1051 * Get list of available import or export formats 1052 * 1053 * Copied and modified from lib/questionlib.php 1054 * 1055 * @param string $type 'import' if import list, otherwise export list assumed 1056 * @return array sorted list of import/export formats available 1057 */ 1058 function lesson_get_import_export_formats($type) { 1059 global $CFG; 1060 $fileformats = core_component::get_plugin_list("qformat"); 1061 1062 $fileformatname=array(); 1063 foreach ($fileformats as $fileformat=>$fdir) { 1064 $format_file = "$fdir/format.php"; 1065 if (file_exists($format_file) ) { 1066 require_once($format_file); 1067 } else { 1068 continue; 1069 } 1070 $classname = "qformat_$fileformat"; 1071 $format_class = new $classname(); 1072 if ($type=='import') { 1073 $provided = $format_class->provide_import(); 1074 } else { 1075 $provided = $format_class->provide_export(); 1076 } 1077 if ($provided) { 1078 $fileformatnames[$fileformat] = get_string('pluginname', 'qformat_'.$fileformat); 1079 } 1080 } 1081 natcasesort($fileformatnames); 1082 1083 return $fileformatnames; 1084 } 1085 1086 /** 1087 * Serves the lesson attachments. Implements needed access control ;-) 1088 * 1089 * @package mod_lesson 1090 * @category files 1091 * @param stdClass $course course object 1092 * @param stdClass $cm course module object 1093 * @param stdClass $context context object 1094 * @param string $filearea file area 1095 * @param array $args extra arguments 1096 * @param bool $forcedownload whether or not force download 1097 * @param array $options additional options affecting the file serving 1098 * @return bool false if file not found, does not return if found - justsend the file 1099 */ 1100 function lesson_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { 1101 global $CFG, $DB; 1102 1103 if ($context->contextlevel != CONTEXT_MODULE) { 1104 return false; 1105 } 1106 1107 $fileareas = lesson_get_file_areas(); 1108 if (!array_key_exists($filearea, $fileareas)) { 1109 return false; 1110 } 1111 1112 if (!$lesson = $DB->get_record('lesson', array('id'=>$cm->instance))) { 1113 return false; 1114 } 1115 1116 require_course_login($course, true, $cm); 1117 1118 if ($filearea === 'page_contents') { 1119 $pageid = (int)array_shift($args); 1120 if (!$page = $DB->get_record('lesson_pages', array('id'=>$pageid))) { 1121 return false; 1122 } 1123 $fullpath = "/$context->id/mod_lesson/$filearea/$pageid/".implode('/', $args); 1124 1125 } else if ($filearea === 'page_answers' || $filearea === 'page_responses') { 1126 $itemid = (int)array_shift($args); 1127 if (!$pageanswers = $DB->get_record('lesson_answers', array('id' => $itemid))) { 1128 return false; 1129 } 1130 $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args); 1131 1132 } else if ($filearea === 'essay_responses' || $filearea === 'essay_answers') { 1133 $itemid = (int)array_shift($args); 1134 if (!$attempt = $DB->get_record('lesson_attempts', array('id' => $itemid))) { 1135 return false; 1136 } 1137 $fullpath = "/$context->id/mod_lesson/$filearea/$itemid/".implode('/', $args); 1138 1139 } else if ($filearea === 'mediafile') { 1140 if (count($args) > 1) { 1141 // Remove the itemid when it appears to be part of the arguments. If there is only one argument 1142 // then it is surely the file name. The itemid is sometimes used to prevent browser caching. 1143 array_shift($args); 1144 } 1145 $fullpath = "/$context->id/mod_lesson/$filearea/0/".implode('/', $args); 1146 1147 } else { 1148 return false; 1149 } 1150 1151 $fs = get_file_storage(); 1152 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) { 1153 return false; 1154 } 1155 1156 // finally send the file 1157 send_stored_file($file, 0, 0, $forcedownload, $options); // download MUST be forced - security! 1158 } 1159 1160 /** 1161 * Returns an array of file areas 1162 * 1163 * @package mod_lesson 1164 * @category files 1165 * @return array a list of available file areas 1166 */ 1167 function lesson_get_file_areas() { 1168 $areas = array(); 1169 $areas['page_contents'] = get_string('pagecontents', 'mod_lesson'); 1170 $areas['mediafile'] = get_string('mediafile', 'mod_lesson'); 1171 $areas['page_answers'] = get_string('pageanswers', 'mod_lesson'); 1172 $areas['page_responses'] = get_string('pageresponses', 'mod_lesson'); 1173 $areas['essay_responses'] = get_string('essayresponses', 'mod_lesson'); 1174 $areas['essay_answers'] = get_string('essayresponses', 'mod_lesson'); 1175 return $areas; 1176 } 1177 1178 /** 1179 * Returns a file_info_stored object for the file being requested here 1180 * 1181 * @package mod_lesson 1182 * @category files 1183 * @global stdClass $CFG 1184 * @param file_browse $browser file browser instance 1185 * @param array $areas file areas 1186 * @param stdClass $course course object 1187 * @param stdClass $cm course module object 1188 * @param stdClass $context context object 1189 * @param string $filearea file area 1190 * @param int $itemid item ID 1191 * @param string $filepath file path 1192 * @param string $filename file name 1193 * @return file_info_stored 1194 */ 1195 function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) { 1196 global $CFG, $DB; 1197 1198 if (!has_capability('moodle/course:managefiles', $context)) { 1199 // No peaking here for students! 1200 return null; 1201 } 1202 1203 // Mediafile area does not have sub directories, so let's select the default itemid to prevent 1204 // the user from selecting a directory to access the mediafile content. 1205 if ($filearea == 'mediafile' && is_null($itemid)) { 1206 $itemid = 0; 1207 } 1208 1209 if (is_null($itemid)) { 1210 return new mod_lesson_file_info($browser, $course, $cm, $context, $areas, $filearea); 1211 } 1212 1213 $fs = get_file_storage(); 1214 $filepath = is_null($filepath) ? '/' : $filepath; 1215 $filename = is_null($filename) ? '.' : $filename; 1216 if (!$storedfile = $fs->get_file($context->id, 'mod_lesson', $filearea, $itemid, $filepath, $filename)) { 1217 return null; 1218 } 1219 1220 $itemname = $filearea; 1221 if ($filearea == 'page_contents') { 1222 $itemname = $DB->get_field('lesson_pages', 'title', array('lessonid' => $cm->instance, 'id' => $itemid)); 1223 $itemname = format_string($itemname, true, array('context' => $context)); 1224 } else { 1225 $areas = lesson_get_file_areas(); 1226 if (isset($areas[$filearea])) { 1227 $itemname = $areas[$filearea]; 1228 } 1229 } 1230 1231 $urlbase = $CFG->wwwroot . '/pluginfile.php'; 1232 return new file_info_stored($browser, $context, $storedfile, $urlbase, $itemname, $itemid, true, true, false); 1233 } 1234 1235 1236 /** 1237 * Return a list of page types 1238 * @param string $pagetype current page type 1239 * @param stdClass $parentcontext Block's parent context 1240 * @param stdClass $currentcontext Current context of block 1241 */ 1242 function lesson_page_type_list($pagetype, $parentcontext, $currentcontext) { 1243 $module_pagetype = array( 1244 'mod-lesson-*'=>get_string('page-mod-lesson-x', 'lesson'), 1245 'mod-lesson-view'=>get_string('page-mod-lesson-view', 'lesson'), 1246 'mod-lesson-edit'=>get_string('page-mod-lesson-edit', 'lesson')); 1247 return $module_pagetype; 1248 } 1249 1250 /** 1251 * Update the lesson activity to include any file 1252 * that was uploaded, or if there is none, set the 1253 * mediafile field to blank. 1254 * 1255 * @param int $lessonid the lesson id 1256 * @param stdClass $context the context 1257 * @param int $draftitemid the draft item 1258 */ 1259 function lesson_update_media_file($lessonid, $context, $draftitemid) { 1260 global $DB; 1261 1262 // Set the filestorage object. 1263 $fs = get_file_storage(); 1264 // Save the file if it exists that is currently in the draft area. 1265 file_save_draft_area_files($draftitemid, $context->id, 'mod_lesson', 'mediafile', 0); 1266 // Get the file if it exists. 1267 $files = $fs->get_area_files($context->id, 'mod_lesson', 'mediafile', 0, 'itemid, filepath, filename', false); 1268 // Check that there is a file to process. 1269 if (count($files) == 1) { 1270 // Get the first (and only) file. 1271 $file = reset($files); 1272 // Set the mediafile column in the lessons table. 1273 $DB->set_field('lesson', 'mediafile', '/' . $file->get_filename(), array('id' => $lessonid)); 1274 } else { 1275 // Set the mediafile column in the lessons table. 1276 $DB->set_field('lesson', 'mediafile', '', array('id' => $lessonid)); 1277 } 1278 } 1279 1280 /** 1281 * Get icon mapping for font-awesome. 1282 */ 1283 function mod_lesson_get_fontawesome_icon_map() { 1284 return [ 1285 'mod_lesson:e/copy' => 'fa-clone', 1286 ]; 1287 } 1288 1289 /* 1290 * Check if the module has any update that affects the current user since a given time. 1291 * 1292 * @param cm_info $cm course module data 1293 * @param int $from the time to check updates from 1294 * @param array $filter if we need to check only specific updates 1295 * @return stdClass an object with the different type of areas indicating if they were updated or not 1296 * @since Moodle 3.3 1297 */ 1298 function lesson_check_updates_since(cm_info $cm, $from, $filter = array()) { 1299 global $DB, $USER; 1300 1301 $updates = course_check_module_updates_since($cm, $from, array(), $filter); 1302 1303 // Check if there are new pages or answers in the lesson. 1304 $updates->pages = (object) array('updated' => false); 1305 $updates->answers = (object) array('updated' => false); 1306 $select = 'lessonid = ? AND (timecreated > ? OR timemodified > ?)'; 1307 $params = array($cm->instance, $from, $from); 1308 1309 $pages = $DB->get_records_select('lesson_pages', $select, $params, '', 'id'); 1310 if (!empty($pages)) { 1311 $updates->pages->updated = true; 1312 $updates->pages->itemids = array_keys($pages); 1313 } 1314 $answers = $DB->get_records_select('lesson_answers', $select, $params, '', 'id'); 1315 if (!empty($answers)) { 1316 $updates->answers->updated = true; 1317 $updates->answers->itemids = array_keys($answers); 1318 } 1319 1320 // Check for new question attempts, grades, pages viewed and timers. 1321 $updates->questionattempts = (object) array('updated' => false); 1322 $updates->grades = (object) array('updated' => false); 1323 $updates->pagesviewed = (object) array('updated' => false); 1324 $updates->timers = (object) array('updated' => false); 1325 1326 $select = 'lessonid = ? AND userid = ? AND timeseen > ?'; 1327 $params = array($cm->instance, $USER->id, $from); 1328 1329 $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id'); 1330 if (!empty($questionattempts)) { 1331 $updates->questionattempts->updated = true; 1332 $updates->questionattempts->itemids = array_keys($questionattempts); 1333 } 1334 $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id'); 1335 if (!empty($pagesviewed)) { 1336 $updates->pagesviewed->updated = true; 1337 $updates->pagesviewed->itemids = array_keys($pagesviewed); 1338 } 1339 1340 $select = 'lessonid = ? AND userid = ? AND completed > ?'; 1341 $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id'); 1342 if (!empty($grades)) { 1343 $updates->grades->updated = true; 1344 $updates->grades->itemids = array_keys($grades); 1345 } 1346 1347 $select = 'lessonid = ? AND userid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)'; 1348 $params = array($cm->instance, $USER->id, $from, $from, $from); 1349 $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id'); 1350 if (!empty($timers)) { 1351 $updates->timers->updated = true; 1352 $updates->timers->itemids = array_keys($timers); 1353 } 1354 1355 // Now, teachers should see other students updates. 1356 if (has_capability('mod/lesson:viewreports', $cm->context)) { 1357 $select = 'lessonid = ? AND timeseen > ?'; 1358 $params = array($cm->instance, $from); 1359 1360 $insql = ''; 1361 $inparams = []; 1362 if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { 1363 $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); 1364 if (empty($groupusers)) { 1365 return $updates; 1366 } 1367 list($insql, $inparams) = $DB->get_in_or_equal($groupusers); 1368 $select .= ' AND userid ' . $insql; 1369 $params = array_merge($params, $inparams); 1370 } 1371 1372 $updates->userquestionattempts = (object) array('updated' => false); 1373 $updates->usergrades = (object) array('updated' => false); 1374 $updates->userpagesviewed = (object) array('updated' => false); 1375 $updates->usertimers = (object) array('updated' => false); 1376 1377 $questionattempts = $DB->get_records_select('lesson_attempts', $select, $params, '', 'id'); 1378 if (!empty($questionattempts)) { 1379 $updates->userquestionattempts->updated = true; 1380 $updates->userquestionattempts->itemids = array_keys($questionattempts); 1381 } 1382 $pagesviewed = $DB->get_records_select('lesson_branch', $select, $params, '', 'id'); 1383 if (!empty($pagesviewed)) { 1384 $updates->userpagesviewed->updated = true; 1385 $updates->userpagesviewed->itemids = array_keys($pagesviewed); 1386 } 1387 1388 $select = 'lessonid = ? AND completed > ?'; 1389 if (!empty($insql)) { 1390 $select .= ' AND userid ' . $insql; 1391 } 1392 $grades = $DB->get_records_select('lesson_grades', $select, $params, '', 'id'); 1393 if (!empty($grades)) { 1394 $updates->usergrades->updated = true; 1395 $updates->usergrades->itemids = array_keys($grades); 1396 } 1397 1398 $select = 'lessonid = ? AND (starttime > ? OR lessontime > ? OR timemodifiedoffline > ?)'; 1399 $params = array($cm->instance, $from, $from, $from); 1400 if (!empty($insql)) { 1401 $select .= ' AND userid ' . $insql; 1402 $params = array_merge($params, $inparams); 1403 } 1404 $timers = $DB->get_records_select('lesson_timer', $select, $params, '', 'id'); 1405 if (!empty($timers)) { 1406 $updates->usertimers->updated = true; 1407 $updates->usertimers->itemids = array_keys($timers); 1408 } 1409 } 1410 return $updates; 1411 } 1412 1413 /** 1414 * This function receives a calendar event and returns the action associated with it, or null if there is none. 1415 * 1416 * This is used by block_myoverview in order to display the event appropriately. If null is returned then the event 1417 * is not displayed on the block. 1418 * 1419 * @param calendar_event $event 1420 * @param \core_calendar\action_factory $factory 1421 * @param int $userid User id to use for all capability checks, etc. Set to 0 for current user (default). 1422 * @return \core_calendar\local\event\entities\action_interface|null 1423 */ 1424 function mod_lesson_core_calendar_provide_event_action(calendar_event $event, 1425 \core_calendar\action_factory $factory, 1426 int $userid = 0) { 1427 global $DB, $CFG, $USER; 1428 require_once($CFG->dirroot . '/mod/lesson/locallib.php'); 1429 1430 if (!$userid) { 1431 $userid = $USER->id; 1432 } 1433 1434 $cm = get_fast_modinfo($event->courseid, $userid)->instances['lesson'][$event->instance]; 1435 1436 if (!$cm->uservisible) { 1437 // The module is not visible to the user for any reason. 1438 return null; 1439 } 1440 1441 $completion = new \completion_info($cm->get_course()); 1442 1443 $completiondata = $completion->get_data($cm, false, $userid); 1444 1445 if ($completiondata->completionstate != COMPLETION_INCOMPLETE) { 1446 return null; 1447 } 1448 1449 $lesson = new lesson($DB->get_record('lesson', array('id' => $cm->instance), '*', MUST_EXIST)); 1450 1451 if ($lesson->count_user_retries($userid)) { 1452 // If the user has attempted the lesson then there is no further action for the user. 1453 return null; 1454 } 1455 1456 // Apply overrides. 1457 $lesson->update_effective_access($userid); 1458 1459 if (!$lesson->is_participant($userid)) { 1460 // If the user is not a participant then they have 1461 // no action to take. This will filter out the events for teachers. 1462 return null; 1463 } 1464 1465 return $factory->create_instance( 1466 get_string('startlesson', 'lesson'), 1467 new \moodle_url('/mod/lesson/view.php', ['id' => $cm->id]), 1468 1, 1469 $lesson->is_accessible() 1470 ); 1471 } 1472 1473 /** 1474 * Add a get_coursemodule_info function in case any lesson type wants to add 'extra' information 1475 * for the course (see resource). 1476 * 1477 * Given a course_module object, this function returns any "extra" information that may be needed 1478 * when printing this activity in a course listing. See get_array_of_activities() in course/lib.php. 1479 * 1480 * @param stdClass $coursemodule The coursemodule object (record). 1481 * @return cached_cm_info An object on information that the courses 1482 * will know about (most noticeably, an icon). 1483 */ 1484 function lesson_get_coursemodule_info($coursemodule) { 1485 global $DB; 1486 1487 $dbparams = ['id' => $coursemodule->instance]; 1488 $fields = 'id, name, intro, introformat, completionendreached, completiontimespent, available, deadline'; 1489 if (!$lesson = $DB->get_record('lesson', $dbparams, $fields)) { 1490 return false; 1491 } 1492 1493 $result = new cached_cm_info(); 1494 $result->name = $lesson->name; 1495 1496 if ($coursemodule->showdescription) { 1497 // Convert intro to html. Do not filter cached version, filters run at display time. 1498 $result->content = format_module_intro('lesson', $lesson, $coursemodule->id, false); 1499 } 1500 1501 // Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'. 1502 if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) { 1503 $result->customdata['customcompletionrules']['completionendreached'] = $lesson->completionendreached; 1504 $result->customdata['customcompletionrules']['completiontimespent'] = $lesson->completiontimespent; 1505 } 1506 1507 // Populate some other values that can be used in calendar or on dashboard. 1508 if ($lesson->available) { 1509 $result->customdata['available'] = $lesson->available; 1510 } 1511 if ($lesson->deadline) { 1512 $result->customdata['deadline'] = $lesson->deadline; 1513 } 1514 1515 return $result; 1516 } 1517 1518 /** 1519 * Sets dynamic information about a course module 1520 * 1521 * This function is called from cm_info when displaying the module 1522 * 1523 * @param cm_info $cm 1524 */ 1525 function mod_lesson_cm_info_dynamic(cm_info $cm) { 1526 global $USER; 1527 1528 $cache = cache::make('mod_lesson', 'overrides'); 1529 $override = $cache->get("{$cm->instance}_u_{$USER->id}"); 1530 1531 if (!$override) { 1532 $override = (object) [ 1533 'available' => null, 1534 'deadline' => null, 1535 ]; 1536 } 1537 1538 // No need to look for group overrides if there are user overrides for both available and deadline. 1539 if (is_null($override->available) || is_null($override->deadline)) { 1540 $availables = []; 1541 $deadlines = []; 1542 $groupings = groups_get_user_groups($cm->course, $USER->id); 1543 foreach ($groupings[0] as $groupid) { 1544 $groupoverride = $cache->get("{$cm->instance}_g_{$groupid}"); 1545 if (isset($groupoverride->available)) { 1546 $availables[] = $groupoverride->available; 1547 } 1548 if (isset($groupoverride->deadline)) { 1549 $deadlines[] = $groupoverride->deadline; 1550 } 1551 } 1552 // If there is a user override for a setting, ignore the group override. 1553 if (is_null($override->available) && count($availables)) { 1554 $override->available = min($availables); 1555 } 1556 if (is_null($override->deadline) && count($deadlines)) { 1557 if (in_array(0, $deadlines)) { 1558 $override->deadline = 0; 1559 } else { 1560 $override->deadline = max($deadlines); 1561 } 1562 } 1563 } 1564 1565 // Populate some other values that can be used in calendar or on dashboard. 1566 if (!is_null($override->available)) { 1567 $cm->override_customdata('available', $override->available); 1568 } 1569 if (!is_null($override->deadline)) { 1570 $cm->override_customdata('deadline', $override->deadline); 1571 } 1572 } 1573 1574 /** 1575 * Callback which returns human-readable strings describing the active completion custom rules for the module instance. 1576 * 1577 * @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules'] 1578 * @return array $descriptions the array of descriptions for the custom rules. 1579 */ 1580 function mod_lesson_get_completion_active_rule_descriptions($cm) { 1581 // Values will be present in cm_info, and we assume these are up to date. 1582 if (empty($cm->customdata['customcompletionrules']) 1583 || $cm->completion != COMPLETION_TRACKING_AUTOMATIC) { 1584 return []; 1585 } 1586 1587 $descriptions = []; 1588 foreach ($cm->customdata['customcompletionrules'] as $key => $val) { 1589 switch ($key) { 1590 case 'completionendreached': 1591 if (!empty($val)) { 1592 $descriptions[] = get_string('completionendreached_desc', 'lesson', $val); 1593 } 1594 break; 1595 case 'completiontimespent': 1596 if (!empty($val)) { 1597 $descriptions[] = get_string('completiontimespentdesc', 'lesson', format_time($val)); 1598 } 1599 break; 1600 default: 1601 break; 1602 } 1603 } 1604 return $descriptions; 1605 } 1606 1607 /** 1608 * This function calculates the minimum and maximum cutoff values for the timestart of 1609 * the given event. 1610 * 1611 * It will return an array with two values, the first being the minimum cutoff value and 1612 * the second being the maximum cutoff value. Either or both values can be null, which 1613 * indicates there is no minimum or maximum, respectively. 1614 * 1615 * If a cutoff is required then the function must return an array containing the cutoff 1616 * timestamp and error string to display to the user if the cutoff value is violated. 1617 * 1618 * A minimum and maximum cutoff return value will look like: 1619 * [ 1620 * [1505704373, 'The due date must be after the start date'], 1621 * [1506741172, 'The due date must be before the cutoff date'] 1622 * ] 1623 * 1624 * @param calendar_event $event The calendar event to get the time range for 1625 * @param stdClass $instance The module instance to get the range from 1626 * @return array 1627 */ 1628 function mod_lesson_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) { 1629 $mindate = null; 1630 $maxdate = null; 1631 1632 if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) { 1633 // The start time of the open event can't be equal to or after the 1634 // close time of the lesson activity. 1635 if (!empty($instance->deadline)) { 1636 $maxdate = [ 1637 $instance->deadline, 1638 get_string('openafterclose', 'lesson') 1639 ]; 1640 } 1641 } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) { 1642 // The start time of the close event can't be equal to or earlier than the 1643 // open time of the lesson activity. 1644 if (!empty($instance->available)) { 1645 $mindate = [ 1646 $instance->available, 1647 get_string('closebeforeopen', 'lesson') 1648 ]; 1649 } 1650 } 1651 1652 return [$mindate, $maxdate]; 1653 } 1654 1655 /** 1656 * This function will update the lesson module according to the 1657 * event that has been modified. 1658 * 1659 * It will set the available or deadline value of the lesson instance 1660 * according to the type of event provided. 1661 * 1662 * @throws \moodle_exception 1663 * @param \calendar_event $event 1664 * @param stdClass $lesson The module instance to get the range from 1665 */ 1666 function mod_lesson_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $lesson) { 1667 global $DB; 1668 1669 if (empty($event->instance) || $event->modulename != 'lesson') { 1670 return; 1671 } 1672 1673 if ($event->instance != $lesson->id) { 1674 return; 1675 } 1676 1677 if (!in_array($event->eventtype, [LESSON_EVENT_TYPE_OPEN, LESSON_EVENT_TYPE_CLOSE])) { 1678 return; 1679 } 1680 1681 $courseid = $event->courseid; 1682 $modulename = $event->modulename; 1683 $instanceid = $event->instance; 1684 $modified = false; 1685 1686 $coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid]; 1687 $context = context_module::instance($coursemodule->id); 1688 1689 // The user does not have the capability to modify this activity. 1690 if (!has_capability('moodle/course:manageactivities', $context)) { 1691 return; 1692 } 1693 1694 if ($event->eventtype == LESSON_EVENT_TYPE_OPEN) { 1695 // If the event is for the lesson activity opening then we should 1696 // set the start time of the lesson activity to be the new start 1697 // time of the event. 1698 if ($lesson->available != $event->timestart) { 1699 $lesson->available = $event->timestart; 1700 $lesson->timemodified = time(); 1701 $modified = true; 1702 } 1703 } else if ($event->eventtype == LESSON_EVENT_TYPE_CLOSE) { 1704 // If the event is for the lesson activity closing then we should 1705 // set the end time of the lesson activity to be the new start 1706 // time of the event. 1707 if ($lesson->deadline != $event->timestart) { 1708 $lesson->deadline = $event->timestart; 1709 $modified = true; 1710 } 1711 } 1712 1713 if ($modified) { 1714 $lesson->timemodified = time(); 1715 $DB->update_record('lesson', $lesson); 1716 $event = \core\event\course_module_updated::create_from_cm($coursemodule, $context); 1717 $event->trigger(); 1718 } 1719 } 1720 1721 /** 1722 * Callback to fetch the activity event type lang string. 1723 * 1724 * @param string $eventtype The event type. 1725 * @return lang_string The event type lang string. 1726 */ 1727 function mod_lesson_core_calendar_get_event_action_string(string $eventtype): string { 1728 $modulename = get_string('modulename', 'lesson'); 1729 1730 switch ($eventtype) { 1731 case LESSON_EVENT_TYPE_OPEN: 1732 $identifier = 'lessoneventopens'; 1733 break; 1734 case LESSON_EVENT_TYPE_CLOSE: 1735 $identifier = 'lessoneventcloses'; 1736 break; 1737 default: 1738 return get_string('requiresaction', 'calendar', $modulename); 1739 } 1740 1741 return get_string($identifier, 'lesson', $modulename); 1742 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body