8c336d489a47765f506046ea86e1be0895259d50
[moodle.git] / mod / lesson / lib.php
1 <?php
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/>.
18 /**
19  * Standard library of functions and constants for lesson
20  *
21  * @package   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  **/
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");
36 /**
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.
41  *
42  * @global object
43  * @global object
44  * @param object $lesson Lesson post data from the form
45  * @return int
46  **/
47 function lesson_add_instance($data, $mform) {
48     global $SESSION, $DB;
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));
64         }
65     }
67     lesson_process_post_save($data);
69     lesson_grade_item_update($data);
71     return $lesson->id;
72 }
74 /**
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.
78  *
79  * @global object
80  * @param object $lesson Lesson post data from the form
81  * @return boolean
82  **/
83 function lesson_update_instance($data, $mform) {
84     global $DB;
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!
94     }
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));
100         }
101     }
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);
111     return $result;
115 /**
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.
119  *
120  * @global object
121  * @param int $id
122  * @return bool
123  */
124 function lesson_delete_instance($id) {
125     global $DB;
126     $lesson = $DB->get_record("lesson", array("id"=>$id), '*', MUST_EXIST);
127     $lesson = new lesson($lesson);
128     return $lesson->delete();
131 /**
132  * Given a course object, this function will clean up anything that
133  * would be leftover after all the instances were deleted
134  *
135  * @global object
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
138  * @return boolean
139  */
140 function lesson_delete_course($course, $feedback=true) {
141     return true;
144 /**
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
150  *
151  * @global object
152  * @param object $course
153  * @param object $user
154  * @param object $mod
155  * @param object $lesson
156  * @return object
157  */
158 function lesson_user_outline($course, $user, $mod, $lesson) {
159     global $DB;
161     global $CFG;
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");
167     } else {
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");
172     }
173     return $return;
176 /**
177  * Print a detailed representation of what a  user has done with
178  * a given particular instance of this module, for user activity reports.
179  *
180  * @global object
181  * @param object $course
182  * @param object $user
183  * @param object $mod
184  * @param object $lesson
185  * @return bool
186  */
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);
198         }
199     }
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;
214         $retry = 0;
215         $npages = 0;
216         $ncorrect = 0;
218         foreach ($attempts as $attempt) {
219             if ($attempt->retry == $retry) {
220                 $npages++;
221                 if ($attempt->correct) {
222                     $ncorrect++;
223                 }
224                 $timeseen = $attempt->timeseen;
225             } else {
226                 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
227                 $retry++;
228                 $npages = 1;
229                 if ($attempt->correct) {
230                     $ncorrect = 1;
231                 } else {
232                     $ncorrect = 0;
233                 }
234             }
235         }
236         if ($npages) {
237                 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
238         }
239         echo html_writer::table($table);
240         echo $OUTPUT->box_end();
241     }
243     return true;
246 /**
247  * Prints lesson summaries on MyMoodle Page
248  *
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.
252  *
253  * @global object
254  * @global stdClass
255  * @global object
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 )
259  * @return void
260  */
261 function lesson_print_overview($courses, &$htmlarray) {
262     global $USER, $CFG, $DB, $OUTPUT;
264     if (!$lessons = get_all_instances_in_courses('lesson', $courses)) {
265         return;
266     }
268 /// Get Necessary Strings
269     $strlesson       = get_string('modulename', 'lesson');
270     $strnotattempted = get_string('nolessonattempts', 'lesson');
271     $strattempted    = get_string('lessonattempted', 'lesson');
273     $now = time();
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
279             // Lesson name
280             if (!$lesson->visible) {
281                 $class = ' class="dimmed"';
282             } else {
283                 $class = '';
284             }
285             $str = $OUTPUT->box("$strlesson: <a$class href=\"$CFG->wwwroot/mod/lesson/view.php?id=$lesson->coursemodule\">".
286                              format_string($lesson->name).'</a>', 'name');
288             // Deadline
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');
296             } else {
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');
300                 } else {
301                     $str .= $OUTPUT->box($strnotattempted, 'info');
302                 }
303             }
304             $str = $OUTPUT->box($str, 'lesson overview');
306             if (empty($htmlarray[$lesson->course]['lesson'])) {
307                 $htmlarray[$lesson->course]['lesson'] = $str;
308             } else {
309                 $htmlarray[$lesson->course]['lesson'] .= $str;
310             }
311         }
312     }
315 /**
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 ...
319  * @global stdClass
320  * @return bool true
321  */
322 function lesson_cron () {
323     global $CFG;
325     return true;
328 /**
329  * Return grade for given user or all users.
330  *
331  * @global stdClass
332  * @global object
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
336  */
337 function lesson_get_user_grades($lesson, $userid=0) {
338     global $CFG, $DB;
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";
347     }
348     else {
349         $user="";
350         $fuser="";
351     }
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
358                            $user
359                   GROUP BY u.id";
360         } else {
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
364                            $user
365                   GROUP BY u.id";
366         }
367         unset($params['lessonid2']);
368         unset($params['userid2']);
369     } else {
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
374                              $fuser
375                        GROUP BY uu.id";
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
381                        $user";
382     }
384     return $DB->get_records_sql($sql, $params);
387 /**
388  * Update grades in central gradebook
389  *
390  * @global stdclass
391  * @global object
392  * @param object $lesson
393  * @param int $userid specific user only, 0 means all
394  * @param bool $nullifnone
395  */
396 function lesson_update_grades($lesson, $userid=0, $nullifnone=true) {
397     global $CFG, $DB;
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);
412     } else {
413         lesson_grade_item_update($lesson);
414     }
417 /**
418  * Update all grades in gradebook.
419  *
420  * @global object
421  */
422 function lesson_upgrade_grades() {
423     global $DB;
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);
435         $i=0;
436         foreach ($rs as $lesson) {
437             $i++;
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).");
441         }
442         $rs->close();
443     }
446 /**
447  * Create grade item for given lesson
448  *
449  * @global stdClass
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
455  */
456 function lesson_grade_item_update($lesson, $grades=NULL) {
457     global $CFG;
458     if (!function_exists('grade_update')) { //workaround for buggy PHP versions
459         require_once($CFG->libdir.'/gradelib.php');
460     }
462     if (array_key_exists('cmidnumber', $lesson)) { //it may not be always present
463         $params = array('itemname'=>$lesson->name, 'idnumber'=>$lesson->cmidnumber);
464     } else {
465         $params = array('itemname'=>$lesson->name);
466     }
468     if ($lesson->grade > 0) {
469         $params['gradetype']  = GRADE_TYPE_VALUE;
470         $params['grademax']   = $lesson->grade;
471         $params['grademin']   = 0;
473     } else {
474         $params['gradetype']  = GRADE_TYPE_NONE;
475     }
477     if ($grades  === 'reset') {
478         $params['reset'] = true;
479         $grades = NULL;
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);
486         }
487         foreach ($grades as $key => $grade) {
488             if (!is_array($grade)) {
489                 $grades[$key] = $grade = (array) $grade;
490             }
491             $grades[$key]['rawgrade'] = ($grade['rawgrade'] * $lesson->grade / 100);
492         }
493     }
495     return grade_update('mod/lesson', $lesson->course, 'mod', 'lesson', $lesson->id, 0, $grades, $params);
498 /**
499  * Delete grade item for given lesson
500  *
501  * @global stdClass
502  * @param object $lesson object
503  * @return object lesson
504  */
505 function lesson_grade_item_delete($lesson) {
506     global $CFG;
511 /**
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...)
515  *
516  * @global stdClass
517  * @global object
518  * @param int $lessonid
519  * @return array
520  */
521 function lesson_get_participants($lessonid) {
522     global $CFG, $DB;
524     //Get students
525     $params = array ("lessonid" => $lessonid);
526     $students = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
527                                  FROM {user} u,
528                                       {lesson_attempts} a
529                                  WHERE a.lessonid = :lessonid and
530                                        u.id = a.userid", $params);
532     //Return students array (it contains an array of unique users)
533     return ($students);
536 /**
537  * @return array
538  */
539 function lesson_get_view_actions() {
540     return array('view','view all');
543 /**
544  * @return array
545  */
546 function lesson_get_post_actions() {
547     return array('end','start');
550 /**
551  * Runs any processes that must run before
552  * a lesson insert/update
553  *
554  * @global object
555  * @param object $lesson Lesson form data
556  * @return void
557  **/
558 function lesson_process_pre_save(&$lesson) {
559     global $DB;
561     $lesson->timemodified = time();
563     if (empty($lesson->timed)) {
564         $lesson->timed = 0;
565     }
566     if (empty($lesson->timespent) or !is_numeric($lesson->timespent) or $lesson->timespent < 0) {
567         $lesson->timespent = 0;
568     }
569     if (!isset($lesson->completed)) {
570         $lesson->completed = 0;
571     }
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;
576     }
578     if (empty($lesson->width)) {
579         $lesson->width = 640;
580     }
581     if (empty($lesson->height)) {
582         $lesson->height = 480;
583     }
584     if (empty($lesson->bgcolor)) {
585         $lesson->bgcolor = '#FFFFFF';
586     }
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);
600     }
603 /**
604  * Runs any processes that must be run
605  * after a lesson insert/update
606  *
607  * @global object
608  * @param object $lesson Lesson form data
609  * @return void
610  **/
611 function lesson_process_post_save(&$lesson) {
612     global $DB;
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);
617             $event->delete();
618         }
619     }
621     $event = new stdClass;
622     $event->description = $lesson->name;
623     $event->courseid    = $lesson->course;
624     $event->groupid     = 0;
625     $event->userid      = 0;
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));
639     } else {
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));
650         }
651     }
655 /**
656  * Implementation of the function for printing the form elements that control
657  * whether the course reset functionality affects the lesson.
658  *
659  * @param $mform form passed by reference
660  */
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'));
666 /**
667  * Course reset form defaults.
668  * @param object $course
669  * @return array
670  */
671 function lesson_reset_course_form_defaults($course) {
672     return array('reset_lesson'=>1);
675 /**
676  * Removes all grades from gradebook
677  *
678  * @global stdClass
679  * @global object
680  * @param int $courseid
681  * @param string optional type
682  */
683 function lesson_reset_gradebook($courseid, $type='') {
684     global $CFG, $DB;
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');
693         }
694     }
697 /**
698  * Actual implementation of the rest coures functionality, delete all the
699  * lesson attempts for course $data->courseid.
700  *
701  * @global stdClass
702  * @global object
703  * @param object $data the data submitted from the reset course.
704  * @return array status array
705  */
706 function lesson_reset_userdata($data) {
707     global $CFG, $DB;
709     $componentstr = get_string('modulenameplural', 'lesson');
710     $status = array();
712     if (!empty($data->reset_lesson)) {
713         $lessonssql = "SELECT l.id
714                          FROM {lesson} l
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);
726         }
728         $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false);
729     }
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);
735     }
737     return $status;
740 /**
741  * Returns all other caps used in module
742  * @return array
743  */
744 function lesson_get_extra_capabilities() {
745     return array('moodle/site:accessallgroups');
748 /**
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
758  */
759 function lesson_supports($feature) {
760     switch($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;
770     }
773 /**
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
777  * available
778  *
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
783  */
784 function lesson_extend_navigation($navigation, $course, $module, $cm) {
785     /**
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
788      * you content.
789      */
790     $navigation->nodetype = navigation_node::NODETYPE_LEAF;
793 /**
794  * This function extends the settings navigation block for the site.
795  *
796  * It is safe to rely on PAGE here as we will only ever be within the module
797  * context when this is called
798  *
799  * @param settings_navigation $settings
800  * @param navigation_node $lessonnode
801  */
802 function lesson_extend_settings_navigation($settings, $lessonnode) {
803     global $PAGE, $DB;
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);
810     if ($canedit) {
811         $url = new moodle_url('/mod/lesson/edit.php', array('id'=>$PAGE->cm->id));
812         $lessonnode->add(get_string('edit', 'lesson'), $url);
813     }
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);
821     }
823     if ($canedit) {
824         $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$PAGE->cm->id));
825         $lessonnode->add(get_string('manualgrading', 'lesson'), $url);
826     }
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);
831     }
834 /**
835  * Get list of available import or export formats
836  *
837  * Copied and modified from lib/questionlib.php
838  *
839  * @param string $type 'import' if import list, otherwise export list assumed
840  * @return array sorted list of import/export formats available
841  */
842 function lesson_get_import_export_formats($type) {
843     global $CFG;
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);
851         } else {
852             continue;
853         }
854         $classname = "qformat_$fileformat";
855         $format_class = new $classname();
856         if ($type=='import') {
857             $provided = $format_class->provide_import();
858         } else {
859             $provided = $format_class->provide_export();
860         }
861         if ($provided) {
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
867                 }
868             }
869             $fileformatnames[$fileformat] = $formatname;
870         }
871     }
872     natcasesort($fileformatnames);
874     return $fileformatnames;
877 /**
878  * Serves the lesson attachments. Implements needed access control ;-)
879  *
880  * @param object $course
881  * @param object $cm
882  * @param object $context
883  * @param string $filearea
884  * @param array $args
885  * @param bool $forcedownload
886  * @return bool false if file not found, does not return if found - justsend the file
887  */
888 function lesson_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload) {
889     global $CFG, $DB;
891     if ($context->contextlevel != CONTEXT_MODULE) {
892         return false;
893     }
895     $fileareas = lesson_get_file_areas();
896     if (!array_key_exists($filearea, $fileareas)) {
897         return false;
898     }
900     if (!$lesson = $DB->get_record('lesson', array('id'=>$cm->instance))) {
901         return false;
902     }
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))) {
909             return false;
910         }
911         $fullpath = "/$context->id/mod_lesson/$filearea/$pageid/".implode('/', $args);
912         $forcedownload = true; //TODO: this is strange (skodak)
913     } else {
915         $fullpath = "/$context->id/mod_lesson/$filearea/".implode('/', $args);
916     }
918     $fs = get_file_storage();
919     if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
920         return false;
921     }
923     // finally send the file
924     send_stored_file($file, 0, 0, $forcedownload); // download MUST be forced - security!
927 /**
928  * Returns an array of file areas
929  * @return array
930  */
931 function lesson_get_file_areas() {
932     $areas = array();
933     $areas['page_contents'] = 'page_contents'; //TODO: localize!!!!
934     $areas['media_files'] = 'media_files'; //TODO: localize!!!!
937 /**
938  * Returns a file_info_stored object for the file being requested here
939  *
940  * @global <type> $CFG
941  * @param file_browse $browser
942  * @param array $areas
943  * @param object $course
944  * @param object $cm
945  * @param object $context
946  * @param string $filearea
947  * @param int $itemid
948  * @param string $filepath
949  * @param string $filename
950  * @return file_info_stored
951  */
952 function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
953     global $CFG;
954     if (has_capability('moodle/course:managefiles', $context)) {
955         // no peaking here for students!!
956         return null;
957     }
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)) {
964         return null;
965     }
966     return new file_info_stored($browser, $context, $storedfile, $urlbase, $filearea, $itemid, true, true, false);
969 /**
970  * Abstract class to provide a core functions to the all lesson classes
971  *
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.
974  *
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.
978  *
979  * @copyright 2009 Sam Hemelryk
980  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
981  */
982 abstract class lesson_base {
984     /**
985      * An object containing properties
986      * @var stdClass
987      */
988     protected $properties;
990     /**
991      * The constructor
992      * @param stdClass $properties
993      */
994     public function __construct($properties) {
995         $this->properties = (object)$properties;
996     }
998     /**
999      * Magic property method
1000      *
1001      * Attempts to call a set_$key method if one exists otherwise falls back
1002      * to simply set the property
1003      *
1004      * @param string $key
1005      * @param mixed $value
1006      */
1007     public function __set($key, $value) {
1008         if (method_exists($this, 'set_'.$key)) {
1009             $this->{'set_'.$key}($value);
1010         }
1011         $this->properties->{$key} = $value;
1012     }
1014     /**
1015      * Magic get method
1016      *
1017      * Attempts to call a get_$key method to return the property and ralls over
1018      * to return the raw property
1019      *
1020      * @param str $key
1021      * @return mixed
1022      */
1023     public function __get($key) {
1024         if (method_exists($this, 'get_'.$key)) {
1025             return $this->{'get_'.$key}();
1026         }
1027         return $this->properties->{$key};
1028     }
1030     /**
1031      * Stupid PHP needs an isset magic method if you use the get magic method and
1032      * still want empty calls to work.... blah ~!
1033      *
1034      * @param string $key
1035      * @return bool
1036      */
1037     public function __isset($key) {
1038         if (method_exists($this, 'get_'.$key)) {
1039             $val = $this->{'get_'.$key}();
1040             return !empty($val);
1041         }
1042         return !empty($this->properties->{$key});
1043     }
1045     /**
1046      * If overriden should create a new instance, save it in the DB and return it
1047      */
1048     public static function create() {}
1049     /**
1050      * If overriden should load an instance from the DB and return it
1051      */
1052     public static function load() {}
1053     /**
1054      * Fetches all of the properties of the object
1055      * @return stdClass
1056      */
1057     public function properties() {
1058         return $this->properties;
1059     }
1062 /**
1063  * Class representation of a lesson
1064  *
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
1067  *
1068  * <code>
1069  * lesson::load($lessonid);
1070  * // or
1071  * $lessonrecord = $DB->get_record('lesson', $lessonid);
1072  * $lesson = new lesson($lessonrecord);
1073  * </code>
1074  *
1075  * The class itself extends lesson_base as all classes within the lesson module should
1076  *
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
1116  *
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)
1120  *
1121  * @copyright 2009 Sam Hemelryk
1122  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1123  */
1124 class lesson extends lesson_base {
1126     /**
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>
1129      * @var int
1130      */
1131     protected $firstpageid = null;
1132     /**
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>
1135      * @var int
1136      */
1137     protected $lastpageid = null;
1138     /**
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
1145      */
1146     protected $pages = array();
1147     /**
1148      * Flag that gets set to true once all of the pages associated with the lesson
1149      * have been loaded.
1150      * @var bool
1151      */
1152     protected $loadedallpages = false;
1154     /**
1155      * Simply generates a lesson object given an array/object of properties
1156      * Overrides {@see lesson_base->create()}
1157      * @static
1158      * @param object|array $properties
1159      * @return lesson
1160      */
1161     public static function create($properties) {
1162         return new lesson($properties);
1163     }
1165     /**
1166      * Generates a lesson object from the database given its id
1167      * @static
1168      * @param int $lessonid
1169      * @return lesson
1170      */
1171     public static function load($lessonid) {
1172         if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1173             print_error('invalidcoursemodule');
1174         }
1175         return new lesson($lesson);
1176     }
1178     /**
1179      * Deletes this lesson from the database
1180      */
1181     public function delete() {
1182         global $CFG, $DB;
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);
1196                 $event->delete();
1197             }
1198         }
1200         grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, NULL, array('deleted'=>1));
1201         return true;
1202     }
1204     /**
1205      * Fetches messages from the session that may have been set in previous page
1206      * actions.
1207      *
1208      * <code>
1209      * // Do not call this method directly instead use
1210      * $lesson->messages;
1211      * </code>
1212      *
1213      * @return array
1214      */
1215     protected function get_messages() {
1216         global $SESSION;
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]);
1222         }
1224         return $messages;
1225     }
1227     /**
1228      * Get all of the attempts for the current user.
1229      *
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
1235      */
1236     public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1237         global $USER, $DB;
1238         $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1239         if ($correct) {
1240             $params['correct'] = 1;
1241         }
1242         if ($pageid !== null) {
1243             $params['pageid'] = $pageid;
1244         }
1245         if ($userid === null) {
1246             $params['userid'] = $USER->id;
1247         }
1248         return $DB->get_records('lesson_attempts', $params, 'timeseen DESC');
1249     }
1251     /**
1252      * Returns the first page for the lesson or false if there isn't one.
1253      *
1254      * This method should be called via the magic method __get();
1255      * <code>
1256      * $firstpage = $lesson->firstpage;
1257      * </code>
1258      *
1259      * @return lesson_page|bool Returns the lesson_page specialised object or false
1260      */
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) {
1266                     return $page;
1267                 }
1268             }
1269         }
1270         return false;
1271     }
1273     /**
1274      * Returns the last page for the lesson or false if there isn't one.
1275      *
1276      * This method should be called via the magic method __get();
1277      * <code>
1278      * $lastpage = $lesson->lastpage;
1279      * </code>
1280      *
1281      * @return lesson_page|bool Returns the lesson_page specialised object or false
1282      */
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) {
1288                     return $page;
1289                 }
1290             }
1291         }
1292         return false;
1293     }
1295     /**
1296      * Returns the id of the first page of this lesson. (prevpageid = 0)
1297      * @return int
1298      */
1299     protected function get_firstpageid() {
1300         global $DB;
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');
1306                 }
1307                 $this->firstpageid = $firstpageid;
1308             } else {
1309                 $firstpage = $this->get_firstpage();
1310                 $this->firstpageid = $firstpage->id;
1311             }
1312         }
1313         return $this->firstpageid;
1314     }
1316     /**
1317      * Returns the id of the last page of this lesson. (nextpageid = 0)
1318      * @return int
1319      */
1320     public function get_lastpageid() {
1321         global $DB;
1322         if ($this->lastpageid == null) {
1323             if (!$this->loadedallpages) {
1324                 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1325                 if (!$lastpageid) {
1326                     print_error('cannotfindlastpage', 'lesson');
1327                 }
1328                 $this->lastpageid = $lastpageid;
1329             } else {
1330                 $lastpageid = $this->get_lastpage();
1331                 $this->lastpageid = $lastpageid->id;
1332             }
1333         }
1335         return $this->lastpageid;
1336     }
1338      /**
1339      * Gets the next page to display after the one that is provided.
1340      * @param int $nextpageid
1341      * @return bool
1342      */
1343     public function get_next_page($nextpageid) {
1344         global $USER;
1345         $allpages = $this->load_all_pages();
1346         if ($this->properties->nextpagedefault) {
1347             // in Flash Card mode...first get number of retakes
1348             shuffle($allpages);
1349             $found = false;
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))) {
1353                         $found = true;
1354                         break;
1355                     }
1356                 }
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))) {
1360                         $found = true;
1361                         break;
1362                     }
1363                 }
1364             }
1365             if ($found) {
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) {
1370                         return false;
1371                     }
1372                 }
1373                 return $nextpage;
1374             }
1375         }
1376         // In a normal lesson mode
1377         foreach ($allpages as $nextpage) {
1378             if ((int)$nextpage->id===(int)$nextpageid) {
1379                 return $nextpage;
1380             }
1381         }
1382         return false;
1383     }
1385     /**
1386      * Sets a message against the session for this lesson that will displayed next
1387      * time the lesson processes messages
1388      *
1389      * @param string $message
1390      * @param string $class
1391      * @param string $align
1392      * @return bool
1393      */
1394     public function add_message($message, $class="notifyproblem", $align='center') {
1395         global $SESSION;
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();
1402         }
1404         $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
1406         return true;
1407     }
1409     /**
1410      * Check if the lesson is accessible at the present time
1411      * @return bool True if the lesson is accessible, false otherwise
1412      */
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));
1417     }
1419     /**
1420      * Starts the lesson time for the current user
1421      * @return bool Returns true
1422      */
1423     public function start_timer() {
1424         global $USER, $DB;
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');
1434         }
1435         return true;
1436     }
1438     /**
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
1444      */
1445     public function update_timer($restart=false, $continue=false) {
1446         global $USER, $DB;
1447         // clock code
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');
1451         } else {
1452             $timer = current($timer); // this will get the latest start time record
1453         }
1455         if ($restart) {
1456             if ($continue) {
1457                 // continue a previous test, need to update the clock  (think this option is disabled atm)
1458                 $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
1459             } else {
1460                 // starting over, so reset the clock
1461                 $timer->starttime = time();
1462             }
1463         }
1465         $timer->lessontime = time();
1466         $DB->update_record('lesson_timer', $timer);
1467         return $timer;
1468     }
1470     /**
1471      * Updates the timer to the current time then stops it by unsetting the user var
1472      * @return bool Returns true
1473      */
1474     public function stop_timer() {
1475         global $USER, $DB;
1476         unset($USER->startlesson[$this->properties->id]);
1477         return $this->update_timer(false, false);
1478     }
1480     /**
1481      * Checks to see if the lesson has pages
1482      */
1483     public function has_pages() {
1484         global $DB;
1485         $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
1486         return ($pagecount>0);
1487     }
1489     /**
1490      * Returns the link for the related activity
1491      * @return array|false
1492      */
1493     public function link_for_activitylink() {
1494         global $DB;
1495         $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
1496         if ($module) {
1497             $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
1498             if ($modname) {
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'));
1504                 }
1505             }
1506         }
1507         return '';
1508     }
1510     /**
1511      * Loads the requested page.
1512      *
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
1517      *
1518      * @param int $pageid
1519      * @return lesson_page A lesson_page object or an object that extends it
1520      */
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);
1525         }
1526         return $this->pages[$pageid];
1527     }
1529     /**
1530      * Loads ALL of the pages for this lesson
1531      *
1532      * @return array An array containing all pages from this lesson
1533      */
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;
1539         }
1540         return $this->pages;
1541     }
1543     /**
1544      * Determins if a jumpto value is correct or not.
1545      *
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.
1548      *
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.
1552      **/
1553     public function jumpto_is_correct($pageid, $jumpto) {
1554         global $DB;
1556         // first test the special values
1557         if (!$jumpto) {
1558             // same page
1559             return false;
1560         } elseif ($jumpto == LESSON_NEXTPAGE) {
1561             return true;
1562         } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
1563             return true;
1564         } elseif ($jumpto == LESSON_RANDOMPAGE) {
1565             return true;
1566         } elseif ($jumpto == LESSON_CLUSTERJUMP) {
1567             return true;
1568         } elseif ($jumpto == LESSON_EOL) {
1569             return true;
1570         }
1572         $pages = $this->load_all_pages();
1573         $apageid = $pages[$pageid]->nextpageid;
1574         while ($apageid != 0) {
1575             if ($jumpto == $apageid) {
1576                 return true;
1577             }
1578             $apageid = $pages[$apageid]->nextpageid;
1579         }
1580         return false;
1581     }
1583     /**
1584      * Returns the time a user has remaining on this lesson
1585      * @param int $starttime Starttime timestamp
1586      * @return string
1587      */
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";
1597         }
1598         if ($secs < 10) {
1599             $secs = "0$secs";
1600         }
1601         $output   = array();
1602         $output[] = $hours;
1603         $output[] = $minutes;
1604         $output[] = $secs;
1605         $output = implode(':', $output);
1606         return $output;
1607     }
1609     /**
1610      * Interprets LESSON_CLUSTERJUMP jumpto value.
1611      *
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).
1617      *
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.
1621      **/
1622     public function cluster_jump($pageid, $userid=null) {
1623         global $DB, $USER;
1625         if ($userid===null) {
1626             $userid = $USER->id;
1627         }
1628         // get the number of retakes
1629         if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
1630             $retakes = 0;
1631         }
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;
1637             }
1639         }
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) {
1646                 break;
1647             }
1648             $pageid = $lessonpages[$pageid]->prevpageid;
1649         }
1651         $clusterpages = array();
1652         $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
1653         $unseen = array();
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;
1659             }
1660         }
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;
1671             }
1672         } else {
1673             // seen all there is to see, leave the cluster
1674             if (end($clusterpages)->nextpageid == 0) {
1675                 return LESSON_EOL;
1676             } else {
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) {
1680                         break;
1681                     }
1682                     $clusterendid = $lessonpages[$clusterendid]->prevpageid;
1683                 }
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;
1687                 }
1688                 if ($exitjump == 0) {
1689                     return LESSON_EOL;
1690                 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
1691                     return $exitjump;
1692                 } else {
1693                     if (!array_key_exists($exitjump, $lessonpages)) {
1694                         $found = false;
1695                         foreach ($lessonpages as $page) {
1696                             if ($page->id === $clusterendid) {
1697                                 $found = true;
1698                             } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1699                                 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
1700                                 break;
1701                             }
1702                         }
1703                     }
1704                     if (!array_key_exists($exitjump, $lessonpages)) {
1705                         return LESSON_EOL;
1706                     }
1707                     return $exitjump;
1708                 }
1709             }
1710         }
1711     }
1713     /**
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
1716      *
1717      * @param int $pageid
1718      * @param array $ends An array of LESSON_PAGE_* types that signify an end of
1719      *               the subtype
1720      * @return array An array of specialised lesson_page objects
1721      */
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
1725         $pages = array();
1727         while (true) {
1728             if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
1729                 break;
1730             }
1731             $pages[] = $lessonpages[$pageid];
1732             $pageid = $lessonpages[$pageid]->nextpageid;
1733         }
1735         return $pages;
1736     }
1738     /**
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
1742      *
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
1746      * @return bool
1747      */
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
1754         while (true) {
1755             if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
1756                 return false;
1757             } else if (in_array($pages[$pageid]->qtype, $types)) {
1758                 return true;
1759             }
1760             $pageid = $pages[$pageid]->prevpageid;
1761         }
1762     }
1765 /**
1766  * Abstract class representation of a page associated with a lesson.
1767  *
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
1773  *
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
1788  *
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
1797  *
1798  * @abstract
1799  * @copyright 2009 Sam Hemelryk
1800  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1801  */
1802 abstract class lesson_page extends lesson_base {
1804     /**
1805      * A reference to the lesson this page belongs to
1806      * @var lesson
1807      */
1808     protected $lesson = null;
1809     /**
1810      * Contains the answers to this lesson_page once loaded
1811      * @var null|array
1812      */
1813     protected $answers = null;
1814     /**
1815      * This sets the type of the page, can be one of the constants defined below
1816      * @var int
1817      */
1818     protected $type = 0;
1820     /**
1821      * Constants used to identify the type of the page
1822      */
1823     const TYPE_QUESTION = 0;
1824     const TYPE_STRUCTURE = 1;
1826     /**
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
1829      * @abstract
1830      * @return int
1831      */
1832     abstract protected function get_typeid();
1833     /**
1834      * This method should return the string that describes the pagetype
1835      * @abstract
1836      * @return string
1837      */
1838     abstract protected function get_typestring();
1840     /**
1841      * This method gets called to display the page to the user taking the lesson
1842      * @abstract
1843      * @param object $renderer
1844      * @param object $attempt
1845      * @return string
1846      */
1847     abstract public function display($renderer, $attempt);
1849     /**
1850      * Creates a new lesson_page within the database and returns the correct pagetype
1851      * object to use to interact with the new lesson
1852      *
1853      * @final
1854      * @static
1855      * @param object $properties
1856      * @param lesson $lesson
1857      * @return lesson_page Specialised object that extends lesson_page
1858      */
1859     final public static function create($properties, lesson $lesson, $context, $maxbytes) {
1860         global $DB;
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');
1876             if (!$prevpage) {
1877                 print_error('cannotfindpages', 'lesson');
1878             }
1879             $newpage->prevpageid = $prevpage->id;
1880             $newpage->nextpageid = $prevpage->nextpageid;
1881         } else {
1882             $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
1883             if ($nextpage) {
1884                 // This is the first page, there are existing pages put this at the start
1885                 $newpage->nextpageid = $nextpage->id;
1886             }
1887         }
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));
1899         }
1900         if ($newpage->nextpageid > 0) {
1901             $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
1902         }
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');
1909         return $page;
1910     }
1912     /**
1913      * This method loads a page object from the database and returns it as a
1914      * specialised object that extends lesson_page
1915      *
1916      * @final
1917      * @static
1918      * @param int $id
1919      * @param lesson $lesson
1920      * @return lesson_page Specialised lesson_page object
1921      */
1922     final public static function load($id, lesson $lesson) {
1923         global $DB;
1925         if (is_object($id) && !empty($id->qtype)) {
1926             $page = $id;
1927         } else {
1928             $page = $DB->get_record("lesson_pages", array("id" => $id));
1929             if (!$page) {
1930                 print_error('cannotfindpages', 'lesson');
1931             }
1932         }
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';
1938         }
1940         return new $class($page, $lesson);
1941     }
1943     /**
1944      * Deletes a lesson_page from the database as well as any associated records.
1945      * @final
1946      * @return bool
1947      */
1948     final public function delete() {
1949         global $DB;
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);
1967             $page->move(0);
1968         } else {
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);
1975         }
1976         return true;
1977     }
1979     /**
1980      * Moves a page by updating its nextpageid and prevpageid values within
1981      * the database
1982      *
1983      * @final
1984      * @param int $nextpageid
1985      * @param int $prevpageid
1986      */
1987     final public function move($nextpageid=null, $prevpageid=null) {
1988         global $DB;
1989         if ($nextpageid === null) {
1990             $nextpageid = $this->properties->nextpageid;
1991         }
1992         if ($prevpageid === null) {
1993             $prevpageid = $this->properties->prevpageid;
1994         }
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);
2000     }
2002     /**
2003      * Returns the answers that are associated with this page in the database
2004      *
2005      * @final
2006      * @return array
2007      */
2008     final public function get_answers() {
2009         global $DB;
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');
2013             if (!$answers) {
2014                 debugging(get_string('cannotfindanswer', 'lesson'));
2015                 return array();
2016             }
2017             foreach ($answers as $answer) {
2018                 $this->answers[count($this->answers)] = new lesson_page_answer($answer);
2019             }
2020         }
2021         return $this->answers;
2022     }
2024     /**
2025      * Returns the lesson this page is associated with
2026      * @final
2027      * @return lesson
2028      */
2029     final protected function get_lesson() {
2030         return $this->lesson;
2031     }
2033     /**
2034      * Returns the type of page this is. Not to be confused with page type
2035      * @final
2036      * @return int
2037      */
2038     final protected function get_type() {
2039         return $this->type;
2040     }
2042     /**
2043      * Records an attempt at this page
2044      *
2045      * @final
2046      * @param stdClass $context
2047      * @return stdClass Returns the result of the attempt
2048      */
2049     final public function record_attempt($context) {
2050         global $DB, $USER, $OUTPUT;
2052         /**
2053          * This should be overriden by each page type to actually check the response
2054          * against what ever custom criteria they have defined
2055          */
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');
2064         } else {
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;
2077                 }
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
2083                 }
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;
2096                         }
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;
2100                     }
2101                 }
2102             }
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;
2110                 } else {
2111                     $result->newpageid = $nextpage->id;
2112                 }
2113             }
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');
2129                 } else {
2130                     $result->response = get_string('thatsthewronganswer', 'lesson');
2131                 }
2132             }
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');
2140                     } else {
2141                         $result->feedback = $OUTPUT->BOX(get_string("secondpluswrong", "lesson"), 'feedback');
2142                     }
2143                 } else {
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)
2149                     }
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
2156                     echo "</div>";
2157                 }
2158             }
2159         }
2161         return $result;
2162     }
2164     /**
2165      * Returns the string for a jump name
2166      *
2167      * @final
2168      * @param int $jumpto Jump code or page ID
2169      * @return string
2170      **/
2171     final protected function get_jump_name($jumpto) {
2172         global $DB;
2173         static $jumpnames = array();
2175         if (!array_key_exists($jumpto, $jumpnames)) {
2176             if ($jumpto == 0) {
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');
2192             } else {
2193                 if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
2194                     $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
2195                 }
2196             }
2197             $jumpnames[$jumpto] = format_string($jumptitle,true);
2198         }
2200         return $jumpnames[$jumpto];
2201     }
2203     /**
2204      * Construstor method
2205      * @param object $properties
2206      * @param lesson $lesson
2207      */
2208     public function __construct($properties, lesson $lesson) {
2209         parent::__construct($properties);
2210         $this->lesson = $lesson;
2211     }
2213     /**
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
2218      * @return int
2219      */
2220     public function earned_score($answers, $attempt) {
2221         return $answers[$attempt->answerid]->score;
2222     }
2224     /**
2225      * This is a callback method that can be override and gets called when ever a page
2226      * is viewed
2227      *
2228      * @param bool $canmanage True if the user has the manage cap
2229      * @return mixed
2230      */
2231     public function callback_on_view($canmanage) {
2232         return true;
2233     }
2235     /**
2236      * Updates a lesson page and its answers within the database
2237      *
2238      * @param object $properties
2239      * @return bool
2240      */
2241     public function update($properties, $context = null, $maxbytes = null) {
2242         global $DB;
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';
2248         }
2249         if (empty($context)) {
2250             $context = $PAGE->context;
2251         }
2252         if ($maxbytes === null) {
2253             $maxbytes =get_max_upload_file_size();
2254         }
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;
2264             }
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'];
2271                 }
2272                 if (isset($properties->jumpto[$i])) {
2273                     $this->answers[$i]->jumpto = $properties->jumpto[$i];
2274                 }
2275                 if ($this->lesson->custom && isset($properties->score[$i])) {
2276                     $this->answers[$i]->score = $properties->score[$i];
2277                 }
2278                 if (!isset($this->answers[$i]->id)) {
2279                     $this->answers[$i]->id =  $DB->insert_record("lesson_answers", $this->answers[$i]);
2280                 } else {
2281                     $DB->update_record("lesson_answers", $this->answers[$i]->properties());
2282                 }
2284             } else {
2285                 break;
2286             }
2287         }
2288         return true;
2289     }
2291     /**
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
2295      * @return bool
2296      */
2297     public function add_page_link($previd) {
2298         return false;
2299     }
2301     /**
2302      * Returns true if a page has been viewed before
2303      *
2304      * @param array|int $param Either an array of pages that have been seen or the
2305      *                   number of retakes a user has had
2306      * @return bool
2307      */
2308     public function is_unseen($param) {
2309         global $USER, $DB;
2310         if (is_array($param)) {
2311             $seenpages = $param;
2312             return (!array_key_exists($this->properties->id, $seenpages));
2313         } else {
2314             $nretakes = $param;
2315             if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
2316                 return true;
2317             }
2318         }
2319         return false;
2320     }
2322     /**
2323      * Checks to see if a page has been answered previously
2324      * @param int $nretakes
2325      * @return bool
2326      */
2327     public function is_unanswered($nretakes) {
2328         global $DB, $USER;
2329         if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
2330             return true;
2331         }
2332         return false;
2333     }
2335     /**
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
2339      * @return array
2340      */
2341     public function create_answers($properties) {
2342         global $DB;
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;
2349         $answers = array();
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'];
2359                 }
2360                 if (isset($properties->jumpto[$i])) {
2361                     $answer->jumpto = $properties->jumpto[$i];
2362                 }
2363                 if ($this->lesson->custom && isset($properties->score[$i])) {
2364                     $answer->score = $properties->score[$i];
2365                 }
2366                 $answer->id = $DB->insert_record("lesson_answers", $answer);
2367                 $answers[$answer->id] = new lesson_page_answer($answer);
2368             } else {
2369                 break;
2370             }
2371         }
2373         $this->answers = $answers;
2374         return $answers;
2375     }
2377     /**
2378      * This method MUST be overriden by all question page types, or page types that
2379      * wish to score a page.
2380      *
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
2383      * <code>
2384      * $result = parent::check_answer();
2385      * </code>
2386      * before modifiying it as required.
2387      *
2388      * @return stdClass
2389      */
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
2402         return $result;
2403     }
2405     /**
2406      * True if the page uses a custom option
2407      *
2408      * Should be override and set to true if the page uses a custom option.
2409      *
2410      * @return bool
2411      */
2412     public function has_option() {
2413         return false;
2414     }
2416     /**
2417      * Returns the maximum number of answers for this page given the maximum number
2418      * of answers permitted by the lesson.
2419      *
2420      * @param int $default
2421      * @return int
2422      */
2423     public function max_answers($default) {
2424         return $default;
2425     }
2427     /**
2428      * Returns the properties of this lesson page as an object
2429      * @return stdClass;
2430      */
2431     public function properties() {
2432         $properties = clone($this->properties);
2433         if ($this->answers === null) {
2434             $this->get_answers();
2435         }
2436         if (count($this->answers)>0) {
2437             $count = 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;
2443                 $count++;
2444             }
2445         }
2446         return $properties;
2447     }
2449     /**
2450      * Returns an array of options to display whn choosing the jumpto for a page/answer
2451      * @static
2452      * @param int $pageid
2453      * @param lesson $lesson
2454      * @return array
2455      */
2456     public static function get_jumptooptions($pageid, lesson $lesson) {
2457         global $DB;
2458         $jump = array();
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");
2464         if ($pageid == 0) {
2465             return $jump;
2466         }
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");
2472         }
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");
2475         }
2476         if (!optional_param('firstpage', 0, PARAM_INT)) {
2477             $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
2478             while (true) {
2479                 if ($apageid) {
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));
2483                 } else {
2484                     // last page reached
2485                     break;
2486                 }
2487             }
2488         }
2489         return $jump;
2490     }
2491     /**
2492      * Returns the contents field for the page properly formatted and with plugin
2493      * file url's converted
2494      * @return string
2495      */
2496     public function get_contents() {
2497         global $PAGE;
2498         if (!empty($this->properties->contents)) {
2499             if (!isset($this->properties->contentsformat)) {
2500                 $this->properties->contentsformat = FORMAT_HTML;
2501             }
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);
2504         } else {
2505             return '';
2506         }
2507     }
2509     /**
2510      * Set to true if this page should display in the menu block
2511      * @return bool
2512      */
2513     protected function get_displayinmenublock() {
2514         return false;
2515     }
2517     /**
2518      * Get the string that describes the options of this page type
2519      * @return string
2520      */
2521     public function option_description_string() {
2522         return '';
2523     }
2525     /**
2526      * Updates a table with the answers for this page
2527      * @param html_table $table
2528      * @return html_table
2529      */
2530     public function display_answers(html_table $table) {
2531         $answers = $this->get_answers();
2532         $i = 1;
2533         foreach ($answers as $answer) {
2534             $cells = array();
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);
2538             if ($i === 1){
2539                 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
2540             }
2541             $i++;
2542         }
2543         return $table;
2544     }
2546     /**
2547      * Determines if this page should be grayed out on the management/report screens
2548      * @return int 0 or 1
2549      */
2550     protected function get_grayout() {
2551         return 0;
2552     }
2554     /**
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
2558      * @param int $tries
2559      * @return bool
2560      */
2561     public function stats(array &$pagestats, $tries) {
2562         return true;
2563     }
2565     /**
2566      * Formats the answers of this page for a report
2567      *
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
2575      */
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;
2584         }
2585         return $answerpage;
2586     }
2588     /**
2589      * Gets an array of the jumps used by the answers of this page
2590      *
2591      * @return array
2592      */
2593     public function get_jumps() {
2594         global $DB;
2595         $jumps = array();
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);
2600             }
2601         }
2602         return $jumps;
2603     }
2604     /**
2605      * Informs whether this page type require manual grading or not
2606      * @return bool
2607      */
2608     public function requires_manual_grading() {
2609         return false;
2610     }
2612     /**
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.
2615      * @return false|int
2616      */
2617     public function override_next_page() {
2618         return false;
2619     }
2621     /**
2622      * This method is used to determine if this page is a valid page
2623      *
2624      * @param array $validpages
2625      * @param array $pageviews
2626      * @return int The next page id to check
2627      */
2628     public function valid_page_and_view(&$validpages, &$pageviews) {
2629         $validpages[$this->properties->id] = 1;
2630         return $this->properties->nextpageid;
2631     }
2634 /**
2635  * Class used to represent an answer to a page
2636  *
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
2648  *
2649  * @copyright 2009 Sam Hemelryk
2650  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2651  */
2652 class lesson_page_answer extends lesson_base {
2654     /**
2655      * Loads an page answer from the DB
2656      *
2657      * @param int $id
2658      * @return lesson_page_answer
2659      */
2660     public static function load($id) {
2661         global $DB;
2662         $answer = $DB->get_record("lesson_answers", array("id" => $id));
2663         return new lesson_page_answer($answer);
2664     }
2666     /**
2667      * Given an object of properties and a page created answer(s) and saves them
2668      * in the database.
2669      *
2670      * @param stdClass $properties
2671      * @param lesson_page $page
2672      * @return array
2673      */
2674     public static function create($properties, lesson_page $page) {
2675         return $page->create_answers($properties);
2676     }
2680 /**
2681  * A management class for page types
2682  *
2683  * This class is responsible for managing the different pages. A manager object can
2684  * be retrieved by calling the following line of code:
2685  * <code>
2686  * $manager  = lesson_page_type_manager::get($lesson);
2687  * </code>
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.
2690  *
2691  * @copyright 2009 Sam Hemelryk
2692  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2693  */
2694 class lesson_page_type_manager {
2696     /**
2697      * An array of different page type classes
2698      * @var array
2699      */
2700     protected $types = array();
2702     /**
2703      * Retrieves the lesson page type manager object
2704      *
2705      * If the object hasn't yet been created it is created here.
2706      *
2707      * @staticvar lesson_page_type_manager $pagetypemanager
2708      * @param lesson $lesson
2709      * @return lesson_page_type_manager
2710      */
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);
2716         }
2717         return $pagetypemanager;
2718     }
2720     /**
2721      * Finds and loads all lesson page types in mod/lesson/pagetypes
2722      *
2723      * @param lesson $lesson
2724      */
2725     public function load_lesson_types(lesson $lesson) {
2726         global $CFG;
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)) {
2731                 continue;
2732             }
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;
2738             }
2739         }
2741     }
2743     /**
2744      * Returns an array of strings to describe the loaded page types
2745      *
2746      * @param int $type Can be used to return JUST the string for the requested type
2747      * @return array
2748      */
2749     public function get_page_type_strings($type=null, $special=true) {
2750         $types = array();
2751         foreach ($this->types as $pagetype) {
2752             if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
2753                 $types[$pagetype->typeid] = $pagetype->typestring;
2754             }
2755         }
2756         return $types;
2757     }
2759     /**
2760      * Returns the basic string used to identify a page type provided with an id
2761      *
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
2764      *
2765      * @param int $id
2766      * @return string
2767      */
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;
2772             }
2773         }
2774         return 'unknown';
2775     }
2777     /**
2778      * Loads a page for the provided lesson given it's id
2779      *
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
2783      *
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
2787      */
2788     public function load_page($pageid, lesson $lesson) {
2789         global $DB;
2790         if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
2791             print_error('cannotfindpages', 'lesson');
2792         }
2793         $pagetype = get_class($this->types[$page->qtype]);
2794         $page = new $pagetype($page, $lesson);
2795         return $page;
2796     }
2798     /**
2799      * This function loads ALL pages that belong to the lesson.
2800      *
2801      * @param lesson $lesson
2802      * @return array An array of lesson_page_type_*
2803      */
2804     public function load_all_pages(lesson $lesson) {
2805         global $DB;
2806         if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
2807             print_error('cannotfindpages', 'lesson');
2808         }
2809         foreach ($pages as $key=>$page) {
2810             $pagetype = get_class($this->types[$page->qtype]);
2811             $pages[$key] = new $pagetype($page, $lesson);
2812         }
2814         $orderedpages = array();
2815         $lastpageid = 0;
2817         while (true) {
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) {
2824                         break 2;
2825                     } else {
2826                         break 1;
2827                     }
2828                 }
2829             }
2830         }
2832         return $orderedpages;
2833     }
2835     /**
2836      * Fetchs an mform that can be used to create/edit an page
2837      *
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
2841      */
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';
2849         }
2850         return new $class(null, $arguments);
2851     }
2853     /**
2854      * Returns an array of links to use as add page links
2855      * @param int $previd The id of the previous page
2856      * @return array
2857      */
2858     public function get_add_page_type_links($previd) {
2859         global $OUTPUT;
2861         $links = array();
2863         foreach ($this->types as $key=>$type) {
2864             if ($link = $type->add_page_link($previd)) {
2865                 $links[$key] = $link;
2866             }
2867         }
2869         return $links;
2870     }
2873 /**
2874  * Abstract class that page type's MUST inherit from.
2875  *
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
2880  *
2881  * @abstract
2882  * @copyright 2009 Sam Hemelryk
2883  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2884  */
2885 abstract class lesson_add_page_form_base extends moodleform {
2887     /**
2888      * This is the classic define that is used to identify this pagetype.
2889      * Will be one of LESSON_*
2890      * @var int
2891      */
2892     public $qtype;
2894     /**
2895      * The simple string that describes the page type e.g. truefalse, multichoice
2896      * @var string
2897      */
2898     public $qtypestring;
2900     /**
2901      * An array of options used in the htmleditor
2902      * @var array
2903      */
2904     protected $editoroptions = array();
2906     /**
2907      * True if this is a standard page of false if it does something special.
2908      * Questions are standard pages, branch tables are not
2909      * @var bool
2910      */
2911     protected $standard = true;
2913     /**
2914      * Each page type can and should override this to add any custom elements to
2915      * the basic form that they want
2916      */
2917     public function custom_definition() {}
2919     /**
2920      * Used to determine if this is a standard page or a special page
2921      * @return bool
2922      */
2923     public final function is_standard() {
2924         return (bool)$this->standard;
2925     }
2927     /**
2928      * Add the required basic elements to the form.
2929      *
2930      * This method adds the basic elements to the form including title and contents
2931      * and then calls custom_definition();
2932      */
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);
2954         }
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"));
2961         } else {
2962             $this->add_action_buttons(get_string('cancel'), get_string("addaquestionpage", "lesson"));
2963         }
2964     }
2966     /**
2967      * Convenience function: Adds a jumpto select element
2968      *
2969      * @param string $name
2970      * @param string|null $label
2971      * @param int $selected The page to select by default
2972      */
2973     protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
2974         $title = get_string("jump", "lesson");
2975         if ($label === null) {
2976             $label = $title;
2977         }
2978         if (is_int($name)) {
2979             $name = "jumpto[$name]";
2980         }
2981         $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
2982         $this->_form->setDefault($name, $selected);
2983         $this->_form->addHelpButton($name, 'jumps', 'lesson');
2984     }
2986     /**
2987      * Convenience function: Adds a score input element
2988      *
2989      * @param string $name
2990      * @param string|null $label
2991      * @param mixed $value The default value
2992      */
2993     protected final function add_score($name, $label=null, $value=null) {
2994         if ($label === null) {
2995             $label = get_string("score", "lesson");
2996         }
2997         if (is_int($name)) {
2998             $name = "score[$name]";
2999         }
3000         $this->_form->addElement('text', $name, $label, array('size'=>5));
3001         if ($value !== null) {
3002             $this->_form->setDefault($name, $value);
3003         }
3004     }
3006     /**
3007      * Convenience function: Adds an answer editor
3008      *
3009      * @param int $count The count of the element to add
3010      */
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));
3014     }
3015     /**
3016      * Convenience function: Adds an response editor
3017      *
3018      * @param int $count The count of the element to add
3019      */
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));
3023     }
3025     /**
3026      * A function that gets called upon init of this object by the calling script.
3027      *
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.
3030      *
3031      * @return bool
3032      */
3033     public function construction_override() {
3034         return true;
3035     }