3 // This file is part of Moodle - http://moodle.org/
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 * Standard library of functions and constants for 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
26 /** Include required libraries */
27 //TODO: these dumb includes have to be removed and this script minimised by moving stuff to locallib.php!!!
28 require_once($CFG->libdir.'/eventslib.php');
29 require_once($CFG->libdir.'/filelib.php');
30 require_once($CFG->dirroot.'/calendar/lib.php');
31 require_once($CFG->dirroot.'/course/moodleform_mod.php');
33 /** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
34 define("LESSON_MAX_EVENT_LENGTH", "432000");
37 * Given an object containing all the necessary data,
38 * (defined by the form in mod_form.php) this function
39 * will create a new instance and return the id number
40 * of the new instance.
44 * @param object $lesson Lesson post data from the form
47 function lesson_add_instance($data, $mform) {
50 $cmid = $data->coursemodule;
52 lesson_process_pre_save($data);
54 unset($data->mediafile);
55 $lessonid = $DB->insert_record("lesson", $data);
56 $data->id = $lessonid;
58 $context = get_context_instance(CONTEXT_MODULE, $cmid);
59 $lesson = $DB->get_record('lesson', array('id'=>$lessonid), '*', MUST_EXIST);
61 if ($filename = $mform->get_new_filename('mediafile')) {
62 if ($file = $mform->save_stored_file('mediafile', $context->id, 'mod_lesson', 'media_file', $lesson->id, '/', $filename)) {
63 $DB->set_field('lesson', 'mediafile', $file->get_filename(), array('id'=>$lesson->id));
67 lesson_process_post_save($data);
69 lesson_grade_item_update($data);
75 * Given an object containing all the necessary data,
76 * (defined by the form in mod_form.php) this function
77 * will update an existing instance with new data.
80 * @param object $lesson Lesson post data from the form
83 function lesson_update_instance($data, $mform) {
86 $data->id = $data->instance;
87 $cmid = $data->coursemodule;
89 lesson_process_pre_save($data);
91 unset($data->mediafile);
92 if (!$result = $DB->update_record("lesson", $data)) {
93 return false; // Awe man!
96 $context = get_context_instance(CONTEXT_MODULE, $cmid);
97 if ($filename = $mform->get_new_filename('mediafile')) {
98 if ($file = $mform->save_stored_file('mediafile', $context->id, 'mod_lesson', 'media_file', $data->id, '/', $filename, true)) {
99 $DB->set_field('lesson', 'mediafile', $file->get_filename(), array('id'=>$data->id));
103 lesson_process_post_save($data);
105 // update grade item definition
106 lesson_grade_item_update($data);
108 // update grades - TODO: do it only when grading style changes
109 lesson_update_grades($data, 0, false);
116 * Given an ID of an instance of this module,
117 * this function will permanently delete the instance
118 * and any data that depends on it.
124 function lesson_delete_instance($id) {
126 $lesson = $DB->get_record("lesson", array("id"=>$id), '*', MUST_EXIST);
127 $lesson = new lesson($lesson);
128 return $lesson->delete();
132 * Given a course object, this function will clean up anything that
133 * would be leftover after all the instances were deleted
136 * @param object $course an object representing the course that is being deleted
137 * @param boolean $feedback to specify if the process must output a summary of its work
140 function lesson_delete_course($course, $feedback=true) {
145 * Return a small object with summary information about what a
146 * user has done with a given particular instance of this module
147 * Used for user activity reports.
148 * $return->time = the time they did it
149 * $return->info = a short text description
152 * @param object $course
153 * @param object $user
155 * @param object $lesson
158 function lesson_user_outline($course, $user, $mod, $lesson) {
162 require_once("$CFG->libdir/gradelib.php");
163 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
165 if (empty($grades->items[0]->grades)) {
166 $return->info = get_string("no")." ".get_string("attempts", "lesson");
168 $grade = reset($grades->items[0]->grades);
169 $return->info = get_string("grade") . ': ' . $grade->str_long_grade;
170 $return->time = $grade->dategraded;
171 $return->info = get_string("no")." ".get_string("attempts", "lesson");
177 * Print a detailed representation of what a user has done with
178 * a given particular instance of this module, for user activity reports.
181 * @param object $course
182 * @param object $user
184 * @param object $lesson
187 function lesson_user_complete($course, $user, $mod, $lesson) {
188 global $DB, $OUTPUT, $CFG;
190 require_once("$CFG->libdir/gradelib.php");
192 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
193 if (!empty($grades->items[0]->grades)) {
194 $grade = reset($grades->items[0]->grades);
195 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
196 if ($grade->str_feedback) {
197 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
201 $params = array ("lessonid" => $lesson->id, "userid" => $user->id);
202 if ($attempts = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND userid = :userid", $params,
203 "retry, timeseen")) {
204 echo $OUTPUT->box_start();
205 $table = new html_table();
206 $table->head = array (get_string("attempt", "lesson"), get_string("numberofpagesviewed", "lesson"),
207 get_string("numberofcorrectanswers", "lesson"), get_string("time"));
208 $table->width = "100%";
209 $table->align = array ("center", "center", "center", "center");
210 $table->size = array ("*", "*", "*", "*");
211 $table->cellpadding = 2;
212 $table->cellspacing = 0;
218 foreach ($attempts as $attempt) {
219 if ($attempt->retry == $retry) {
221 if ($attempt->correct) {
224 $timeseen = $attempt->timeseen;
226 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
229 if ($attempt->correct) {
237 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
239 echo html_writer::table($table);
240 echo $OUTPUT->box_end();
247 * Prints lesson summaries on MyMoodle Page
249 * Prints lesson name, due date and attempt information on
250 * lessons that have a deadline that has not already passed
251 * and it is available for taking.
256 * @uses CONTEXT_MODULE
257 * @param array $courses An array of course objects to get lesson instances from
258 * @param array $htmlarray Store overview output array( course ID => 'lesson' => HTML output )
261 function lesson_print_overview($courses, &$htmlarray) {
262 global $USER, $CFG, $DB, $OUTPUT;
264 if (!$lessons = get_all_instances_in_courses('lesson', $courses)) {
268 /// Get Necessary Strings
269 $strlesson = get_string('modulename', 'lesson');
270 $strnotattempted = get_string('nolessonattempts', 'lesson');
271 $strattempted = get_string('lessonattempted', 'lesson');
274 foreach ($lessons as $lesson) {
275 if ($lesson->deadline != 0 // The lesson has a deadline
276 and $lesson->deadline >= $now // And it is before the deadline has been met
277 and ($lesson->available == 0 or $lesson->available <= $now)) { // And the lesson is available
280 if (!$lesson->visible) {
281 $class = ' class="dimmed"';
285 $str = $OUTPUT->box("$strlesson: <a$class href=\"$CFG->wwwroot/mod/lesson/view.php?id=$lesson->coursemodule\">".
286 format_string($lesson->name).'</a>', 'name');
289 $str .= $OUTPUT->box(get_string('lessoncloseson', 'lesson', userdate($lesson->deadline)), 'info');
291 // Attempt information
292 if (has_capability('mod/lesson:manage', get_context_instance(CONTEXT_MODULE, $lesson->coursemodule))) {
293 // Number of user attempts
294 $attempts = $DB->count_records('lesson_attempts', array('lessonid'=>$lesson->id));
295 $str .= $OUTPUT->box(get_string('xattempts', 'lesson', $attempts), 'info');
297 // Determine if the user has attempted the lesson or not
298 if ($DB->count_records('lesson_attempts', array('lessonid'=>$lesson->id, 'userid'=>$USER->id))) {
299 $str .= $OUTPUT->box($strattempted, 'info');
301 $str .= $OUTPUT->box($strnotattempted, 'info');
304 $str = $OUTPUT->box($str, 'lesson overview');
306 if (empty($htmlarray[$lesson->course]['lesson'])) {
307 $htmlarray[$lesson->course]['lesson'] = $str;
309 $htmlarray[$lesson->course]['lesson'] .= $str;
316 * Function to be run periodically according to the moodle cron
317 * This function searches for things that need to be done, such
318 * as sending out mail, toggling flags etc ...
322 function lesson_cron () {
329 * Return grade for given user or all users.
333 * @param int $lessonid id of lesson
334 * @param int $userid optional user id, 0 means all users
335 * @return array array of grades, false if none
337 function lesson_get_user_grades($lesson, $userid=0) {
340 $params = array("lessonid" => $lesson->id,"lessonid2" => $lesson->id);
342 if (isset($userid)) {
343 $params["userid"] = $userid;
344 $params["userid2"] = $userid;
345 $user = "AND u.id = :userid";
346 $fuser = "AND uu.id = :userid2";
353 if ($lesson->retake) {
354 if ($lesson->usemaxgrade) {
355 $sql = "SELECT u.id, u.id AS userid, MAX(g.grade) AS rawgrade
356 FROM {user} u, {lesson_grades} g
357 WHERE u.id = g.userid AND g.lessonid = :lessonid
361 $sql = "SELECT u.id, u.id AS userid, AVG(g.grade) AS rawgrade
362 FROM {user} u, {lesson_grades} g
363 WHERE u.id = g.userid AND g.lessonid = :lessonid
367 unset($params['lessonid2']);
368 unset($params['userid2']);
370 // use only first attempts (with lowest id in lesson_grades table)
371 $firstonly = "SELECT uu.id AS userid, MIN(gg.id) AS firstcompleted
372 FROM {user} uu, {lesson_grades} gg
373 WHERE uu.id = gg.userid AND gg.lessonid = :lessonid2
377 $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade
378 FROM {user} u, {lesson_grades} g, ($firstonly) f
379 WHERE u.id = g.userid AND g.lessonid = :lessonid
380 AND g.id = f.firstcompleted AND g.userid=f.userid
384 return $DB->get_records_sql($sql, $params);
388 * Update grades in central gradebook
392 * @param object $lesson
393 * @param int $userid specific user only, 0 means all
394 * @param bool $nullifnone
396 function lesson_update_grades($lesson, $userid=0, $nullifnone=true) {
398 require_once($CFG->libdir.'/gradelib.php');
400 if ($lesson->grade == 0) {
401 lesson_grade_item_update($lesson);
403 } else if ($grades = lesson_get_user_grades($lesson, $userid)) {
404 lesson_grade_item_update($lesson, $grades);
406 } else if ($userid and $nullifnone) {
407 $grade = new object();
408 $grade->userid = $userid;
409 $grade->rawgrade = NULL;
410 lesson_grade_item_update($lesson, $grade);
413 lesson_grade_item_update($lesson);
418 * Update all grades in gradebook.
422 function lesson_upgrade_grades() {
425 $sql = "SELECT COUNT('x')
426 FROM {lesson} l, {course_modules} cm, {modules} m
427 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id";
428 $count = $DB->count_records_sql($sql);
430 $sql = "SELECT l.*, cm.idnumber AS cmidnumber, l.course AS courseid
431 FROM {lesson} l, {course_modules} cm, {modules} m
432 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id";
433 if ($rs = $DB->get_recordset_sql($sql)) {
434 $pbar = new progress_bar('lessonupgradegrades', 500, true);
436 foreach ($rs as $lesson) {
438 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
439 lesson_update_grades($lesson, 0, false);
440 $pbar->update($i, $count, "Updating Lesson grades ($i/$count).");
447 * Create grade item for given lesson
450 * @uses GRADE_TYPE_VALUE
451 * @uses GRADE_TYPE_NONE
452 * @param object $lesson object with extra cmidnumber
453 * @param array|object $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
454 * @return int 0 if ok, error code otherwise
456 function lesson_grade_item_update($lesson, $grades=NULL) {
458 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
459 require_once($CFG->libdir.'/gradelib.php');
462 if (array_key_exists('cmidnumber', $lesson)) { //it may not be always present
463 $params = array('itemname'=>$lesson->name, 'idnumber'=>$lesson->cmidnumber);
465 $params = array('itemname'=>$lesson->name);
468 if ($lesson->grade > 0) {
469 $params['gradetype'] = GRADE_TYPE_VALUE;
470 $params['grademax'] = $lesson->grade;
471 $params['grademin'] = 0;
474 $params['gradetype'] = GRADE_TYPE_NONE;
477 if ($grades === 'reset') {
478 $params['reset'] = true;
480 } else if (!empty($grades)) {
481 // Need to calculate raw grade (Note: $grades has many forms)
482 if (is_object($grades)) {
483 $grades = array($grades->userid => $grades);
484 } else if (array_key_exists('userid', $grades)) {
485 $grades = array($grades['userid'] => $grades);
487 foreach ($grades as $key => $grade) {
488 if (!is_array($grade)) {
489 $grades[$key] = $grade = (array) $grade;
491 $grades[$key]['rawgrade'] = ($grade['rawgrade'] * $lesson->grade / 100);
495 return grade_update('mod/lesson', $lesson->course, 'mod', 'lesson', $lesson->id, 0, $grades, $params);
499 * Delete grade item for given lesson
502 * @param object $lesson object
503 * @return object lesson
505 function lesson_grade_item_delete($lesson) {
512 * Must return an array of user records (all data) who are participants
513 * for a given instance of lesson. Must include every user involved
514 * in the instance, independient of his role (student, teacher, admin...)
518 * @param int $lessonid
521 function lesson_get_participants($lessonid) {
525 $params = array ("lessonid" => $lessonid);
526 $students = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
529 WHERE a.lessonid = :lessonid and
530 u.id = a.userid", $params);
532 //Return students array (it contains an array of unique users)
539 function lesson_get_view_actions() {
540 return array('view','view all');
546 function lesson_get_post_actions() {
547 return array('end','start');
551 * Runs any processes that must run before
552 * a lesson insert/update
555 * @param object $lesson Lesson form data
558 function lesson_process_pre_save(&$lesson) {
561 $lesson->timemodified = time();
563 if (empty($lesson->timed)) {
566 if (empty($lesson->timespent) or !is_numeric($lesson->timespent) or $lesson->timespent < 0) {
567 $lesson->timespent = 0;
569 if (!isset($lesson->completed)) {
570 $lesson->completed = 0;
572 if (empty($lesson->gradebetterthan) or !is_numeric($lesson->gradebetterthan) or $lesson->gradebetterthan < 0) {
573 $lesson->gradebetterthan = 0;
574 } else if ($lesson->gradebetterthan > 100) {
575 $lesson->gradebetterthan = 100;
578 if (empty($lesson->width)) {
579 $lesson->width = 640;
581 if (empty($lesson->height)) {
582 $lesson->height = 480;
584 if (empty($lesson->bgcolor)) {
585 $lesson->bgcolor = '#FFFFFF';
588 // Conditions for dependency
589 $conditions = new stdClass;
590 $conditions->timespent = $lesson->timespent;
591 $conditions->completed = $lesson->completed;
592 $conditions->gradebetterthan = $lesson->gradebetterthan;
593 $lesson->conditions = serialize($conditions);
594 unset($lesson->timespent);
595 unset($lesson->completed);
596 unset($lesson->gradebetterthan);
598 if (empty($lesson->password)) {
599 unset($lesson->password);
604 * Runs any processes that must be run
605 * after a lesson insert/update
608 * @param object $lesson Lesson form data
611 function lesson_process_post_save(&$lesson) {
614 if ($events = $DB->get_records('event', array('modulename'=>'lesson', 'instance'=>$lesson->id))) {
615 foreach($events as $event) {
616 $event = calendar_event::load($event->id);
621 $event = new stdClass;
622 $event->description = $lesson->name;
623 $event->courseid = $lesson->course;
626 $event->modulename = 'lesson';
627 $event->instance = $lesson->id;
628 $event->eventtype = 'open';
629 $event->timestart = $lesson->available;
631 $event->visible = instance_is_visible('lesson', $lesson);
633 $event->timeduration = ($lesson->deadline - $lesson->available);
635 if ($lesson->deadline and $lesson->available and $event->timeduration <= LESSON_MAX_EVENT_LENGTH) {
636 // Single event for the whole lesson.
637 $event->name = $lesson->name;
638 calendar_event::create(clone($event));
640 // Separate start and end events.
641 $event->timeduration = 0;
642 if ($lesson->available) {
643 $event->name = $lesson->name.' ('.get_string('lessonopens', 'lesson').')';
644 calendar_event::create(clone($event));
645 } else if ($lesson->deadline) {
646 $event->name = $lesson->name.' ('.get_string('lessoncloses', 'lesson').')';
647 $event->timestart = $lesson->deadline;
648 $event->eventtype = 'close';
649 calendar_event::create(clone($event));
656 * Implementation of the function for printing the form elements that control
657 * whether the course reset functionality affects the lesson.
659 * @param $mform form passed by reference
661 function lesson_reset_course_form_definition(&$mform) {
662 $mform->addElement('header', 'lessonheader', get_string('modulenameplural', 'lesson'));
663 $mform->addElement('advcheckbox', 'reset_lesson', get_string('deleteallattempts','lesson'));
667 * Course reset form defaults.
668 * @param object $course
671 function lesson_reset_course_form_defaults($course) {
672 return array('reset_lesson'=>1);
676 * Removes all grades from gradebook
680 * @param int $courseid
681 * @param string optional type
683 function lesson_reset_gradebook($courseid, $type='') {
686 $sql = "SELECT l.*, cm.idnumber as cmidnumber, l.course as courseid
687 FROM {lesson} l, {course_modules} cm, {modules} m
688 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id AND l.course=:course";
689 $params = array ("course" => $courseid);
690 if ($lessons = $DB->get_records_sql($sql,$params)) {
691 foreach ($lessons as $lesson) {
692 lesson_grade_item_update($lesson, 'reset');
698 * Actual implementation of the rest coures functionality, delete all the
699 * lesson attempts for course $data->courseid.
703 * @param object $data the data submitted from the reset course.
704 * @return array status array
706 function lesson_reset_userdata($data) {
709 $componentstr = get_string('modulenameplural', 'lesson');
712 if (!empty($data->reset_lesson)) {
713 $lessonssql = "SELECT l.id
715 WHERE l.course=:course";
717 $params = array ("course" => $data->courseid);
718 $DB->delete_records_select('lesson_timer', "lessonid IN ($lessonssql)", $params);
719 $DB->delete_records_select('lesson_high_scores', "lessonid IN ($lessonssql)", $params);
720 $DB->delete_records_select('lesson_grades', "lessonid IN ($lessonssql)", $params);
721 $DB->delete_records_select('lesson_attempts', "lessonid IN ($lessonssql)", $params);
723 // remove all grades from gradebook
724 if (empty($data->reset_gradebook_grades)) {
725 lesson_reset_gradebook($data->courseid);
728 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false);
731 /// updating dates - shift may be negative too
732 if ($data->timeshift) {
733 shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid);
734 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
741 * Returns all other caps used in module
744 function lesson_get_extra_capabilities() {
745 return array('moodle/site:accessallgroups');
749 * @uses FEATURE_GROUPS
750 * @uses FEATURE_GROUPINGS
751 * @uses FEATURE_GROUPMEMBERSONLY
752 * @uses FEATURE_MOD_INTRO
753 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
754 * @uses FEATURE_GRADE_HAS_GRADE
755 * @uses FEATURE_GRADE_OUTCOMES
756 * @param string $feature FEATURE_xx constant for requested feature
757 * @return mixed True if module supports feature, false if not, null if doesn't know
759 function lesson_supports($feature) {
761 case FEATURE_GROUPS: return false;
762 case FEATURE_GROUPINGS: return false;
763 case FEATURE_GROUPMEMBERSONLY: return true;
764 case FEATURE_MOD_INTRO: return false;
765 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
766 case FEATURE_GRADE_HAS_GRADE: return true;
767 case FEATURE_GRADE_OUTCOMES: return true;
768 case FEATURE_BACKUP_MOODLE2: return true;
769 default: return null;
774 * This function extends the global navigation for the site.
775 * It is important to note that you should not rely on PAGE objects within this
776 * body of code as there is no guarantee that during an AJAX request they are
779 * @param navigation_node $navigation The lesson node within the global navigation
780 * @param stdClass $course The course object returned from the DB
781 * @param stdClass $module The module object returned from the DB
782 * @param stdClass $cm The course module instance returned from the DB
784 function lesson_extend_navigation($navigation, $course, $module, $cm) {
786 * This is currently just a stub so that it can be easily expanded upon.
787 * When expanding just remove this comment and the line below and then add
790 $navigation->nodetype = navigation_node::NODETYPE_LEAF;
794 * This function extends the settings navigation block for the site.
796 * It is safe to rely on PAGE here as we will only ever be within the module
797 * context when this is called
799 * @param settings_navigation $settings
800 * @param navigation_node $lessonnode
802 function lesson_extend_settings_navigation($settings, $lessonnode) {
805 $canedit = has_capability('mod/lesson:edit', $PAGE->cm->context);
807 $url = new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id));
808 $lessonnode->add(get_string('preview', 'lesson'), $url);
811 $url = new moodle_url('/mod/lesson/edit.php', array('id'=>$PAGE->cm->id));
812 $lessonnode->add(get_string('edit', 'lesson'), $url);
815 if (has_capability('mod/lesson:manage', $PAGE->cm->context)) {
816 $reportsnode = $lessonnode->add(get_string('reports', 'lesson'));
817 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportoverview'));
818 $reportsnode->add(get_string('overview', 'lesson'), $url);
819 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportdetail'));
820 $reportsnode->add(get_string('detailedstats', 'lesson'), $url);
824 $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$PAGE->cm->id));
825 $lessonnode->add(get_string('manualgrading', 'lesson'), $url);
828 if ($PAGE->activityrecord->highscores) {
829 $url = new moodle_url('/mod/lesson/highscores.php', array('id'=>$PAGE->cm->id));
830 $lessonnode->add(get_string('highscores', 'lesson'), $url);
835 * Get list of available import or export formats
837 * Copied and modified from lib/questionlib.php
839 * @param string $type 'import' if import list, otherwise export list assumed
840 * @return array sorted list of import/export formats available
842 function lesson_get_import_export_formats($type) {
844 $fileformats = get_plugin_list("qformat");
846 $fileformatname=array();
847 foreach ($fileformats as $fileformat=>$fdir) {
848 $format_file = "$fdir/format.php";
849 if (file_exists($format_file) ) {
850 require_once($format_file);
854 $classname = "qformat_$fileformat";
855 $format_class = new $classname();
856 if ($type=='import') {
857 $provided = $format_class->provide_import();
859 $provided = $format_class->provide_export();
862 $formatname = get_string($fileformat, 'quiz');
863 if ($formatname == "[[$fileformat]]") {
864 $formatname = get_string($fileformat, 'qformat_'.$fileformat);
865 if ($formatname == "[[$fileformat]]") {
866 $formatname = $fileformat; // Just use the raw folder name
869 $fileformatnames[$fileformat] = $formatname;
872 natcasesort($fileformatnames);
874 return $fileformatnames;
878 * Serves the lesson attachments. Implements needed access control ;-)
880 * @param object $course
882 * @param object $context
883 * @param string $filearea
885 * @param bool $forcedownload
886 * @return bool false if file not found, does not return if found - justsend the file
888 function lesson_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
891 if ($context->contextlevel != CONTEXT_MODULE) {
895 $fileareas = lesson_get_file_areas();
896 if (!array_key_exists($filearea, $fileareas)) {
900 if (!$lesson = $DB->get_record('lesson', array('id'=>$cm->instance))) {
904 require_course_login($course, true, $cm);
906 if ($filearea === 'page_contents') {
907 $pageid = (int)array_shift($args);
908 if (!$page = $DB->get_record('lesson_pages', array('id'=>$pageid))) {
911 $fullpath = "/$context->id/mod_lesson/$filearea/$pageid/".implode('/', $args);
912 $forcedownload = true; //TODO: this is strange (skodak)
915 $fullpath = "/$context->id/mod_lesson/$filearea/".implode('/', $args);
918 $fs = get_file_storage();
919 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
923 // finally send the file
924 send_stored_file($file, 0, 0, $forcedownload); // download MUST be forced - security!
928 * Returns an array of file areas
931 function lesson_get_file_areas() {
933 $areas['page_contents'] = 'page_contents'; //TODO: localize!!!!
934 $areas['media_files'] = 'media_files'; //TODO: localize!!!!
938 * Returns a file_info_stored object for the file being requested here
940 * @global <type> $CFG
941 * @param file_browse $browser
942 * @param array $areas
943 * @param object $course
945 * @param object $context
946 * @param string $filearea
948 * @param string $filepath
949 * @param string $filename
950 * @return file_info_stored
952 function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
954 if (has_capability('moodle/course:managefiles', $context)) {
955 // no peaking here for students!!
959 $fs = get_file_storage();
960 $filepath = is_null($filepath) ? '/' : $filepath;
961 $filename = is_null($filename) ? '.' : $filename;
962 $urlbase = $CFG->wwwroot.'/pluginfile.php';
963 if (!$storedfile = $fs->get_file($context->id, 'mod_lesson', $filearea, $itemid, $filepath, $filename)) {
966 return new file_info_stored($browser, $context, $storedfile, $urlbase, $filearea, $itemid, true, true, false);
970 * Abstract class to provide a core functions to the all lesson classes
972 * This class should be abstracted by ALL classes with the lesson module to ensure
973 * that all classes within this module can be interacted with in the same way.
975 * This class provides the user with a basic properties array that can be fetched
976 * or set via magic methods, or alternativily by defining methods get_blah() or
977 * set_blah() within the extending object.
979 * @copyright 2009 Sam Hemelryk
980 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
982 abstract class lesson_base {
985 * An object containing properties
988 protected $properties;
992 * @param stdClass $properties
994 public function __construct($properties) {
995 $this->properties = (object)$properties;
999 * Magic property method
1001 * Attempts to call a set_$key method if one exists otherwise falls back
1002 * to simply set the property
1004 * @param string $key
1005 * @param mixed $value
1007 public function __set($key, $value) {
1008 if (method_exists($this, 'set_'.$key)) {
1009 $this->{'set_'.$key}($value);
1011 $this->properties->{$key} = $value;
1017 * Attempts to call a get_$key method to return the property and ralls over
1018 * to return the raw property
1023 public function __get($key) {
1024 if (method_exists($this, 'get_'.$key)) {
1025 return $this->{'get_'.$key}();
1027 return $this->properties->{$key};
1031 * Stupid PHP needs an isset magic method if you use the get magic method and
1032 * still want empty calls to work.... blah ~!
1034 * @param string $key
1037 public function __isset($key) {
1038 if (method_exists($this, 'get_'.$key)) {
1039 $val = $this->{'get_'.$key}();
1040 return !empty($val);
1042 return !empty($this->properties->{$key});
1046 * If overriden should create a new instance, save it in the DB and return it
1048 public static function create() {}
1050 * If overriden should load an instance from the DB and return it
1052 public static function load() {}
1054 * Fetches all of the properties of the object
1057 public function properties() {
1058 return $this->properties;
1063 * Class representation of a lesson
1065 * This class is used the interact with, and manage a lesson once instantiated.
1066 * If you need to fetch a lesson object you can do so by calling
1069 * lesson::load($lessonid);
1071 * $lessonrecord = $DB->get_record('lesson', $lessonid);
1072 * $lesson = new lesson($lessonrecord);
1075 * The class itself extends lesson_base as all classes within the lesson module should
1077 * These properties are from the database
1078 * @property int $id The id of this lesson
1079 * @property int $course The ID of the course this lesson belongs to
1080 * @property string $name The name of this lesson
1081 * @property int $practice Flag to toggle this as a practice lesson
1082 * @property int $modattempts Toggle to allow the user to go back and review answers
1083 * @property int $usepassword Toggle the use of a password for entry
1084 * @property string $password The password to require users to enter
1085 * @property int $dependency ID of another lesson this lesson is dependant on
1086 * @property string $conditions Conditions of the lesson dependency
1087 * @property int $grade The maximum grade a user can achieve (%)
1088 * @property int $custom Toggle custom scoring on or off
1089 * @property int $ongoing Toggle display of an ongoing score
1090 * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
1091 * @property int $maxanswers The max number of answers or branches
1092 * @property int $maxattempts The maximum number of attempts a user can record
1093 * @property int $review Toggle use or wrong answer review button
1094 * @property int $nextpagedefault Override the default next page
1095 * @property int $feedback Toggles display of default feedback
1096 * @property int $minquestions Sets a minimum value of pages seen when calculating grades
1097 * @property int $maxpages Maximum number of pages this lesson can contain
1098 * @property int $retake Flag to allow users to retake a lesson
1099 * @property int $activitylink Relate this lesson to another lesson
1100 * @property string $mediafile File to pop up to or webpage to display
1101 * @property int $mediaheight Sets the height of the media file popup
1102 * @property int $mediawidth Sets the width of the media file popup
1103 * @property int $mediaclose Toggle display of a media close button
1104 * @property int $slideshow Flag for whether branch pages should be shown as slideshows
1105 * @property int $width Width of slideshow
1106 * @property int $height Height of slideshow
1107 * @property string $bgcolor Background colour of slideshow
1108 * @property int $displayleft Display a left meun
1109 * @property int $displayleftif Sets the condition on which the left menu is displayed
1110 * @property int $progressbar Flag to toggle display of a lesson progress bar
1111 * @property int $highscores Flag to toggle collection of high scores
1112 * @property int $maxhighscores Number of high scores to limit to
1113 * @property int $available Timestamp of when this lesson becomes available
1114 * @property int $deadline Timestamp of when this lesson is no longer available
1115 * @property int $timemodified Timestamp when lesson was last modified
1117 * These properties are calculated
1118 * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
1119 * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
1121 * @copyright 2009 Sam Hemelryk
1122 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1124 class lesson extends lesson_base {
1127 * The id of the first page (where prevpageid = 0) gets set and retrieved by
1128 * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
1131 protected $firstpageid = null;
1133 * The id of the last page (where nextpageid = 0) gets set and retrieved by
1134 * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
1137 protected $lastpageid = null;
1139 * An array used to cache the pages associated with this lesson after the first
1140 * time they have been loaded.
1141 * A note to developers: If you are going to be working with MORE than one or
1142 * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1143 * in order to save excess database queries.
1144 * @var array An array of lesson_page objects
1146 protected $pages = array();
1148 * Flag that gets set to true once all of the pages associated with the lesson
1152 protected $loadedallpages = false;
1155 * Simply generates a lesson object given an array/object of properties
1156 * Overrides {@see lesson_base->create()}
1158 * @param object|array $properties
1161 public static function create($properties) {
1162 return new lesson($properties);
1166 * Generates a lesson object from the database given its id
1168 * @param int $lessonid
1171 public static function load($lessonid) {
1172 if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1173 print_error('invalidcoursemodule');
1175 return new lesson($lesson);
1179 * Deletes this lesson from the database
1181 public function delete() {
1183 require_once($CFG->libdir.'/gradelib.php');
1185 $DB->delete_records("lesson", array("id"=>$this->properties->id));;
1186 $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1187 $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1188 $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1189 $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1190 $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1191 $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1192 $DB->delete_records("lesson_high_scores", array("lessonid"=>$this->properties->id));
1193 if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
1194 foreach($events as $event) {
1195 $event = calendar_event::load($event);
1200 grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, NULL, array('deleted'=>1));
1205 * Fetches messages from the session that may have been set in previous page
1209 * // Do not call this method directly instead use
1210 * $lesson->messages;
1215 protected function get_messages() {
1218 $messages = array();
1219 if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1220 $messages = $SESSION->lesson_messages[$this->properties->id];
1221 unset($SESSION->lesson_messages[$this->properties->id]);
1228 * Get all of the attempts for the current user.
1230 * @param int $retries
1231 * @param bool $correct Optional: only fetch correct attempts
1232 * @param int $pageid Optional: only fetch attempts at the given page
1233 * @param int $userid Optional: defaults to the current user if not set
1234 * @return array|false
1236 public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1238 $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1240 $params['correct'] = 1;
1242 if ($pageid !== null) {
1243 $params['pageid'] = $pageid;
1245 if ($userid === null) {
1246 $params['userid'] = $USER->id;
1248 return $DB->get_records('lesson_attempts', $params, 'timeseen DESC');
1252 * Returns the first page for the lesson or false if there isn't one.
1254 * This method should be called via the magic method __get();
1256 * $firstpage = $lesson->firstpage;
1259 * @return lesson_page|bool Returns the lesson_page specialised object or false
1261 protected function get_firstpage() {
1262 $pages = $this->load_all_pages();
1263 if (count($pages) > 0) {
1264 foreach ($pages as $page) {
1265 if ((int)$page->prevpageid === 0) {
1274 * Returns the last page for the lesson or false if there isn't one.
1276 * This method should be called via the magic method __get();
1278 * $lastpage = $lesson->lastpage;
1281 * @return lesson_page|bool Returns the lesson_page specialised object or false
1283 protected function get_lastpage() {
1284 $pages = $this->load_all_pages();
1285 if (count($pages) > 0) {
1286 foreach ($pages as $page) {
1287 if ((int)$page->nextpageid === 0) {
1296 * Returns the id of the first page of this lesson. (prevpageid = 0)
1299 protected function get_firstpageid() {
1301 if ($this->firstpageid == null) {
1302 if (!$this->loadedallpages) {
1303 $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
1304 if (!$firstpageid) {
1305 print_error('cannotfindfirstpage', 'lesson');
1307 $this->firstpageid = $firstpageid;
1309 $firstpage = $this->get_firstpage();
1310 $this->firstpageid = $firstpage->id;
1313 return $this->firstpageid;
1317 * Returns the id of the last page of this lesson. (nextpageid = 0)
1320 public function get_lastpageid() {
1322 if ($this->lastpageid == null) {
1323 if (!$this->loadedallpages) {
1324 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1326 print_error('cannotfindlastpage', 'lesson');
1328 $this->lastpageid = $lastpageid;
1330 $lastpageid = $this->get_lastpage();
1331 $this->lastpageid = $lastpageid->id;
1335 return $this->lastpageid;
1339 * Gets the next page to display after the one that is provided.
1340 * @param int $nextpageid
1343 public function get_next_page($nextpageid) {
1345 $allpages = $this->load_all_pages();
1346 if ($this->properties->nextpagedefault) {
1347 // in Flash Card mode...first get number of retakes
1350 if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
1351 foreach ($allpages as $nextpage) {
1352 if (!$DB->count_records("lesson_attempts", array("pageid"=>$nextpage->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
1357 } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
1358 foreach ($allpages as $nextpage) {
1359 if (!$DB->count_records("lesson_attempts", array('pageid'=>$nextpage->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
1366 if ($this->properties->maxpages) {
1367 // check number of pages viewed (in the lesson)
1368 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$USER->id));
1369 if ($DB->count_records("lesson_attempts", array("lessonid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes)) >= $this->properties->maxpages) {
1376 // In a normal lesson mode
1377 foreach ($allpages as $nextpage) {
1378 if ((int)$nextpage->id===(int)$nextpageid) {
1386 * Sets a message against the session for this lesson that will displayed next
1387 * time the lesson processes messages
1389 * @param string $message
1390 * @param string $class
1391 * @param string $align
1394 public function add_message($message, $class="notifyproblem", $align='center') {
1397 if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
1398 $SESSION->lesson_messages = array();
1399 $SESSION->lesson_messages[$this->properties->id] = array();
1400 } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1401 $SESSION->lesson_messages[$this->properties->id] = array();
1404 $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
1410 * Check if the lesson is accessible at the present time
1411 * @return bool True if the lesson is accessible, false otherwise
1413 public function is_accessible() {
1414 $available = $this->properties->available;
1415 $deadline = $this->properties->deadline;
1416 return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
1420 * Starts the lesson time for the current user
1421 * @return bool Returns true
1423 public function start_timer() {
1425 $USER->startlesson[$this->properties->id] = true;
1426 $startlesson = new stdClass;
1427 $startlesson->lessonid = $this->properties->id;
1428 $startlesson->userid = $USER->id;
1429 $startlesson->starttime = time();
1430 $startlesson->lessontime = time();
1431 $DB->insert_record('lesson_timer', $startlesson);
1432 if ($this->properties->timed) {
1433 $this->add_message(get_string('maxtimewarning', 'lesson', $this->properties->maxtime), 'center');
1439 * Updates the timer to the current time and returns the new timer object
1440 * @param bool $restart If set to true the timer is restarted
1441 * @param bool $continue If set to true AND $restart=true then the timer
1442 * will continue from a previous attempt
1443 * @return stdClass The new timer
1445 public function update_timer($restart=false, $continue=false) {
1448 // get time information for this user
1449 if (!$timer = $DB->get_records('lesson_timer', array ("lessonid" => $this->properties->id, "userid" => $USER->id), 'starttime DESC', '*', 0, 1)) {
1450 print_error('cannotfindtimer', 'lesson');
1452 $timer = current($timer); // this will get the latest start time record
1457 // continue a previous test, need to update the clock (think this option is disabled atm)
1458 $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
1460 // starting over, so reset the clock
1461 $timer->starttime = time();
1465 $timer->lessontime = time();
1466 $DB->update_record('lesson_timer', $timer);
1471 * Updates the timer to the current time then stops it by unsetting the user var
1472 * @return bool Returns true
1474 public function stop_timer() {
1476 unset($USER->startlesson[$this->properties->id]);
1477 return $this->update_timer(false, false);
1481 * Checks to see if the lesson has pages
1483 public function has_pages() {
1485 $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
1486 return ($pagecount>0);
1490 * Returns the link for the related activity
1491 * @return array|false
1493 public function link_for_activitylink() {
1495 $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
1497 $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
1499 $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
1500 if ($instancename) {
1501 return html_writer::link(new moodle_url('/mod/'.$modname.'/view.php', array('id'=>$this->properties->activitylink)),
1502 get_string('returnto', 'lesson', get_string('activitylinkname', 'lesson', $instancename)),
1503 array('class'=>'centerpadded lessonbutton standardbutton'));
1511 * Loads the requested page.
1513 * This function will return the requested page id as either a specialised
1514 * lesson_page object OR as a generic lesson_page.
1515 * If the page has been loaded previously it will be returned from the pages
1516 * array, otherwise it will be loaded from the database first
1518 * @param int $pageid
1519 * @return lesson_page A lesson_page object or an object that extends it
1521 public function load_page($pageid) {
1522 if (!array_key_exists($pageid, $this->pages)) {
1523 $manager = lesson_page_type_manager::get($this);
1524 $this->pages[$pageid] = $manager->load_page($pageid, $this);
1526 return $this->pages[$pageid];
1530 * Loads ALL of the pages for this lesson
1532 * @return array An array containing all pages from this lesson
1534 public function load_all_pages() {
1535 if (!$this->loadedallpages) {
1536 $manager = lesson_page_type_manager::get($this);
1537 $this->pages = $manager->load_all_pages($this);
1538 $this->loadedallpages = true;
1540 return $this->pages;
1544 * Determins if a jumpto value is correct or not.
1546 * returns true if jumpto page is (logically) after the pageid page or
1547 * if the jumpto value is a special value. Returns false in all other cases.
1549 * @param int $pageid Id of the page from which you are jumping from.
1550 * @param int $jumpto The jumpto number.
1551 * @return boolean True or false after a series of tests.
1553 public function jumpto_is_correct($pageid, $jumpto) {
1556 // first test the special values
1560 } elseif ($jumpto == LESSON_NEXTPAGE) {
1562 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
1564 } elseif ($jumpto == LESSON_RANDOMPAGE) {
1566 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
1568 } elseif ($jumpto == LESSON_EOL) {
1572 $pages = $this->load_all_pages();
1573 $apageid = $pages[$pageid]->nextpageid;
1574 while ($apageid != 0) {
1575 if ($jumpto == $apageid) {
1578 $apageid = $pages[$apageid]->nextpageid;
1584 * Returns the time a user has remaining on this lesson
1585 * @param int $starttime Starttime timestamp
1588 public function time_remaining($starttime) {
1589 $timeleft = $starttime + $this->maxtime * 60 - time();
1590 $hours = floor($timeleft/3600);
1591 $timeleft = $timeleft - ($hours * 3600);
1592 $minutes = floor($timeleft/60);
1593 $secs = $timeleft - ($minutes * 60);
1595 if ($minutes < 10) {
1596 $minutes = "0$minutes";
1603 $output[] = $minutes;
1605 $output = implode(':', $output);
1610 * Interprets LESSON_CLUSTERJUMP jumpto value.
1612 * This will select a page randomly
1613 * and the page selected will be inbetween a cluster page and end of cluter or end of lesson
1614 * and the page selected will be a page that has not been viewed already
1615 * and if any pages are within a branch table or end of branch then only 1 page within
1616 * the branch table or end of branch will be randomly selected (sub clustering).
1618 * @param int $pageid Id of the current page from which we are jumping from.
1619 * @param int $userid Id of the user.
1620 * @return int The id of the next page.
1622 public function cluster_jump($pageid, $userid=null) {
1625 if ($userid===null) {
1626 $userid = $USER->id;
1628 // get the number of retakes
1629 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
1632 // get all the lesson_attempts aka what the user has seen
1633 $seenpages = array();
1634 if ($attempts = $this->get_attempts($retakes)) {
1635 foreach ($attempts as $attempt) {
1636 $seenpages[$attempt->pageid] = $attempt->pageid;
1641 // get the lesson pages
1642 $lessonpages = $this->load_all_pages();
1643 // find the start of the cluster
1644 while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
1645 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
1648 $pageid = $lessonpages[$pageid]->prevpageid;
1651 $clusterpages = array();
1652 $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
1654 foreach ($clusterpages as $key=>$cluster) {
1655 if ($cluster->type !== lesson_page::TYPE_QUESTION) {
1656 unset($clusterpages[$key]);
1657 } elseif ($cluster->is_unseen($seenpages)) {
1658 $unseen[] = $cluster;
1662 if (count($unseen) > 0) {
1663 // it does not contain elements, then use exitjump, otherwise find out next page/branch
1664 $nextpage = $unseen[rand(0, count($unseen)-1)];
1665 if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
1666 // if branch table, then pick a random page inside of it
1667 $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
1668 return $branchpages[rand(0, count($branchpages)-1)]->id;
1669 } else { // otherwise, return the page's id
1670 return $nextpage->id;
1673 // seen all there is to see, leave the cluster
1674 if (end($clusterpages)->nextpageid == 0) {
1677 $clusterendid = $pageid;
1678 while ($clusterendid != 0) { // this condition should not be satisfied... should be a cluster page
1679 if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_CLUSTER) {
1682 $clusterendid = $lessonpages[$clusterendid]->prevpageid;
1684 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
1685 if ($exitjump == LESSON_NEXTPAGE) {
1686 $exitjump = $lessonpages[$pageid]->nextpageid;
1688 if ($exitjump == 0) {
1690 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
1693 if (!array_key_exists($exitjump, $lessonpages)) {
1695 foreach ($lessonpages as $page) {
1696 if ($page->id === $clusterendid) {
1698 } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1699 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
1704 if (!array_key_exists($exitjump, $lessonpages)) {
1714 * Finds all pages that appear to be a subtype of the provided pageid until
1715 * an end point specified within $ends is encountered or no more pages exist
1717 * @param int $pageid
1718 * @param array $ends An array of LESSON_PAGE_* types that signify an end of
1720 * @return array An array of specialised lesson_page objects
1722 public function get_sub_pages_of($pageid, array $ends) {
1723 $lessonpages = $this->load_all_pages();
1724 $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table
1728 if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
1731 $pages[] = $lessonpages[$pageid];
1732 $pageid = $lessonpages[$pageid]->nextpageid;
1739 * Checks to see if the specified page[id] is a subpage of a type specified in
1740 * the $types array, until either there are no more pages of we find a type
1741 * corrosponding to that of a type specified in $ends
1743 * @param int $pageid The id of the page to check
1744 * @param array $types An array of types that would signify this page was a subpage
1745 * @param array $ends An array of types that mean this is not a subpage
1748 public function is_sub_page_of_type($pageid, array $types, array $ends) {
1749 $pages = $this->load_all_pages();
1750 $pageid = $pages[$pageid]->prevpageid; // move up one
1752 array_unshift($ends, 0);
1753 // go up the pages till branch table
1755 if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
1757 } else if (in_array($pages[$pageid]->qtype, $types)) {
1760 $pageid = $pages[$pageid]->prevpageid;
1766 * Abstract class representation of a page associated with a lesson.
1768 * This class should MUST be extended by all specialised page types defined in
1769 * mod/lesson/pagetypes/.
1770 * There are a handful of abstract methods that need to be defined as well as
1771 * severl methods that can optionally be defined in order to make the page type
1772 * operate in the desired way
1774 * Database properties
1775 * @property int $id The id of this lesson page
1776 * @property int $lessonid The id of the lesson this page belongs to
1777 * @property int $prevpageid The id of the page before this one
1778 * @property int $nextpageid The id of the next page in the page sequence
1779 * @property int $qtype Identifies the page type of this page
1780 * @property int $qoption Used to record page type specific options
1781 * @property int $layout Used to record page specific layout selections
1782 * @property int $display Used to record page specific display selections
1783 * @property int $timecreated Timestamp for when the page was created
1784 * @property int $timemodified Timestamp for when the page was last modified
1785 * @property string $title The title of this page
1786 * @property string $contents The rich content shown to describe the page
1787 * @property int $contentsformat The format of the contents field
1789 * Calculated properties
1790 * @property-read array $answers An array of answers for this page
1791 * @property-read bool $displayinmenublock Toggles display in the left menu block
1792 * @property-read array $jumps An array containing all the jumps this page uses
1793 * @property-read lesson $lesson The lesson this page belongs to
1794 * @property-read int $type The type of the page [question | structure]
1795 * @property-read typeid The unique identifier for the page type
1796 * @property-read typestring The string that describes this page type
1799 * @copyright 2009 Sam Hemelryk
1800 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1802 abstract class lesson_page extends lesson_base {
1805 * A reference to the lesson this page belongs to
1808 protected $lesson = null;
1810 * Contains the answers to this lesson_page once loaded
1813 protected $answers = null;
1815 * This sets the type of the page, can be one of the constants defined below
1818 protected $type = 0;
1821 * Constants used to identify the type of the page
1823 const TYPE_QUESTION = 0;
1824 const TYPE_STRUCTURE = 1;
1827 * This method should return the integer used to identify the page type within
1828 * the database and thoughout code. This maps back to the defines used in 1.x
1832 abstract protected function get_typeid();
1834 * This method should return the string that describes the pagetype
1838 abstract protected function get_typestring();
1841 * This method gets called to display the page to the user taking the lesson
1843 * @param object $renderer
1844 * @param object $attempt
1847 abstract public function display($renderer, $attempt);
1850 * Creates a new lesson_page within the database and returns the correct pagetype
1851 * object to use to interact with the new lesson
1855 * @param object $properties
1856 * @param lesson $lesson
1857 * @return lesson_page Specialised object that extends lesson_page
1859 final public static function create($properties, lesson $lesson, $context, $maxbytes) {
1861 $newpage = new stdClass;
1862 $newpage->title = $properties->title;
1863 $newpage->contents = $properties->contents_editor['text'];
1864 $newpage->contentsformat = $properties->contents_editor['format'];
1865 $newpage->lessonid = $lesson->id;
1866 $newpage->timecreated = time();
1867 $newpage->qtype = $properties->qtype;
1868 $newpage->qoption = (isset($properties->qoption))?1:0;
1869 $newpage->layout = (isset($properties->layout))?1:0;
1870 $newpage->display = (isset($properties->display))?1:0;
1871 $newpage->prevpageid = 0; // this is a first page
1872 $newpage->nextpageid = 0; // this is the only page
1874 if ($properties->pageid) {
1875 $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
1877 print_error('cannotfindpages', 'lesson');
1879 $newpage->prevpageid = $prevpage->id;
1880 $newpage->nextpageid = $prevpage->nextpageid;
1882 $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
1884 // This is the first page, there are existing pages put this at the start
1885 $newpage->nextpageid = $nextpage->id;
1889 $newpage->id = $DB->insert_record("lesson_pages", $newpage);
1891 $editor = new stdClass;
1892 $editor->id = $newpage->id;
1893 $editor->contents_editor = $properties->contents_editor;
1894 $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $editor->id);
1895 $DB->update_record("lesson_pages", $editor);
1897 if ($newpage->prevpageid > 0) {
1898 $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
1900 if ($newpage->nextpageid > 0) {
1901 $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
1904 $page = lesson_page::load($newpage, $lesson);
1905 $page->create_answers($properties);
1907 $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
1913 * This method loads a page object from the database and returns it as a
1914 * specialised object that extends lesson_page
1919 * @param lesson $lesson
1920 * @return lesson_page Specialised lesson_page object
1922 final public static function load($id, lesson $lesson) {
1925 if (is_object($id) && !empty($id->qtype)) {
1928 $page = $DB->get_record("lesson_pages", array("id" => $id));
1930 print_error('cannotfindpages', 'lesson');
1933 $manager = lesson_page_type_manager::get($lesson);
1935 $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
1936 if (!class_exists($class)) {
1937 $class = 'lesson_page';
1940 return new $class($page, $lesson);
1944 * Deletes a lesson_page from the database as well as any associated records.
1948 final public function delete() {
1950 // first delete all the associated records...
1951 $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
1952 // ...now delete the answers...
1953 $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
1954 // ..and the page itself
1955 $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
1957 // repair the hole in the linkage
1958 if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
1959 //This is the only page, no repair needed
1960 } elseif (!$this->properties->prevpageid) {
1961 // this is the first page...
1962 $page = $this->lesson->load_page($this->properties->nextpageid);
1963 $page->move(null, 0);
1964 } elseif (!$this->properties->nextpageid) {
1965 // this is the last page...
1966 $page = $this->lesson->load_page($this->properties->prevpageid);
1969 // page is in the middle...
1970 $prevpage = $this->lesson->load_page($this->properties->prevpageid);
1971 $nextpage = $this->lesson->load_page($this->properties->nextpageid);
1973 $prevpage->move($nextpage->id);
1974 $nextpage->move(null, $prevpage->id);
1980 * Moves a page by updating its nextpageid and prevpageid values within
1984 * @param int $nextpageid
1985 * @param int $prevpageid
1987 final public function move($nextpageid=null, $prevpageid=null) {
1989 if ($nextpageid === null) {
1990 $nextpageid = $this->properties->nextpageid;
1992 if ($prevpageid === null) {
1993 $prevpageid = $this->properties->prevpageid;
1995 $obj = new stdClass;
1996 $obj->id = $this->properties->id;
1997 $obj->prevpageid = $prevpageid;
1998 $obj->nextpageid = $nextpageid;
1999 $DB->update_record('lesson_pages', $obj);
2003 * Returns the answers that are associated with this page in the database
2008 final public function get_answers() {
2010 if ($this->answers === null) {
2011 $this->answers = array();
2012 $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
2014 debugging(get_string('cannotfindanswer', 'lesson'));
2017 foreach ($answers as $answer) {
2018 $this->answers[count($this->answers)] = new lesson_page_answer($answer);
2021 return $this->answers;
2025 * Returns the lesson this page is associated with
2029 final protected function get_lesson() {
2030 return $this->lesson;
2034 * Returns the type of page this is. Not to be confused with page type
2038 final protected function get_type() {
2043 * Records an attempt at this page
2046 * @param stdClass $context
2047 * @return stdClass Returns the result of the attempt
2049 final public function record_attempt($context) {
2050 global $DB, $USER, $OUTPUT;
2053 * This should be overriden by each page type to actually check the response
2054 * against what ever custom criteria they have defined
2056 $result = $this->check_answer();
2058 $result->attemptsremaining = 0;
2059 $result->maxattemptsreached = false;
2061 if ($result->noanswer) {
2062 $result->newpageid = $this->properties->id; // display same page again
2063 $result->feedback = get_string('noanswer', 'lesson');
2065 if (!has_capability('mod/lesson:manage', $context)) {
2066 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2067 // record student's attempt
2068 $attempt = new stdClass;
2069 $attempt->lessonid = $this->lesson->id;
2070 $attempt->pageid = $this->properties->id;
2071 $attempt->userid = $USER->id;
2072 $attempt->answerid = $result->answerid;
2073 $attempt->retry = $nretakes;
2074 $attempt->correct = $result->correctanswer;
2075 if($result->userresponse !== null) {
2076 $attempt->useranswer = $result->userresponse;
2079 $attempt->timeseen = time();
2080 // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
2081 if (isset($USER->modattempts[$this->lesson->id])) {
2082 $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
2085 $DB->insert_record("lesson_attempts", $attempt);
2086 // "number of attempts remaining" message if $this->lesson->maxattempts > 1
2087 // displaying of message(s) is at the end of page for more ergonomic display
2088 if (!$result->correctanswer && ($result->newpageid == 0)) {
2089 // wrong answer and student is stuck on this page - check how many attempts
2090 // the student has had at this page/question
2091 $nattempts = $DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id),"retry", $nretakes);
2092 // retreive the number of attempts left counter for displaying at bottom of feedback page
2093 if ($nattempts >= $this->lesson->maxattempts) {
2094 if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2095 $result->maxattemptsreached = true;
2097 $result->newpageid = LESSON_NEXTPAGE;
2098 } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2099 $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
2103 // TODO: merge this code with the jump code below. Convert jumpto page into a proper page id
2104 if ($result->newpageid == 0) {
2105 $result->newpageid = $this->properties->id;
2106 } elseif ($result->newpageid == LESSON_NEXTPAGE) {
2107 $nextpage = $this->lesson->get_next_page($this->properties->nextpageid);
2108 if ($nextpage === false) {
2109 $result->newpageid = LESSON_EOL;
2111 $result->newpageid = $nextpage->id;
2115 // Determine default feedback if necessary
2116 if (empty($result->response)) {
2117 if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
2118 // These conditions have been met:
2119 // 1. The lesson manager has not supplied feedback to the student
2120 // 2. Not displaying default feedback
2121 // 3. The user did provide an answer
2122 // 4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
2124 $result->nodefaultresponse = true; // This will cause a redirect below
2125 } else if ($result->isessayquestion) {
2126 $result->response = get_string('defaultessayresponse', 'lesson');
2127 } else if ($result->correctanswer) {
2128 $result->response = get_string('thatsthecorrectanswer', 'lesson');
2130 $result->response = get_string('thatsthewronganswer', 'lesson');
2134 if ($result->response) {
2135 if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
2136 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2137 $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
2138 if ($qattempts == 1) {
2139 $result->feedback = $OUTPUT->box(get_string("firstwrong", "lesson"), 'feedback');
2141 $result->feedback = $OUTPUT->BOX(get_string("secondpluswrong", "lesson"), 'feedback');
2144 $class = 'response';
2145 if ($result->correctanswer) {
2146 $class .= ' correct'; //CSS over-ride this if they exist (!important)
2147 } else if (!$result->isessayquestion) {
2148 $class .= ' incorrect'; //CSS over-ride this if they exist (!important)
2150 $options = new stdClass;
2151 $options->noclean = true;
2152 $options->para = true;
2153 $result->feedback = $OUTPUT->box(format_text($this->properties->contents, $this->properties->contentsformat, $options), 'generalbox boxaligncenter');
2154 $result->feedback .= '<div class="correctanswer generalbox"><em>'.get_string("youranswer", "lesson").'</em> : '.$result->studentanswer; // already in clean html
2155 $result->feedback .= $OUTPUT->box($result->response, $class); // already conerted to HTML
2165 * Returns the string for a jump name
2168 * @param int $jumpto Jump code or page ID
2171 final protected function get_jump_name($jumpto) {
2173 static $jumpnames = array();
2175 if (!array_key_exists($jumpto, $jumpnames)) {
2177 $jumptitle = get_string('thispage', 'lesson');
2178 } elseif ($jumpto == LESSON_NEXTPAGE) {
2179 $jumptitle = get_string('nextpage', 'lesson');
2180 } elseif ($jumpto == LESSON_EOL) {
2181 $jumptitle = get_string('endoflesson', 'lesson');
2182 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2183 $jumptitle = get_string('unseenpageinbranch', 'lesson');
2184 } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
2185 $jumptitle = get_string('previouspage', 'lesson');
2186 } elseif ($jumpto == LESSON_RANDOMPAGE) {
2187 $jumptitle = get_string('randompageinbranch', 'lesson');
2188 } elseif ($jumpto == LESSON_RANDOMBRANCH) {
2189 $jumptitle = get_string('randombranch', 'lesson');
2190 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2191 $jumptitle = get_string('clusterjump', 'lesson');
2193 if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
2194 $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
2197 $jumpnames[$jumpto] = format_string($jumptitle,true);
2200 return $jumpnames[$jumpto];
2204 * Construstor method
2205 * @param object $properties
2206 * @param lesson $lesson
2208 public function __construct($properties, lesson $lesson) {
2209 parent::__construct($properties);
2210 $this->lesson = $lesson;
2214 * Returns the score for the attempt
2215 * This may be overriden by page types that require manual grading
2216 * @param array $answers
2217 * @param object $attempt
2220 public function earned_score($answers, $attempt) {
2221 return $answers[$attempt->answerid]->score;
2225 * This is a callback method that can be override and gets called when ever a page
2228 * @param bool $canmanage True if the user has the manage cap
2231 public function callback_on_view($canmanage) {
2236 * Updates a lesson page and its answers within the database
2238 * @param object $properties
2241 public function update($properties, $context = null, $maxbytes = null) {
2243 $answers = $this->get_answers();
2244 $properties->id = $this->properties->id;
2245 $properties->lessonid = $this->lesson->id;
2246 if (empty($properties->qoption)) {
2247 $properties->qoption = '0';
2249 if (empty($context)) {
2250 $context = $PAGE->context;
2252 if ($maxbytes === null) {
2253 $maxbytes =get_max_upload_file_size();
2255 $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'mod_lesson', 'page_contents', $properties->id);
2256 $DB->update_record("lesson_pages", $properties);
2258 for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2259 if (!array_key_exists($i, $this->answers)) {
2260 $this->answers[$i] = new stdClass;
2261 $this->answers[$i]->lessonid = $this->lesson->id;
2262 $this->answers[$i]->pageid = $this->id;
2263 $this->answers[$i]->timecreated = $this->timecreated;
2265 if (!empty($properties->answer_editor[$i])) {
2266 $this->answers[$i]->answer = $properties->answer_editor[$i]['text'];
2267 $this->answers[$i]->answerformat = $properties->answer_editor[$i]['format'];
2268 if (isset($properties->response_editor[$i])) {
2269 $this->answers[$i]->response = $properties->response_editor[$i]['text'];
2270 $this->answers[$i]->responseformat = $properties->response_editor[$i]['format'];
2272 if (isset($properties->jumpto[$i])) {
2273 $this->answers[$i]->jumpto = $properties->jumpto[$i];
2275 if ($this->lesson->custom && isset($properties->score[$i])) {
2276 $this->answers[$i]->score = $properties->score[$i];
2278 if (!isset($this->answers[$i]->id)) {
2279 $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]);
2281 $DB->update_record("lesson_answers", $this->answers[$i]->properties());
2292 * Can be set to true if the page requires a static link to create a new instance
2293 * instead of simply being included in the dropdown
2294 * @param int $previd
2297 public function add_page_link($previd) {
2302 * Returns true if a page has been viewed before
2304 * @param array|int $param Either an array of pages that have been seen or the
2305 * number of retakes a user has had
2308 public function is_unseen($param) {
2310 if (is_array($param)) {
2311 $seenpages = $param;
2312 return (!array_key_exists($this->properties->id, $seenpages));
2315 if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
2323 * Checks to see if a page has been answered previously
2324 * @param int $nretakes
2327 public function is_unanswered($nretakes) {
2329 if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
2336 * Creates answers within the database for this lesson_page. Usually only ever
2337 * called when creating a new page instance
2338 * @param object $properties
2341 public function create_answers($properties) {
2343 // now add the answers
2344 $newanswer = new stdClass;
2345 $newanswer->lessonid = $this->lesson->id;
2346 $newanswer->pageid = $this->properties->id;
2347 $newanswer->timecreated = $this->properties->timecreated;
2351 for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2352 $answer = clone($newanswer);
2353 if (!empty($properties->answer_editor[$i])) {
2354 $answer->answer = $properties->answer_editor[$i]['text'];
2355 $answer->answerformat = $properties->answer_editor[$i]['format'];
2356 if (isset($properties->response_editor[$i])) {
2357 $answer->response = $properties->response_editor[$i]['text'];
2358 $answer->responseformat = $properties->response_editor[$i]['format'];
2360 if (isset($properties->jumpto[$i])) {
2361 $answer->jumpto = $properties->jumpto[$i];
2363 if ($this->lesson->custom && isset($properties->score[$i])) {
2364 $answer->score = $properties->score[$i];
2366 $answer->id = $DB->insert_record("lesson_answers", $answer);
2367 $answers[$answer->id] = new lesson_page_answer($answer);
2373 $this->answers = $answers;
2378 * This method MUST be overriden by all question page types, or page types that
2379 * wish to score a page.
2381 * The structure of result should always be the same so it is a good idea when
2382 * overriding this method on a page type to call
2384 * $result = parent::check_answer();
2386 * before modifiying it as required.
2390 public function check_answer() {
2391 $result = new stdClass;
2392 $result->answerid = 0;
2393 $result->noanswer = false;
2394 $result->correctanswer = false;
2395 $result->isessayquestion = false; // use this to turn off review button on essay questions
2396 $result->response = '';
2397 $result->newpageid = 0; // stay on the page
2398 $result->studentanswer = ''; // use this to store student's answer(s) in order to display it on feedback page
2399 $result->userresponse = null;
2400 $result->feedback = '';
2401 $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off
2406 * True if the page uses a custom option
2408 * Should be override and set to true if the page uses a custom option.
2412 public function has_option() {
2417 * Returns the maximum number of answers for this page given the maximum number
2418 * of answers permitted by the lesson.
2420 * @param int $default
2423 public function max_answers($default) {
2428 * Returns the properties of this lesson page as an object
2431 public function properties() {
2432 $properties = clone($this->properties);
2433 if ($this->answers === null) {
2434 $this->get_answers();
2436 if (count($this->answers)>0) {
2438 foreach ($this->answers as $answer) {
2439 $properties->{'answer_editor['.$count.']'} = array('text'=>$answer->answer, 'format'=>$answer->answerformat);
2440 $properties->{'response_editor['.$count.']'} = array('text'=>$answer->response, 'format'=>$answer->responseformat);
2441 $properties->{'jumpto['.$count.']'} = $answer->jumpto;
2442 $properties->{'score['.$count.']'} = $answer->score;
2450 * Returns an array of options to display whn choosing the jumpto for a page/answer
2452 * @param int $pageid
2453 * @param lesson $lesson
2456 public static function get_jumptooptions($pageid, lesson $lesson) {
2459 $jump[0] = get_string("thispage", "lesson");
2460 $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
2461 $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
2462 $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
2468 $pages = $lesson->load_all_pages();
2469 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))) {
2470 $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
2471 $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
2473 if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
2474 $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
2476 if (!optional_param('firstpage', 0, PARAM_INT)) {
2477 $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
2480 $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
2481 $jump[$apageid] = strip_tags(format_string($title,true));
2482 $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
2484 // last page reached
2492 * Returns the contents field for the page properly formatted and with plugin
2493 * file url's converted
2496 public function get_contents() {
2498 if (!empty($this->properties->contents)) {
2499 if (!isset($this->properties->contentsformat)) {
2500 $this->properties->contentsformat = FORMAT_HTML;
2502 $context = get_context_instance(CONTEXT_MODULE, $PAGE->cm->id);
2503 return file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'mod_lesson', 'page_contents', $this->properties->id);
2510 * Set to true if this page should display in the menu block
2513 protected function get_displayinmenublock() {
2518 * Get the string that describes the options of this page type
2521 public function option_description_string() {
2526 * Updates a table with the answers for this page
2527 * @param html_table $table
2528 * @return html_table
2530 public function display_answers(html_table $table) {
2531 $answers = $this->get_answers();
2533 foreach ($answers as $answer) {
2535 $cells[] = "<span class=\"label\">".get_string("jump", "lesson")." $i<span>: ";
2536 $cells[] = $this->get_jump_name($answer->jumpto);
2537 $table->data[] = new html_table_row($cells);
2539 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
2547 * Determines if this page should be grayed out on the management/report screens
2548 * @return int 0 or 1
2550 protected function get_grayout() {
2555 * Adds stats for this page to the &pagestats object. This should be defined
2556 * for all page types that grade
2557 * @param array $pagestats
2561 public function stats(array &$pagestats, $tries) {
2566 * Formats the answers of this page for a report
2568 * @param object $answerpage
2569 * @param object $answerdata
2570 * @param object $useranswer
2571 * @param array $pagestats
2572 * @param int $i Count of first level answers
2573 * @param int $n Count of second level answers
2574 * @return object The answer page for this
2576 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
2577 $answers = $this->get_answers();
2578 $formattextdefoptions = new stdClass;
2579 $formattextdefoptions->para = false; //I'll use it widely in this page
2580 foreach ($answers as $answer) {
2581 $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
2582 $answerdata->answers[] = array($data, "");
2583 $answerpage->answerdata = $answerdata;
2589 * Gets an array of the jumps used by the answers of this page
2593 public function get_jumps() {
2596 $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
2597 if ($answers = $this->get_answers()) {
2598 foreach ($answers as $answer) {
2599 $jumps[] = $this->get_jump_name($answer->jumpto);
2605 * Informs whether this page type require manual grading or not
2608 public function requires_manual_grading() {
2613 * A callback method that allows a page to override the next page a user will
2614 * see during when this page is being completed.
2617 public function override_next_page() {
2622 * This method is used to determine if this page is a valid page
2624 * @param array $validpages
2625 * @param array $pageviews
2626 * @return int The next page id to check
2628 public function valid_page_and_view(&$validpages, &$pageviews) {
2629 $validpages[$this->properties->id] = 1;
2630 return $this->properties->nextpageid;
2635 * Class used to represent an answer to a page
2637 * @property int $id The ID of this answer in the database
2638 * @property int $lessonid The ID of the lesson this answer belongs to
2639 * @property int $pageid The ID of the page this answer belongs to
2640 * @property int $jumpto Identifies where the user goes upon completing a page with this answer
2641 * @property int $grade The grade this answer is worth
2642 * @property int $score The score this answer will give
2643 * @property int $flags Used to store options for the answer
2644 * @property int $timecreated A timestamp of when the answer was created
2645 * @property int $timemodified A timestamp of when the answer was modified
2646 * @property string $answer The answer itself
2647 * @property string $response The response the user sees if selecting this answer
2649 * @copyright 2009 Sam Hemelryk
2650 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2652 class lesson_page_answer extends lesson_base {
2655 * Loads an page answer from the DB
2658 * @return lesson_page_answer
2660 public static function load($id) {
2662 $answer = $DB->get_record("lesson_answers", array("id" => $id));
2663 return new lesson_page_answer($answer);
2667 * Given an object of properties and a page created answer(s) and saves them
2670 * @param stdClass $properties
2671 * @param lesson_page $page
2674 public static function create($properties, lesson_page $page) {
2675 return $page->create_answers($properties);
2681 * A management class for page types
2683 * This class is responsible for managing the different pages. A manager object can
2684 * be retrieved by calling the following line of code:
2686 * $manager = lesson_page_type_manager::get($lesson);
2688 * The first time the page type manager is retrieved the it includes all of the
2689 * different page types located in mod/lesson/pagetypes.
2691 * @copyright 2009 Sam Hemelryk
2692 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2694 class lesson_page_type_manager {
2697 * An array of different page type classes
2700 protected $types = array();
2703 * Retrieves the lesson page type manager object
2705 * If the object hasn't yet been created it is created here.
2707 * @staticvar lesson_page_type_manager $pagetypemanager
2708 * @param lesson $lesson
2709 * @return lesson_page_type_manager
2711 public static function get(lesson $lesson) {
2712 static $pagetypemanager;
2713 if (!($pagetypemanager instanceof lesson_page_type_manager)) {
2714 $pagetypemanager = new lesson_page_type_manager();
2715 $pagetypemanager->load_lesson_types($lesson);
2717 return $pagetypemanager;
2721 * Finds and loads all lesson page types in mod/lesson/pagetypes
2723 * @param lesson $lesson
2725 public function load_lesson_types(lesson $lesson) {
2727 $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
2728 $dir = dir($basedir);
2729 while (false !== ($entry = $dir->read())) {
2730 if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
2733 require_once($basedir.$entry);
2734 $class = 'lesson_page_type_'.strtok($entry,'.');
2735 if (class_exists($class)) {
2736 $pagetype = new $class(new stdClass, $lesson);
2737 $this->types[$pagetype->typeid] = $pagetype;
2744 * Returns an array of strings to describe the loaded page types
2746 * @param int $type Can be used to return JUST the string for the requested type
2749 public function get_page_type_strings($type=null, $special=true) {
2751 foreach ($this->types as $pagetype) {
2752 if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
2753 $types[$pagetype->typeid] = $pagetype->typestring;
2760 * Returns the basic string used to identify a page type provided with an id
2762 * This string can be used to instantiate or identify the page type class.
2763 * If the page type id is unknown then 'unknown' is returned
2768 public function get_page_type_idstring($id) {
2769 foreach ($this->types as $pagetype) {
2770 if ((int)$pagetype->typeid === (int)$id) {
2771 return $pagetype->idstring;
2778 * Loads a page for the provided lesson given it's id
2780 * This function loads a page from the lesson when given both the lesson it belongs
2781 * to as well as the page's id.
2782 * If the page doesn't exist an error is thrown
2784 * @param int $pageid The id of the page to load
2785 * @param lesson $lesson The lesson the page belongs to
2786 * @return lesson_page A class that extends lesson_page
2788 public function load_page($pageid, lesson $lesson) {
2790 if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
2791 print_error('cannotfindpages', 'lesson');
2793 $pagetype = get_class($this->types[$page->qtype]);
2794 $page = new $pagetype($page, $lesson);
2799 * This function loads ALL pages that belong to the lesson.
2801 * @param lesson $lesson
2802 * @return array An array of lesson_page_type_*
2804 public function load_all_pages(lesson $lesson) {
2806 if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
2807 print_error('cannotfindpages', 'lesson');
2809 foreach ($pages as $key=>$page) {
2810 $pagetype = get_class($this->types[$page->qtype]);
2811 $pages[$key] = new $pagetype($page, $lesson);
2814 $orderedpages = array();
2818 foreach ($pages as $page) {
2819 if ((int)$page->prevpageid === (int)$lastpageid) {
2820 $orderedpages[$page->id] = $page;
2821 unset($pages[$page->id]);
2822 $lastpageid = $page->id;
2823 if ((int)$page->nextpageid===0) {
2832 return $orderedpages;
2836 * Fetchs an mform that can be used to create/edit an page
2838 * @param int $type The id for the page type
2839 * @param array $arguments Any arguments to pass to the mform
2840 * @return lesson_add_page_form_base
2842 public function get_page_form($type, $arguments) {
2843 $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
2844 if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
2845 debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
2846 $class = 'lesson_add_page_form_selection';
2847 } else if ($class === 'lesson_add_page_form_unknown') {
2848 $class = 'lesson_add_page_form_selection';
2850 return new $class(null, $arguments);
2854 * Returns an array of links to use as add page links
2855 * @param int $previd The id of the previous page
2858 public function get_add_page_type_links($previd) {
2863 foreach ($this->types as $key=>$type) {
2864 if ($link = $type->add_page_link($previd)) {
2865 $links[$key] = $link;
2874 * Abstract class that page type's MUST inherit from.
2876 * This is the abstract class that ALL add page type forms must extend.
2877 * You will notice that all but two of the methods this class contains are final.
2878 * Essentially the only thing that extending classes can do is extend custom_definition.
2879 * OR if it has a special requirement on creation it can extend construction_override
2882 * @copyright 2009 Sam Hemelryk
2883 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2885 abstract class lesson_add_page_form_base extends moodleform {
2888 * This is the classic define that is used to identify this pagetype.
2889 * Will be one of LESSON_*
2895 * The simple string that describes the page type e.g. truefalse, multichoice
2898 public $qtypestring;
2901 * An array of options used in the htmleditor
2904 protected $editoroptions = array();
2907 * True if this is a standard page of false if it does something special.
2908 * Questions are standard pages, branch tables are not
2911 protected $standard = true;
2914 * Each page type can and should override this to add any custom elements to
2915 * the basic form that they want
2917 public function custom_definition() {}
2920 * Used to determine if this is a standard page or a special page
2923 public final function is_standard() {
2924 return (bool)$this->standard;
2928 * Add the required basic elements to the form.
2930 * This method adds the basic elements to the form including title and contents
2931 * and then calls custom_definition();
2933 public final function definition() {
2934 $mform = $this->_form;
2935 $editoroptions = $this->_customdata['editoroptions'];
2937 $mform->addElement('header', 'qtypeheading', get_string('addaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
2939 $mform->addElement('hidden', 'id');
2940 $mform->setType('id', PARAM_INT);
2942 $mform->addElement('hidden', 'pageid');
2943 $mform->setType('pageid', PARAM_INT);
2945 if ($this->standard === true) {
2946 $mform->addElement('hidden', 'qtype');
2947 $mform->setType('qtype', PARAM_TEXT);
2949 $mform->addElement('text', 'title', get_string("pagetitle", "lesson"), array('size'=>70));
2950 $mform->setType('title', PARAM_TEXT);
2951 $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
2952 $mform->addElement('editor', 'contents_editor', get_string("pagecontents", "lesson"), null, $this->editoroptions);
2953 $mform->setType('contents_editor', PARAM_CLEANHTML);
2956 $this->custom_definition();
2958 if ($this->_customdata['edit'] === true) {
2959 $mform->addElement('hidden', 'edit', 1);
2960 $this->add_action_buttons(get_string('cancel'), get_string("savepage", "lesson"));
2962 $this->add_action_buttons(get_string('cancel'), get_string("addaquestionpage", "lesson"));
2967 * Convenience function: Adds a jumpto select element
2969 * @param string $name
2970 * @param string|null $label
2971 * @param int $selected The page to select by default
2973 protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
2974 $title = get_string("jump", "lesson");
2975 if ($label === null) {
2978 if (is_int($name)) {
2979 $name = "jumpto[$name]";
2981 $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
2982 $this->_form->setDefault($name, $selected);
2983 $this->_form->addHelpButton($name, 'jumps', 'lesson');
2987 * Convenience function: Adds a score input element
2989 * @param string $name
2990 * @param string|null $label
2991 * @param mixed $value The default value
2993 protected final function add_score($name, $label=null, $value=null) {
2994 if ($label === null) {
2995 $label = get_string("score", "lesson");
2997 if (is_int($name)) {
2998 $name = "score[$name]";
3000 $this->_form->addElement('text', $name, $label, array('size'=>5));
3001 if ($value !== null) {
3002 $this->_form->setDefault($name, $value);
3007 * Convenience function: Adds an answer editor
3009 * @param int $count The count of the element to add
3011 protected final function add_answer($count) {
3012 $this->_form->addElement('editor', 'answer_editor['.$count.']', get_string('answer', 'lesson'), null, array('noclean'=>true));
3013 $this->_form->setDefault('answer_editor['.$count.']', array('text'=>'', 'format'=>FORMAT_MOODLE));
3016 * Convenience function: Adds an response editor
3018 * @param int $count The count of the element to add
3020 protected final function add_response($count) {
3021 $this->_form->addElement('editor', 'response_editor['.$count.']', get_string('response', 'lesson'), null, array('noclean'=>true));
3022 $this->_form->setDefault('response_editor['.$count.']', array('text'=>'', 'format'=>FORMAT_MOODLE));
3026 * A function that gets called upon init of this object by the calling script.
3028 * This can be used to process an immediate action if required. Currently it
3029 * is only used in special cases by non-standard page types.
3033 public function construction_override() {