MDL-21400 finalising JS api - removing ->on_dom_ready (now bool param in js() and...
[moodle.git] / mod / lesson / lib.php
CommitLineData
8cc86111 1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
5491947a 18/**
19 * Standard library of functions and constants for lesson
20 *
0a4abb73 21 * @package lesson
8cc86111 22 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5491947a 24 **/
97c44107 25
0a4abb73 26/** Include required libraries */
3b120e46 27require_once($CFG->libdir.'/eventslib.php');
0a4abb73
SH
28require_once($CFG->libdir.'/filelib.php');
29require_once($CFG->dirroot.'/calendar/lib.php');
30require_once($CFG->dirroot.'/course/moodleform_mod.php');
3b120e46 31
8cc86111 32/** LESSON_MAX_EVENT_LENGTH = 432000 ; 5 days maximum */
fe75e76b 33define("LESSON_MAX_EVENT_LENGTH", "432000");
97c44107 34
acf85537 35/**
36 * Given an object containing all the necessary data,
37 * (defined by the form in mod_form.php) this function
38 * will create a new instance and return the id number
39 * of the new instance.
40 *
8cc86111 41 * @global object
42 * @global object
acf85537 43 * @param object $lesson Lesson post data from the form
44 * @return int
45 **/
0a4abb73 46function lesson_add_instance($data, $mform) {
c18269c7 47 global $SESSION, $DB;
bbcbc0fe 48
0a4abb73 49 $cmid = $data->coursemodule;
5f649aaa 50
0a4abb73 51 lesson_process_pre_save($data);
271fea97 52
0a4abb73
SH
53 unset($data->mediafile);
54 $lessonid = $DB->insert_record("lesson", $data);
55 $data->id = $lessonid;
97c44107 56
0a4abb73
SH
57 $context = get_context_instance(CONTEXT_MODULE, $cmid);
58 $lesson = $DB->get_record('lesson', array('id'=>$lessonid), '*', MUST_EXIST);
59
60 if ($filename = $mform->get_new_filename('mediafile')) {
61 if ($file = $mform->save_stored_file('mediafile', $context->id, 'lesson_media_file', $lesson->id, '/', $filename)) {
62 $DB->set_field('lesson', 'mediafile', $file->get_filename(), array('id'=>$lesson->id));
63 }
64 }
65
66 lesson_process_post_save($data);
67
68 lesson_grade_item_update($data);
92bcca38 69
97c44107 70 return $lesson->id;
bbcbc0fe 71}
72
acf85537 73/**
74 * Given an object containing all the necessary data,
75 * (defined by the form in mod_form.php) this function
76 * will update an existing instance with new data.
77 *
8cc86111 78 * @global object
acf85537 79 * @param object $lesson Lesson post data from the form
80 * @return boolean
81 **/
0a4abb73 82function lesson_update_instance($data, $mform) {
c18269c7 83 global $DB;
bbcbc0fe 84
0a4abb73
SH
85 $data->id = $data->instance;
86 $cmid = $data->coursemodule;
1535c81b 87
0a4abb73 88 lesson_process_pre_save($data);
97c44107 89
0a4abb73
SH
90 unset($data->mediafile);
91 if (!$result = $DB->update_record("lesson", $data)) {
acf85537 92 return false; // Awe man!
97c44107 93 }
94
0a4abb73
SH
95 $context = get_context_instance(CONTEXT_MODULE, $cmid);
96 if ($filename = $mform->get_new_filename('mediafile')) {
97 if ($file = $mform->save_stored_file('mediafile', $context->id, 'lesson_media_file', $data->id, '/', $filename, true)) {
98 $DB->set_field('lesson', 'mediafile', $file->get_filename(), array('id'=>$data->id));
99 }
100 }
101
102 lesson_process_post_save($data);
92bcca38 103
104 // update grade item definition
0a4abb73 105 lesson_grade_item_update($data);
92bcca38 106
107 // update grades - TODO: do it only when grading style changes
0a4abb73 108 lesson_update_grades($data, 0, false);
92bcca38 109
110 return $result;
bbcbc0fe 111}
112
113
8cc86111 114/**
115 * Given an ID of an instance of this module,
116 * this function will permanently delete the instance
117 * and any data that depends on it.
118 *
119 * @global object
120 * @param int $id
121 * @return bool
122 */
bbcbc0fe 123function lesson_delete_instance($id) {
c18269c7 124 global $DB;
0a4abb73
SH
125 $lesson = $DB->get_record("lesson", array("id"=>$id), '*', MUST_EXIST);
126 $lesson = new lesson($lesson);
127 return $lesson->delete();
bbcbc0fe 128}
129
eee85098 130/**
131 * Given a course object, this function will clean up anything that
0a4abb73 132 * would be leftover after all the instances were deleted
eee85098 133 *
8cc86111 134 * @global object
eee85098 135 * @param object $course an object representing the course that is being deleted
136 * @param boolean $feedback to specify if the process must output a summary of its work
137 * @return boolean
138 */
139function lesson_delete_course($course, $feedback=true) {
eee85098 140 return true;
141}
142
8cc86111 143/**
144 * Return a small object with summary information about what a
145 * user has done with a given particular instance of this module
146 * Used for user activity reports.
147 * $return->time = the time they did it
148 * $return->info = a short text description
149 *
150 * @global object
151 * @param object $course
152 * @param object $user
153 * @param object $mod
154 * @param object $lesson
155 * @return object
156 */
bbcbc0fe 157function lesson_user_outline($course, $user, $mod, $lesson) {
646fc290 158 global $DB;
fe75e76b 159
1a96363a
NC
160 global $CFG;
161 require_once("$CFG->libdir/gradelib.php");
162 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
163
164 if (empty($grades->items[0]->grades)) {
165 $return->info = get_string("no")." ".get_string("attempts", "lesson");
bbcbc0fe 166 } else {
1a96363a
NC
167 $grade = reset($grades->items[0]->grades);
168 $return->info = get_string("grade") . ': ' . $grade->str_long_grade;
169 $return->time = $grade->dategraded;
bbcbc0fe 170 $return->info = get_string("no")." ".get_string("attempts", "lesson");
171 }
172 return $return;
173}
174
8cc86111 175/**
176 * Print a detailed representation of what a user has done with
177 * a given particular instance of this module, for user activity reports.
178 *
179 * @global object
180 * @param object $course
181 * @param object $user
182 * @param object $mod
183 * @param object $lesson
184 * @return bool
185 */
bbcbc0fe 186function lesson_user_complete($course, $user, $mod, $lesson) {
1a96363a
NC
187 global $DB, $OUTPUT, $CFG;
188
189 require_once("$CFG->libdir/gradelib.php");
190
191 $grades = grade_get_grades($course->id, 'mod', 'lesson', $lesson->id, $user->id);
192 if (!empty($grades->items[0]->grades)) {
193 $grade = reset($grades->items[0]->grades);
194 echo $OUTPUT->container(get_string('grade').': '.$grade->str_long_grade);
195 if ($grade->str_feedback) {
196 echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
197 }
198 }
fe75e76b 199
646fc290 200 $params = array ("lessonid" => $lesson->id, "userid" => $user->id);
201 if ($attempts = $DB->get_records_select("lesson_attempts", "lessonid = :lessonid AND userid = :userid", $params,
bbcbc0fe 202 "retry, timeseen")) {
d68ccdba 203 echo $OUTPUT->box_start();
3ab269ed 204 $table = new html_table();
ac8e16be 205 $table->head = array (get_string("attempt", "lesson"), get_string("numberofpagesviewed", "lesson"),
206 get_string("numberofcorrectanswers", "lesson"), get_string("time"));
207 $table->width = "100%";
208 $table->align = array ("center", "center", "center", "center");
209 $table->size = array ("*", "*", "*", "*");
210 $table->cellpadding = 2;
211 $table->cellspacing = 0;
bbcbc0fe 212
213 $retry = 0;
214 $npages = 0;
215 $ncorrect = 0;
5f649aaa 216
ac8e16be 217 foreach ($attempts as $attempt) {
218 if ($attempt->retry == $retry) {
219 $npages++;
bbcbc0fe 220 if ($attempt->correct) {
221 $ncorrect++;
222 }
223 $timeseen = $attempt->timeseen;
224 } else {
ac8e16be 225 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
bbcbc0fe 226 $retry++;
227 $npages = 1;
228 if ($attempt->correct) {
229 $ncorrect = 1;
230 } else {
231 $ncorrect = 0;
232 }
ac8e16be 233 }
234 }
bbcbc0fe 235 if ($npages) {
ac8e16be 236 $table->data[] = array($retry + 1, $npages, $ncorrect, userdate($timeseen));
bbcbc0fe 237 }
3ab269ed 238 echo $OUTPUT->table($table);
d68ccdba 239 echo $OUTPUT->box_end();
bbcbc0fe 240 }
241
bbcbc0fe 242 return true;
243}
244
4342dc32 245/**
246 * Prints lesson summaries on MyMoodle Page
247 *
248 * Prints lesson name, due date and attempt information on
249 * lessons that have a deadline that has not already passed
250 * and it is available for taking.
251 *
8cc86111 252 * @global object
253 * @global stdClass
254 * @global object
255 * @uses CONTEXT_MODULE
4342dc32 256 * @param array $courses An array of course objects to get lesson instances from
257 * @param array $htmlarray Store overview output array( course ID => 'lesson' => HTML output )
8cc86111 258 * @return void
4342dc32 259 */
260function lesson_print_overview($courses, &$htmlarray) {
fe75e76b 261 global $USER, $CFG, $DB, $OUTPUT;
4342dc32 262
263 if (!$lessons = get_all_instances_in_courses('lesson', $courses)) {
264 return;
265 }
266
267/// Get Necessary Strings
268 $strlesson = get_string('modulename', 'lesson');
269 $strnotattempted = get_string('nolessonattempts', 'lesson');
270 $strattempted = get_string('lessonattempted', 'lesson');
271
272 $now = time();
273 foreach ($lessons as $lesson) {
274 if ($lesson->deadline != 0 // The lesson has a deadline
275 and $lesson->deadline >= $now // And it is before the deadline has been met
276 and ($lesson->available == 0 or $lesson->available <= $now)) { // And the lesson is available
277
278 // Lesson name
279 if (!$lesson->visible) {
280 $class = ' class="dimmed"';
281 } else {
282 $class = '';
283 }
fe75e76b 284 $str = $OUTPUT->box("$strlesson: <a$class href=\"$CFG->wwwroot/mod/lesson/view.php?id=$lesson->coursemodule\">".
285 format_string($lesson->name).'</a>', 'name');
4342dc32 286
287 // Deadline
fe75e76b 288 $str .= $OUTPUT->box(get_string('lessoncloseson', 'lesson', userdate($lesson->deadline)), 'info');
4342dc32 289
290 // Attempt information
291 if (has_capability('mod/lesson:manage', get_context_instance(CONTEXT_MODULE, $lesson->coursemodule))) {
292 // Number of user attempts
318e3745 293 $attempts = $DB->count_records('lesson_attempts', array('lessonid'=>$lesson->id));
fe75e76b 294 $str .= $OUTPUT->box(get_string('xattempts', 'lesson', $attempts), 'info');
4342dc32 295 } else {
296 // Determine if the user has attempted the lesson or not
318e3745 297 if ($DB->count_records('lesson_attempts', array('lessonid'=>$lesson->id, 'userid'=>$USER->id))) {
fe75e76b 298 $str .= $OUTPUT->box($strattempted, 'info');
4342dc32 299 } else {
fe75e76b 300 $str .= $OUTPUT->box($strnotattempted, 'info');
4342dc32 301 }
302 }
fe75e76b 303 $str = $OUTPUT->box($str, 'lesson overview');
4342dc32 304
305 if (empty($htmlarray[$lesson->course]['lesson'])) {
306 $htmlarray[$lesson->course]['lesson'] = $str;
307 } else {
308 $htmlarray[$lesson->course]['lesson'] .= $str;
309 }
310 }
311 }
312}
313
8cc86111 314/**
315 * Function to be run periodically according to the moodle cron
316 * This function searches for things that need to be done, such
317 * as sending out mail, toggling flags etc ...
318 * @global stdClass
319 * @return bool true
320 */
bbcbc0fe 321function lesson_cron () {
bbcbc0fe 322 global $CFG;
323
324 return true;
325}
326
92bcca38 327/**
328 * Return grade for given user or all users.
329 *
8cc86111 330 * @global stdClass
331 * @global object
92bcca38 332 * @param int $lessonid id of lesson
333 * @param int $userid optional user id, 0 means all users
334 * @return array array of grades, false if none
335 */
336function lesson_get_user_grades($lesson, $userid=0) {
646fc290 337 global $CFG, $DB;
bbcbc0fe 338
b920f6e8 339 $params = array("lessonid" => $lesson->id,"lessonid2" => $lesson->id);
fe75e76b 340
646fc290 341 if (isset($userid)) {
342 $params["userid"] = $userid;
b920f6e8 343 $params["userid2"] = $userid;
646fc290 344 $user = "AND u.id = :userid";
b920f6e8 345 $fuser = "AND uu.id = :userid2";
646fc290 346 }
347 else {
e57bc529 348 $user="";
646fc290 349 $fuser="";
350 }
fe75e76b 351
512ce792 352 if ($lesson->retake) {
353 if ($lesson->usemaxgrade) {
ac9b0805 354 $sql = "SELECT u.id, u.id AS userid, MAX(g.grade) AS rawgrade
646fc290 355 FROM {user} u, {lesson_grades} g
356 WHERE u.id = g.userid AND g.lessonid = :lessonid
92bcca38 357 $user
358 GROUP BY u.id";
512ce792 359 } else {
ac9b0805 360 $sql = "SELECT u.id, u.id AS userid, AVG(g.grade) AS rawgrade
646fc290 361 FROM {user} u, {lesson_grades} g
362 WHERE u.id = g.userid AND g.lessonid = :lessonid
92bcca38 363 $user
364 GROUP BY u.id";
512ce792 365 }
96fa52a4 366 unset($params['lessonid2']);
367 unset($params['userid2']);
512ce792 368 } else {
92bcca38 369 // use only first attempts (with lowest id in lesson_grades table)
370 $firstonly = "SELECT uu.id AS userid, MIN(gg.id) AS firstcompleted
646fc290 371 FROM {user} uu, {lesson_grades} gg
b920f6e8 372 WHERE uu.id = gg.userid AND gg.lessonid = :lessonid2
92bcca38 373 $fuser
374 GROUP BY uu.id";
375
ac9b0805 376 $sql = "SELECT u.id, u.id AS userid, g.grade AS rawgrade
646fc290 377 FROM {user} u, {lesson_grades} g, ($firstonly) f
378 WHERE u.id = g.userid AND g.lessonid = :lessonid
92bcca38 379 AND g.id = f.firstcompleted AND g.userid=f.userid
380 $user";
381 }
382
646fc290 383 return $DB->get_records_sql($sql, $params);
92bcca38 384}
385
386/**
387 * Update grades in central gradebook
388 *
8cc86111 389 * @global stdclass
390 * @global object
775f811a 391 * @param object $lesson
392 * @param int $userid specific user only, 0 means all
8cc86111 393 * @param bool $nullifnone
92bcca38 394 */
775f811a 395function lesson_update_grades($lesson, $userid=0, $nullifnone=true) {
646fc290 396 global $CFG, $DB;
775f811a 397 require_once($CFG->libdir.'/gradelib.php');
92bcca38 398
775f811a 399 if ($lesson->grade == 0) {
400 lesson_grade_item_update($lesson);
92bcca38 401
775f811a 402 } else if ($grades = lesson_get_user_grades($lesson, $userid)) {
403 lesson_grade_item_update($lesson, $grades);
eafb9d9e 404
775f811a 405 } else if ($userid and $nullifnone) {
406 $grade = new object();
407 $grade->userid = $userid;
408 $grade->rawgrade = NULL;
409 lesson_grade_item_update($lesson, $grade);
92bcca38 410
411 } else {
775f811a 412 lesson_grade_item_update($lesson);
413 }
414}
415
416/**
417 * Update all grades in gradebook.
8cc86111 418 *
419 * @global object
775f811a 420 */
421function lesson_upgrade_grades() {
422 global $DB;
423
424 $sql = "SELECT COUNT('x')
425 FROM {lesson} l, {course_modules} cm, {modules} m
426 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id";
427 $count = $DB->count_records_sql($sql);
428
429 $sql = "SELECT l.*, cm.idnumber AS cmidnumber, l.course AS courseid
430 FROM {lesson} l, {course_modules} cm, {modules} m
431 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id";
432 if ($rs = $DB->get_recordset_sql($sql)) {
775f811a 433 $pbar = new progress_bar('lessonupgradegrades', 500, true);
434 $i=0;
435 foreach ($rs as $lesson) {
436 $i++;
437 upgrade_set_timeout(60*5); // set up timeout, may also abort execution
438 lesson_update_grades($lesson, 0, false);
439 $pbar->update($i, $count, "Updating Lesson grades ($i/$count).");
ac8e16be 440 }
775f811a 441 $rs->close();
92bcca38 442 }
443}
5f649aaa 444
92bcca38 445/**
446 * Create grade item for given lesson
447 *
8cc86111 448 * @global stdClass
449 * @uses GRADE_TYPE_VALUE
450 * @uses GRADE_TYPE_NONE
92bcca38 451 * @param object $lesson object with extra cmidnumber
8cc86111 452 * @param array|object $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
92bcca38 453 * @return int 0 if ok, error code otherwise
454 */
0b5a80a1 455function lesson_grade_item_update($lesson, $grades=NULL) {
92bcca38 456 global $CFG;
457 if (!function_exists('grade_update')) { //workaround for buggy PHP versions
458 require_once($CFG->libdir.'/gradelib.php');
459 }
460
461 if (array_key_exists('cmidnumber', $lesson)) { //it may not be always present
462 $params = array('itemname'=>$lesson->name, 'idnumber'=>$lesson->cmidnumber);
ac8e16be 463 } else {
92bcca38 464 $params = array('itemname'=>$lesson->name);
ac8e16be 465 }
92bcca38 466
467 if ($lesson->grade > 0) {
468 $params['gradetype'] = GRADE_TYPE_VALUE;
91ca18e8 469 $params['grademax'] = $lesson->grade;
92bcca38 470 $params['grademin'] = 0;
92bcca38 471
472 } else {
473 $params['gradetype'] = GRADE_TYPE_NONE;
92bcca38 474 }
475
0b5a80a1 476 if ($grades === 'reset') {
477 $params['reset'] = true;
478 $grades = NULL;
13b92708 479 } else if (!empty($grades)) {
480 // Need to calculate raw grade (Note: $grades has many forms)
481 if (is_object($grades)) {
482 $grades = array($grades->userid => $grades);
483 } else if (array_key_exists('userid', $grades)) {
484 $grades = array($grades['userid'] => $grades);
485 }
486 foreach ($grades as $key => $grade) {
487 if (!is_array($grade)) {
488 $grades[$key] = $grade = (array) $grade;
489 }
490 $grades[$key]['rawgrade'] = ($grade['rawgrade'] * $lesson->grade / 100);
491 }
0b5a80a1 492 }
493
494 return grade_update('mod/lesson', $lesson->course, 'mod', 'lesson', $lesson->id, 0, $grades, $params);
92bcca38 495}
496
497/**
498 * Delete grade item for given lesson
499 *
8cc86111 500 * @global stdClass
92bcca38 501 * @param object $lesson object
502 * @return object lesson
503 */
504function lesson_grade_item_delete($lesson) {
505 global $CFG;
2f67a9b3 506
bbcbc0fe 507}
508
92bcca38 509
8cc86111 510/**
511 * Must return an array of user records (all data) who are participants
512 * for a given instance of lesson. Must include every user involved
513 * in the instance, independient of his role (student, teacher, admin...)
514 *
515 * @global stdClass
516 * @global object
517 * @param int $lessonid
518 * @return array
519 */
bbcbc0fe 520function lesson_get_participants($lessonid) {
646fc290 521 global $CFG, $DB;
5f649aaa 522
6e773096 523 //Get students
646fc290 524 $params = array ("lessonid" => $lessonid);
525 $students = $DB->get_records_sql("SELECT DISTINCT u.id, u.id
526 FROM {user} u,
527 {lesson_attempts} a
528 WHERE a.lessonid = :lessonid and
529 u.id = a.userid", $params);
6e773096 530
531 //Return students array (it contains an array of unique users)
532 return ($students);
bbcbc0fe 533}
f3221af9 534
8cc86111 535/**
536 * @return array
537 */
f3221af9 538function lesson_get_view_actions() {
539 return array('view','view all');
540}
541
8cc86111 542/**
543 * @return array
544 */
f3221af9 545function lesson_get_post_actions() {
0a4abb73 546 return array('end','start');
f3221af9 547}
548
acf85537 549/**
550 * Runs any processes that must run before
551 * a lesson insert/update
552 *
8cc86111 553 * @global object
acf85537 554 * @param object $lesson Lesson form data
555 * @return void
556 **/
557function lesson_process_pre_save(&$lesson) {
646fc290 558 global $DB;
fe75e76b 559
acf85537 560 $lesson->timemodified = time();
561
862bd9cd 562 if (empty($lesson->timed)) {
563 $lesson->timed = 0;
564 }
acf85537 565 if (empty($lesson->timespent) or !is_numeric($lesson->timespent) or $lesson->timespent < 0) {
566 $lesson->timespent = 0;
567 }
568 if (!isset($lesson->completed)) {
569 $lesson->completed = 0;
570 }
571 if (empty($lesson->gradebetterthan) or !is_numeric($lesson->gradebetterthan) or $lesson->gradebetterthan < 0) {
572 $lesson->gradebetterthan = 0;
573 } else if ($lesson->gradebetterthan > 100) {
574 $lesson->gradebetterthan = 100;
575 }
576
0a4abb73
SH
577 if (empty($lesson->width)) {
578 $lesson->width = 640;
579 }
580 if (empty($lesson->height)) {
581 $lesson->height = 480;
582 }
583 if (empty($lesson->bgcolor)) {
584 $lesson->bgcolor = '#FFFFFF';
585 }
586
acf85537 587 // Conditions for dependency
588 $conditions = new stdClass;
589 $conditions->timespent = $lesson->timespent;
590 $conditions->completed = $lesson->completed;
591 $conditions->gradebetterthan = $lesson->gradebetterthan;
646fc290 592 $lesson->conditions = serialize($conditions);
acf85537 593 unset($lesson->timespent);
594 unset($lesson->completed);
595 unset($lesson->gradebetterthan);
596
4d22ec81 597 if (empty($lesson->password)) {
acf85537 598 unset($lesson->password);
599 }
acf85537 600}
601
602/**
603 * Runs any processes that must be run
604 * after a lesson insert/update
605 *
8cc86111 606 * @global object
acf85537 607 * @param object $lesson Lesson form data
608 * @return void
609 **/
610function lesson_process_post_save(&$lesson) {
c18269c7 611 global $DB;
612
613 if ($events = $DB->get_records('event', array('modulename'=>'lesson', 'instance'=>$lesson->id))) {
acf85537 614 foreach($events as $event) {
0a4abb73
SH
615 $event = calendar_event::load($event->id);
616 $event->delete();
acf85537 617 }
618 }
619
620 $event = new stdClass;
621 $event->description = $lesson->name;
622 $event->courseid = $lesson->course;
623 $event->groupid = 0;
624 $event->userid = 0;
625 $event->modulename = 'lesson';
626 $event->instance = $lesson->id;
627 $event->eventtype = 'open';
628 $event->timestart = $lesson->available;
fe75e76b 629
acf85537 630 $event->visible = instance_is_visible('lesson', $lesson);
fe75e76b 631
acf85537 632 $event->timeduration = ($lesson->deadline - $lesson->available);
633
634 if ($lesson->deadline and $lesson->available and $event->timeduration <= LESSON_MAX_EVENT_LENGTH) {
635 // Single event for the whole lesson.
636 $event->name = $lesson->name;
0a4abb73 637 calendar_event::create(clone($event));
acf85537 638 } else {
639 // Separate start and end events.
640 $event->timeduration = 0;
641 if ($lesson->available) {
642 $event->name = $lesson->name.' ('.get_string('lessonopens', 'lesson').')';
0a4abb73
SH
643 calendar_event::create(clone($event));
644 } else if ($lesson->deadline) {
acf85537 645 $event->name = $lesson->name.' ('.get_string('lessoncloses', 'lesson').')';
646 $event->timestart = $lesson->deadline;
647 $event->eventtype = 'close';
0a4abb73 648 calendar_event::create(clone($event));
acf85537 649 }
650 }
651}
652
0b5a80a1 653
654/**
655 * Implementation of the function for printing the form elements that control
656 * whether the course reset functionality affects the lesson.
fe75e76b 657 *
0b5a80a1 658 * @param $mform form passed by reference
659 */
660function lesson_reset_course_form_definition(&$mform) {
661 $mform->addElement('header', 'lessonheader', get_string('modulenameplural', 'lesson'));
662 $mform->addElement('advcheckbox', 'reset_lesson', get_string('deleteallattempts','lesson'));
663}
664
665/**
666 * Course reset form defaults.
8cc86111 667 * @param object $course
668 * @return array
0b5a80a1 669 */
670function lesson_reset_course_form_defaults($course) {
671 return array('reset_lesson'=>1);
672}
673
674/**
675 * Removes all grades from gradebook
8cc86111 676 *
677 * @global stdClass
678 * @global object
0b5a80a1 679 * @param int $courseid
680 * @param string optional type
681 */
682function lesson_reset_gradebook($courseid, $type='') {
646fc290 683 global $CFG, $DB;
0b5a80a1 684
685 $sql = "SELECT l.*, cm.idnumber as cmidnumber, l.course as courseid
646fc290 686 FROM {lesson} l, {course_modules} cm, {modules} m
687 WHERE m.name='lesson' AND m.id=cm.module AND cm.instance=l.id AND l.course=:course";
688 $params = array ("course" => $courseid);
689 if ($lessons = $DB->get_records_sql($sql,$params)) {
0b5a80a1 690 foreach ($lessons as $lesson) {
691 lesson_grade_item_update($lesson, 'reset');
692 }
693 }
694}
695
696/**
697 * Actual implementation of the rest coures functionality, delete all the
698 * lesson attempts for course $data->courseid.
8cc86111 699 *
700 * @global stdClass
701 * @global object
702 * @param object $data the data submitted from the reset course.
0b5a80a1 703 * @return array status array
704 */
705function lesson_reset_userdata($data) {
646fc290 706 global $CFG, $DB;
0b5a80a1 707
708 $componentstr = get_string('modulenameplural', 'lesson');
709 $status = array();
710
711 if (!empty($data->reset_lesson)) {
712 $lessonssql = "SELECT l.id
646fc290 713 FROM {lesson} l
714 WHERE l.course=:course";
715
716 $params = array ("course" => $data->courseid);
717 $DB->delete_records_select('lesson_timer', "lessonid IN ($lessonssql)", $params);
718 $DB->delete_records_select('lesson_high_scores', "lessonid IN ($lessonssql)", $params);
719 $DB->delete_records_select('lesson_grades', "lessonid IN ($lessonssql)", $params);
720 $DB->delete_records_select('lesson_attempts', "lessonid IN ($lessonssql)", $params);
0b5a80a1 721
722 // remove all grades from gradebook
723 if (empty($data->reset_gradebook_grades)) {
724 lesson_reset_gradebook($data->courseid);
725 }
726
727 $status[] = array('component'=>$componentstr, 'item'=>get_string('deleteallattempts', 'lesson'), 'error'=>false);
728 }
729
730 /// updating dates - shift may be negative too
731 if ($data->timeshift) {
732 shift_course_mod_dates('lesson', array('available', 'deadline'), $data->timeshift, $data->courseid);
733 $status[] = array('component'=>$componentstr, 'item'=>get_string('datechanged'), 'error'=>false);
734 }
735
736 return $status;
737}
738
f432bebf 739/**
740 * Returns all other caps used in module
8cc86111 741 * @return array
f432bebf 742 */
743function lesson_get_extra_capabilities() {
744 return array('moodle/site:accessallgroups');
745}
746
18a2a0cb 747/**
8cc86111 748 * @uses FEATURE_GROUPS
749 * @uses FEATURE_GROUPINGS
750 * @uses FEATURE_GROUPMEMBERSONLY
751 * @uses FEATURE_MOD_INTRO
752 * @uses FEATURE_COMPLETION_TRACKS_VIEWS
753 * @uses FEATURE_GRADE_HAS_GRADE
754 * @uses FEATURE_GRADE_OUTCOMES
18a2a0cb 755 * @param string $feature FEATURE_xx constant for requested feature
8cc86111 756 * @return mixed True if module supports feature, false if not, null if doesn't know
18a2a0cb 757 */
758function lesson_supports($feature) {
759 switch($feature) {
42f103be 760 case FEATURE_GROUPS: return false;
761 case FEATURE_GROUPINGS: return false;
762 case FEATURE_GROUPMEMBERSONLY: return true;
c6949936 763 case FEATURE_MOD_INTRO: return false;
18a2a0cb 764 case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
42f103be 765 case FEATURE_GRADE_HAS_GRADE: return true;
766 case FEATURE_GRADE_OUTCOMES: return true;
767
18a2a0cb 768 default: return null;
769 }
770}
c6949936 771
772/**
0a4abb73 773 * This function extends the global navigaiton for the site.
c6949936 774 * It is important to note that you should not rely on PAGE objects within this
775 * body of code as there is no guarantee that during an AJAX request they are
776 * available
777 *
778 * @param navigation_node $navigation The lesson node within the global navigation
779 * @param stdClass $course The course object returned from the DB
780 * @param stdClass $module The module object returned from the DB
781 * @param stdClass $cm The course module isntance returned from the DB
782 */
783function lesson_extend_navigation($navigation, $course, $module, $cm) {
784 /**
785 * This is currently just a stub so that it can be easily expanded upon.
786 * When expanding just remove this comment and the line below and then add
787 * you content.
788 */
789 $navigation->nodetype = navigation_node::NODETYPE_LEAF;
790}
791
792/**
793 * This function extends the settings navigation block for the site.
794 *
795 * It is safe to rely on PAGE here as we will only ever be within the module
796 * context when this is called
797 *
798 * @param navigation_node $settings
799 * @param stdClass $module
800 */
801function lesson_extend_settings_navigation($settings, $module) {
56115eea 802 global $PAGE, $CFG, $USER, $OUTPUT;
c6949936 803
c6949936 804 $lessonnavkey = $settings->add(get_string('lessonadministration', 'lesson'));
805 $lessonnav = $settings->get($lessonnavkey);
806 $lessonnav->forceopen = true;
807
0a4abb73
SH
808 if (empty($PAGE->cm->context)) {
809 $PAGE->cm->context = get_context_instance(CONTEXT_MODULE, $PAGE->cm->instance);
810 }
811
c6949936 812 $canedit = has_capability('mod/lesson:edit', $PAGE->cm->context);
813
a6855934 814 $url = new moodle_url('/mod/lesson/view.php', array('id'=>$PAGE->cm->id));
c6949936 815 $key = $lessonnav->add(get_string('preview', 'lesson'), $url);
816
817 if ($canedit) {
a6855934 818 $url = new moodle_url('/mod/lesson/edit.php', array('id'=>$PAGE->cm->id));
c6949936 819 $key = $lessonnav->add(get_string('edit', 'lesson'), $url);
820 }
821
822 if (has_capability('mod/lesson:manage', $PAGE->cm->context)) {
823 $key = $lessonnav->add(get_string('reports', 'lesson'));
a6855934 824 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportoverview'));
c6949936 825 $lessonnav->get($key)->add(get_string('overview', 'lesson'), $url);
a6855934 826 $url = new moodle_url('/mod/lesson/report.php', array('id'=>$PAGE->cm->id, 'action'=>'reportdetail'));
c6949936 827 $lessonnav->get($key)->add(get_string('detailedstats', 'lesson'), $url);
828 }
829
830 if ($canedit) {
a6855934 831 $url = new moodle_url('/mod/lesson/essay.php', array('id'=>$PAGE->cm->id));
c6949936 832 $lessonnav->add(get_string('manualgrading', 'lesson'), $url);
833 }
834
835 if ($lesson->highscores) {
a6855934 836 $url = new moodle_url('/mod/lesson/highscores.php', array('id'=>$PAGE->cm->id));
c6949936 837 $lessonnav->add(get_string('highscores', 'lesson'), $url);
838 }
839
840 if (has_capability('moodle/course:manageactivities', $PAGE->cm->context)) {
a6855934 841 $url = new moodle_url('/course/mod.php', array('update' => $PAGE->cm->id, 'return' => true, 'sesskey' => sesskey()));
c6949936 842 $lessonnav->add(get_string('updatethis', '', get_string('modulename', 'lesson')), $url);
843 }
844
845 if (count($lessonnav->children)<1) {
846 $settings->remove_child($lessonnavkey);
847 }
1a96363a 848}
0a4abb73
SH
849
850/**
851 * Get list of available import or export formats
852 *
853 * Copied and modified from lib/questionlib.php
854 *
855 * @param string $type 'import' if import list, otherwise export list assumed
856 * @return array sorted list of import/export formats available
857 */
858function lesson_get_import_export_formats($type) {
859 global $CFG;
860 $fileformats = get_plugin_list("qformat");
861
862 $fileformatname=array();
863 foreach ($fileformats as $fileformat=>$fdir) {
864 $format_file = "$fdir/format.php";
865 if (file_exists($format_file) ) {
866 require_once($format_file);
867 } else {
868 continue;
869 }
870 $classname = "qformat_$fileformat";
871 $format_class = new $classname();
872 if ($type=='import') {
873 $provided = $format_class->provide_import();
874 } else {
875 $provided = $format_class->provide_export();
876 }
877 if ($provided) {
878 $formatname = get_string($fileformat, 'quiz');
879 if ($formatname == "[[$fileformat]]") {
880 $formatname = get_string($fileformat, 'qformat_'.$fileformat);
881 if ($formatname == "[[$fileformat]]") {
882 $formatname = $fileformat; // Just use the raw folder name
883 }
884 }
885 $fileformatnames[$fileformat] = $formatname;
886 }
887 }
888 natcasesort($fileformatnames);
889
890 return $fileformatnames;
891}
892
893/**
894 * Serves the lesson attachments. Implements needed access control ;-)
895 *
896 * @param object $course
897 * @param object $cminfo
898 * @param object $context
899 * @param string $filearea
900 * @param array $args
901 * @param bool $forcedownload
902 * @return bool false if file not found, does not return if found - justsend the file
903 */
904function lesson_pluginfile($course, $cminfo, $context, $filearea, $args, $forcedownload) {
905 global $CFG, $DB;
906
907 if (!$cminfo->uservisible) {
908 return false;
909 }
2f67a9b3 910
0a4abb73
SH
911 $fileareas = lesson_get_file_areas();
912 if (!array_key_exists($filearea, $fileareas)) {
913 return false;
914 }
915
916 if (!$cm = get_coursemodule_from_instance('lesson', $cminfo->instance, $course->id)) {
917 return false;
918 }
919
920 if (!$lesson = $DB->get_record('lesson', array('id'=>$cminfo->instance))) {
921 return false;
922 }
923
924 require_course_login($course, true, $cm);
925
926 if ($filearea === 'lesson_page_content') {
927 $pageid = (int)array_shift($args);
928 if (!$page = $DB->get_record('lesson_pages', array('id'=>$pageid))) {
929 return false;
930 }
931 $fullpath = $context->id.$filearea.$pageid.'/'.implode('/', $args);
932 $forcedownload = true;
933 } else {
934 $fullpath = $context->id.$filearea.implode('/', $args);
935 }
936
937 $fs = get_file_storage();
938 if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
939 return false;
940 }
941
942 // finally send the file
943 send_stored_file($file, 0, 0, $forcedownload); // download MUST be forced - security!
944}
945
946/**
947 * Returns an array of file areas
948 * @return array
949 */
950function lesson_get_file_areas() {
951 return array('lesson_page_contents'=>'lesson_page_contents', 'lesson_media_file'=>'lesson_media_file');
952}
953
954/**
955 * Returns a file_info_stored object for the file being requested here
956 *
957 * @global <type> $CFG
958 * @param file_browse $browser
959 * @param array $areas
960 * @param object $course
961 * @param object $cm
962 * @param object $context
963 * @param string $filearea
964 * @param int $itemid
965 * @param string $filepath
966 * @param string $filename
967 * @return file_info_stored
968 */
969function lesson_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
970 global $CFG;
971 $fs = get_file_storage();
972 $filepath = is_null($filepath) ? '/' : $filepath;
973 $filename = is_null($filename) ? '.' : $filename;
974 $urlbase = $CFG->wwwroot.'/pluginfile.php';
975 if (!$storedfile = $fs->get_file($context->id, $filearea, $itemid, $filepath, $filename)) {
976 return null;
977 }
978 return new file_info_stored($browser, $context, $storedfile, $urlbase, $filearea, $itemid, true, true, false);
979}
980
981/**
982 * This is a function used to detect media types and generate html code.
983 *
984 * @global object $CFG
985 * @global object $PAGE
986 * @param object $lesson
987 * @param object $context
988 * @return string $code the html code of media
989 */
990function lesson_get_media_html($lesson, $context) {
991 global $CFG, $PAGE, $OUTPUT;
992
993 // get the media file from file pool
994 $browser = get_file_browser();
995 $file_info = $browser->get_file_info($context, 'lesson_media_file', $lesson->id, '/', $lesson->mediafile);
996 $url = $file_info->get_url();
997 $title = $lesson->mediafile;
998
999 $clicktoopen = $OUTPUT->link(new moodle_url($url), get_string('download'));
1000
1001 $mimetype = resourcelib_guess_url_mimetype($url);
1002
1003 // find the correct type and print it out
1004 if (in_array($mimetype, array('image/gif','image/jpeg','image/png'))) { // It's an image
1005 $code = resourcelib_embed_image($url, $title);
1006
1007 } else if ($mimetype == 'audio/mp3') {
1008 // MP3 audio file
1009 $code = resourcelib_embed_mp3($url, $title, $clicktoopen);
1010
1011 } else if ($mimetype == 'video/x-flv') {
1012 // Flash video file
1013 $code = resourcelib_embed_flashvideo($url, $title, $clicktoopen);
1014
1015 } else if ($mimetype == 'application/x-shockwave-flash') {
1016 // Flash file
1017 $code = resourcelib_embed_flash($url, $title, $clicktoopen);
1018
1019 } else if (substr($mimetype, 0, 10) == 'video/x-ms') {
1020 // Windows Media Player file
1021 $code = resourcelib_embed_mediaplayer($url, $title, $clicktoopen);
1022
1023 } else if ($mimetype == 'video/quicktime') {
1024 // Quicktime file
1025 $code = resourcelib_embed_quicktime($url, $title, $clicktoopen);
1026
1027 } else if ($mimetype == 'video/mpeg') {
1028 // Mpeg file
1029 $code = resourcelib_embed_mpeg($url, $title, $clicktoopen);
1030
1031 } else if ($mimetype == 'audio/x-pn-realaudio-plugin') {
1032 // RealMedia file
1033 $code = resourcelib_embed_real($url, $title, $clicktoopen);
1034
1035 } else {
1036 // anything else - just try object tag enlarged as much as possible
1037 $code = resourcelib_embed_general($url, $title, $clicktoopen, $mimetype);
f44b10ed 1038 $PAGE->requires->yui2_lib('dom');
9dec75db 1039 $PAGE->requires->js('/mod/url/functions.js');
593f9b87 1040 $PAGE->requires->js_function_call('imscp_setup_object', null, true);
0a4abb73
SH
1041 }
1042
1043 return $code;
1044}
1045
1046/**
1047 * Abstract class to provide a core functions to the all lesson classes
1048 *
1049 * This class should be abstracted by ALL classes with the lesson module to ensure
1050 * that all classes within this module can be interacted with in the same way.
1051 *
1052 * This class provides the user with a basic properties array that can be fetched
1053 * or set via magic methods, or alternativily by defining methods get_blah() or
1054 * set_blah() within the extending object.
1055 *
1056 * @copyright 2009 Sam Hemelryk
1057 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1058 */
1059abstract class lesson_base {
1060
1061 /**
1062 * An object containing properties
1063 * @var stdClass
1064 */
1065 protected $properties;
1066
1067 /**
1068 * The constructor
1069 * @param stdClass $properties
1070 */
1071 public function __construct($properties) {
1072 $this->properties = (object)$properties;
1073 }
1074
1075 /**
1076 * Magic property method
1077 *
1078 * Attempts to call a set_$key method if one exists otherwise falls back
1079 * to simply set the property
1080 *
1081 * @param string $key
1082 * @param mixed $value
1083 */
1084 public function __set($key, $value) {
1085 if (method_exists($this, 'set_'.$key)) {
1086 $this->{'set_'.$key}($value);
1087 }
1088 $this->properties->{$key} = $value;
1089 }
1090
1091 /**
1092 * Magic get method
1093 *
1094 * Attempts to call a get_$key method to return the property and ralls over
1095 * to return the raw property
1096 *
1097 * @param str $key
1098 * @return mixed
1099 */
1100 public function __get($key) {
1101 if (method_exists($this, 'get_'.$key)) {
1102 return $this->{'get_'.$key}();
1103 }
1104 return $this->properties->{$key};
1105 }
1106
1107 /**
1108 * Stupid PHP needs an isset magic method if you use the get magic method and
1109 * still want empty calls to work.... blah ~!
1110 *
1111 * @param string $key
1112 * @return bool
1113 */
1114 public function __isset($key) {
1115 if (method_exists($this, 'get_'.$key)) {
1116 $val = $this->{'get_'.$key}();
1117 return !empty($val);
1118 }
1119 return !empty($this->properties->{$key});
1120 }
1121
1122 /**
1123 * If overriden should create a new instance, save it in the DB and return it
1124 */
1125 public static function create() {}
1126 /**
1127 * If overriden should load an instance from the DB and return it
1128 */
1129 public static function load() {}
1130 /**
1131 * Fetches all of the properties of the object
1132 * @return stdClass
1133 */
1134 public function properties() {
1135 return $this->properties;
1136 }
1137}
1138
1139/**
1140 * Class representation of a lesson
1141 *
1142 * This class is used the interact with, and manage a lesson once instantiated.
1143 * If you need to fetch a lesson object you can do so by calling
1144 *
1145 * <code>
1146 * lesson::load($lessonid);
1147 * // or
1148 * $lessonrecord = $DB->get_record('lesson', $lessonid);
1149 * $lesson = new lesson($lessonrecord);
1150 * </code>
1151 *
1152 * The class itself extends lesson_base as all classes within the lesson module should
1153 *
1154 * These properties are from the database
1155 * @property int $id The id of this lesson
1156 * @property int $course The ID of the course this lesson belongs to
1157 * @property string $name The name of this lesson
1158 * @property int $practice Flag to toggle this as a practice lesson
1159 * @property int $modattempts Toggle to allow the user to go back and review answers
1160 * @property int $usepassword Toggle the use of a password for entry
1161 * @property string $password The password to require users to enter
1162 * @property int $dependency ID of another lesson this lesson is dependant on
1163 * @property string $conditions Conditions of the lesson dependency
1164 * @property int $grade The maximum grade a user can achieve (%)
1165 * @property int $custom Toggle custom scoring on or off
1166 * @property int $ongoing Toggle display of an ongoing score
1167 * @property int $usemaxgrade How retakes are handled (max=1, mean=0)
1168 * @property int $maxanswers The max number of answers or branches
1169 * @property int $maxattempts The maximum number of attempts a user can record
1170 * @property int $review Toggle use or wrong answer review button
1171 * @property int $nextpagedefault Override the default next page
1172 * @property int $feedback Toggles display of default feedback
1173 * @property int $minquestions Sets a minimum value of pages seen when calculating grades
1174 * @property int $maxpages Maximum number of pages this lesson can contain
1175 * @property int $retake Flag to allow users to retake a lesson
1176 * @property int $activitylink Relate this lesson to another lesson
1177 * @property string $mediafile File to pop up to or webpage to display
1178 * @property int $mediaheight Sets the height of the media file popup
1179 * @property int $mediawidth Sets the width of the media file popup
1180 * @property int $mediaclose Toggle display of a media close button
1181 * @property int $slideshow Flag for whether branch pages should be shown as slideshows
1182 * @property int $width Width of slideshow
1183 * @property int $height Height of slideshow
1184 * @property string $bgcolor Background colour of slideshow
1185 * @property int $displayleft Display a left meun
1186 * @property int $displayleftif Sets the condition on which the left menu is displayed
1187 * @property int $progressbar Flag to toggle display of a lesson progress bar
1188 * @property int $highscores Flag to toggle collection of high scores
1189 * @property int $maxhighscores Number of high scores to limit to
1190 * @property int $available Timestamp of when this lesson becomes available
1191 * @property int $deadline Timestamp of when this lesson is no longer available
1192 * @property int $timemodified Timestamp when lesson was last modified
1193 *
1194 * These properties are calculated
1195 * @property int $firstpageid Id of the first page of this lesson (prevpageid=0)
1196 * @property int $lastpageid Id of the last page of this lesson (nextpageid=0)
2f67a9b3 1197 *
0a4abb73
SH
1198 * @copyright 2009 Sam Hemelryk
1199 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1200 */
1201class lesson extends lesson_base {
1202
1203 /**
1204 * The id of the first page (where prevpageid = 0) gets set and retrieved by
1205 * {@see get_firstpageid()} by directly calling <code>$lesson->firstpageid;</code>
1206 * @var int
1207 */
1208 protected $firstpageid = null;
1209 /**
1210 * The id of the last page (where nextpageid = 0) gets set and retrieved by
1211 * {@see get_lastpageid()} by directly calling <code>$lesson->lastpageid;</code>
1212 * @var int
1213 */
1214 protected $lastpageid = null;
1215 /**
1216 * An array used to cache the pages associated with this lesson after the first
1217 * time they have been loaded.
1218 * A note to developers: If you are going to be working with MORE than one or
1219 * two pages from a lesson you should probably call {@see $lesson->load_all_pages()}
1220 * in order to save excess database queries.
1221 * @var array An array of lesson_page objects
1222 */
1223 protected $pages = array();
1224 /**
1225 * Flag that gets set to true once all of the pages associated with the lesson
1226 * have been loaded.
1227 * @var bool
1228 */
1229 protected $loadedallpages = false;
1230
1231 /**
1232 * Simply generates a lesson object given an array/object of properties
1233 * Overrides {@see lesson_base->create()}
1234 * @static
1235 * @param object|array $properties
1236 * @return lesson
1237 */
1238 public static function create($properties) {
1239 return new lesson($properties);
1240 }
1241
1242 /**
1243 * Generates a lesson object from the database given its id
1244 * @static
1245 * @param int $lessonid
1246 * @return lesson
1247 */
1248 public static function load($lessonid) {
1249 if (!$lesson = $DB->get_record('lesson', array('id' => $lessonid))) {
1250 print_error('invalidcoursemodule');
1251 }
1252 return new lesson($lesson);
1253 }
1254
1255 /**
1256 * Deletes this lesson from the database
1257 */
1258 public function delete() {
1259 global $CFG, $DB;
1260 require_once($CFG->libdir.'/gradelib.php');
1261
1262 $DB->delete_records("lesson", array("id"=>$this->properties->id));;
1263 $DB->delete_records("lesson_pages", array("lessonid"=>$this->properties->id));
1264 $DB->delete_records("lesson_answers", array("lessonid"=>$this->properties->id));
1265 $DB->delete_records("lesson_attempts", array("lessonid"=>$this->properties->id));
1266 $DB->delete_records("lesson_grades", array("lessonid"=>$this->properties->id));
1267 $DB->delete_records("lesson_timer", array("lessonid"=>$this->properties->id));
1268 $DB->delete_records("lesson_branch", array("lessonid"=>$this->properties->id));
1269 $DB->delete_records("lesson_high_scores", array("lessonid"=>$this->properties->id));
1270 if ($events = $DB->get_records('event', array("modulename"=>'lesson', "instance"=>$this->properties->id))) {
1271 foreach($events as $event) {
1272 $event = calendar_event::load($event);
1273 $event->delete();
1274 }
1275 }
1276
1277 grade_update('mod/lesson', $this->properties->course, 'mod', 'lesson', $this->properties->id, 0, NULL, array('deleted'=>1));
1278 return true;
1279 }
1280
1281 /**
1282 * Fetches messages from the session that may have been set in previous page
1283 * actions.
1284 *
1285 * <code>
1286 * // Do not call this method directly instead use
1287 * $lesson->messages;
1288 * </code>
1289 *
1290 * @return array
1291 */
1292 protected function get_messages() {
1293 global $SESSION;
1294
1295 $messages = array();
1296 if (!empty($SESSION->lesson_messages) && is_array($SESSION->lesson_messages) && array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1297 $messages = $SESSION->lesson_messages[$this->properties->id];
1298 unset($SESSION->lesson_messages[$this->properties->id]);
1299 }
2f67a9b3 1300
0a4abb73
SH
1301 return $messages;
1302 }
1303
1304 /**
1305 * Get all of the attempts for the current user.
1306 *
1307 * @param int $retries
1308 * @param bool $correct Optional: only fetch correct attempts
1309 * @param int $pageid Optional: only fetch attempts at the given page
1310 * @param int $userid Optional: defaults to the current user if not set
1311 * @return array|false
1312 */
1313 public function get_attempts($retries, $correct=false, $pageid=null, $userid=null) {
1314 global $USER, $DB;
1315 $params = array("lessonid"=>$this->properties->id, "userid"=>$userid, "retry"=>$retries);
1316 if ($correct) {
1317 $params['correct'] = 1;
1318 }
1319 if ($pageid !== null) {
1320 $params['pageid'] = $pageid;
1321 }
1322 if ($userid === null) {
1323 $params['userid'] = $USER->id;
1324 }
1325 return $DB->get_records('lesson_attempts', $params, 'timeseen DESC');
1326 }
1327
1328 /**
1329 * Returns the first page for the lesson or false if there isn't one.
1330 *
1331 * This method should be called via the magic method __get();
1332 * <code>
1333 * $firstpage = $lesson->firstpage;
1334 * </code>
1335 *
1336 * @return lesson_page|bool Returns the lesson_page specialised object or false
1337 */
1338 protected function get_firstpage() {
1339 $pages = $this->load_all_pages();
1340 if (count($pages) > 0) {
1341 foreach ($pages as $page) {
1342 if ((int)$page->prevpageid === 0) {
1343 return $page;
1344 }
1345 }
1346 }
1347 return false;
1348 }
1349
1350 /**
1351 * Returns the last page for the lesson or false if there isn't one.
1352 *
1353 * This method should be called via the magic method __get();
1354 * <code>
1355 * $lastpage = $lesson->lastpage;
1356 * </code>
1357 *
1358 * @return lesson_page|bool Returns the lesson_page specialised object or false
1359 */
1360 protected function get_lastpage() {
1361 $pages = $this->load_all_pages();
1362 if (count($pages) > 0) {
1363 foreach ($pages as $page) {
1364 if ((int)$page->nextpageid === 0) {
1365 return $page;
1366 }
1367 }
1368 }
1369 return false;
1370 }
1371
1372 /**
1373 * Returns the id of the first page of this lesson. (prevpageid = 0)
1374 * @return int
1375 */
1376 protected function get_firstpageid() {
1377 global $DB;
1378 if ($this->firstpageid == null) {
1379 if (!$this->loadedallpages) {
1380 $firstpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'prevpageid'=>0));
1381 if (!$firstpageid) {
1382 print_error('cannotfindfirstpage', 'lesson');
1383 }
1384 $this->firstpageid = $firstpageid;
1385 } else {
1386 $firstpage = $this->get_firstpage();
1387 $this->firstpageid = $firstpage->id;
1388 }
1389 }
1390 return $this->firstpageid;
1391 }
2f67a9b3 1392
0a4abb73
SH
1393 /**
1394 * Returns the id of the last page of this lesson. (nextpageid = 0)
1395 * @return int
1396 */
1397 public function get_lastpageid() {
1398 global $DB;
1399 if ($this->lastpageid == null) {
1400 if (!$this->loadedallpages) {
1401 $lastpageid = $DB->get_field('lesson_pages', 'id', array('lessonid'=>$this->properties->id, 'nextpageid'=>0));
1402 if (!$lastpageid) {
1403 print_error('cannotfindlastpage', 'lesson');
1404 }
1405 $this->lastpageid = $lastpageid;
1406 } else {
1407 $lastpageid = $this->get_lastpage();
1408 $this->lastpageid = $lastpageid->id;
1409 }
1410 }
1411
1412 return $this->lastpageid;
1413 }
1414
1415 /**
1416 * Gets the next page to display after the one that is provided.
1417 * @param int $nextpageid
1418 * @return bool
1419 */
1420 public function get_next_page($nextpageid) {
1421 global $USER;
1422 $allpages = $this->load_all_pages();
1423 if ($this->properties->nextpagedefault) {
1424 // in Flash Card mode...first get number of retakes
1425 shuffle($allpages);
1426 $found = false;
1427 if ($this->properties->nextpagedefault == LESSON_UNSEENPAGE) {
1428 foreach ($allpages as $nextpage) {
1429 if (!$DB->count_records("lesson_attempts", array("pageid"=>$nextpage->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
1430 $found = true;
1431 break;
1432 }
1433 }
1434 } elseif ($this->properties->nextpagedefault == LESSON_UNANSWEREDPAGE) {
1435 foreach ($allpages as $nextpage) {
1436 if (!$DB->count_records("lesson_attempts", array('pageid'=>$nextpage->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
1437 $found = true;
1438 break;
1439 }
1440 }
1441 }
1442 if ($found) {
1443 if ($this->properties->maxpages) {
1444 // check number of pages viewed (in the lesson)
1445 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$USER->id));
1446 if ($DB->count_records("lesson_attempts", array("lessonid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes)) >= $this->properties->maxpages) {
1447 return false;
1448 }
1449 }
1450 return $nextpage;
1451 }
1452 }
1453 // In a normal lesson mode
1454 foreach ($allpages as $nextpage) {
1455 if ((int)$nextpage->id===(int)$nextpageid) {
1456 return $nextpage;
1457 }
1458 }
1459 return false;
1460 }
1461
1462 /**
1463 * Sets a message against the session for this lesson that will displayed next
1464 * time the lesson processes messages
1465 *
1466 * @param string $message
1467 * @param string $class
1468 * @param string $align
1469 * @return bool
1470 */
1471 public function add_message($message, $class="notifyproblem", $align='center') {
1472 global $SESSION;
1473
1474 if (empty($SESSION->lesson_messages) || !is_array($SESSION->lesson_messages)) {
1475 $SESSION->lesson_messages = array();
1476 $SESSION->lesson_messages[$this->properties->id] = array();
1477 } else if (!array_key_exists($this->properties->id, $SESSION->lesson_messages)) {
1478 $SESSION->lesson_messages[$this->properties->id] = array();
1479 }
1480
1481 $SESSION->lesson_messages[$this->properties->id][] = array($message, $class, $align);
1482
1483 return true;
1484 }
1485
1486 /**
1487 * Check if the lesson is accessible at the present time
1488 * @return bool True if the lesson is accessible, false otherwise
1489 */
1490 public function is_accessible() {
1491 $available = $this->properties->available;
1492 $deadline = $this->properties->deadline;
1493 return (($available == 0 || time() >= $available) && ($deadline == 0 || time() < $deadline));
1494 }
1495
1496 /**
1497 * Starts the lesson time for the current user
1498 * @return bool Returns true
1499 */
1500 public function start_timer() {
1501 global $USER, $DB;
1502 $USER->startlesson[$this->properties->id] = true;
1503 $startlesson = new stdClass;
1504 $startlesson->lessonid = $this->properties->id;
1505 $startlesson->userid = $USER->id;
1506 $startlesson->starttime = time();
1507 $startlesson->lessontime = time();
1508 $DB->insert_record('lesson_timer', $startlesson);
1509 if ($this->properties->timed) {
1510 $this->add_message(get_string('maxtimewarning', 'lesson', $this->properties->maxtime), 'center');
1511 }
1512 return true;
1513 }
1514
1515 /**
1516 * Updates the timer to the current time and returns the new timer object
1517 * @param bool $restart If set to true the timer is restarted
1518 * @param bool $continue If set to true AND $restart=true then the timer
1519 * will continue from a previous attempt
1520 * @return stdClass The new timer
1521 */
1522 public function update_timer($restart=false, $continue=false) {
1523 global $USER, $DB;
1524 // clock code
1525 // get time information for this user
1526 if (!$timer = $DB->get_records('lesson_timer', array ("lessonid" => $this->properties->id, "userid" => $USER->id), 'starttime DESC', '*', 0, 1)) {
1527 print_error('cannotfindtimer', 'lesson');
1528 } else {
1529 $timer = current($timer); // this will get the latest start time record
1530 }
1531
1532 if ($restart) {
1533 if ($continue) {
1534 // continue a previous test, need to update the clock (think this option is disabled atm)
1535 $timer->starttime = time() - ($timer->lessontime - $timer->starttime);
1536 } else {
1537 // starting over, so reset the clock
1538 $timer->starttime = time();
1539 }
1540 }
1541
1542 $timer->lessontime = time();
1543 $DB->update_record('lesson_timer', $timer);
1544 return $timer;
1545 }
1546
1547 /**
1548 * Updates the timer to the current time then stops it by unsetting the user var
1549 * @return bool Returns true
1550 */
1551 public function stop_timer() {
1552 global $USER, $DB;
1553 unset($USER->startlesson[$this->properties->id]);
1554 return $this->update_timer(false, false);
1555 }
1556
1557 /**
1558 * Checks to see if the lesson has pages
1559 */
1560 public function has_pages() {
1561 global $DB;
1562 $pagecount = $DB->count_records('lesson_pages', array('lessonid'=>$this->properties->id));
1563 return ($pagecount>0);
1564 }
1565
1566 /**
1567 * Returns the link for the related activity
1568 * @return html_link|false
1569 */
1570 public function link_for_activitylink() {
1571 global $DB;
1572 $module = $DB->get_record('course_modules', array('id' => $this->properties->activitylink));
1573 if ($module) {
1574 $modname = $DB->get_field('modules', 'name', array('id' => $module->module));
1575 if ($modname) {
1576 $instancename = $DB->get_field($modname, 'name', array('id' => $module->instance));
1577 if ($instancename) {
a6855934 1578 $link = html_link::make(new moodle_url('/mod/'.$modname.'/view.php', array('id'=>$this->properties->activitylink)), get_string('returnto', 'lesson', get_string('activitylinkname', 'lesson', $instancename)));
0a4abb73
SH
1579 $link->set_classes(array('centerpadded','lessonbutton','standardbutton'));
1580 return $link;
1581 }
1582 }
1583 }
1584 return false;
1585 }
1586
1587 /**
1588 * Loads the requested page.
1589 *
1590 * This function will return the requested page id as either a specialised
1591 * lesson_page object OR as a generic lesson_page.
1592 * If the page has been loaded previously it will be returned from the pages
1593 * array, otherwise it will be loaded from the database first
1594 *
1595 * @param int $pageid
1596 * @return lesson_page A lesson_page object or an object that extends it
1597 */
1598 public function load_page($pageid) {
1599 if (!array_key_exists($pageid, $this->pages)) {
1600 $manager = lesson_page_type_manager::get($this);
1601 $this->pages[$pageid] = $manager->load_page($pageid, $this);
1602 }
1603 return $this->pages[$pageid];
1604 }
1605
1606 /**
1607 * Loads ALL of the pages for this lesson
1608 *
1609 * @return array An array containing all pages from this lesson
1610 */
1611 public function load_all_pages() {
1612 if (!$this->loadedallpages) {
1613 $manager = lesson_page_type_manager::get($this);
1614 $this->pages = $manager->load_all_pages($this);
1615 $this->loadedallpages = true;
1616 }
1617 return $this->pages;
1618 }
1619
1620 /**
1621 * Determins if a jumpto value is correct or not.
1622 *
1623 * returns true if jumpto page is (logically) after the pageid page or
1624 * if the jumpto value is a special value. Returns false in all other cases.
1625 *
1626 * @param int $pageid Id of the page from which you are jumping from.
1627 * @param int $jumpto The jumpto number.
1628 * @return boolean True or false after a series of tests.
1629 **/
1630 public function jumpto_is_correct($pageid, $jumpto) {
1631 global $DB;
1632
1633 // first test the special values
1634 if (!$jumpto) {
1635 // same page
1636 return false;
1637 } elseif ($jumpto == LESSON_NEXTPAGE) {
1638 return true;
1639 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
1640 return true;
1641 } elseif ($jumpto == LESSON_RANDOMPAGE) {
1642 return true;
1643 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
1644 return true;
1645 } elseif ($jumpto == LESSON_EOL) {
1646 return true;
1647 }
1648
1649 $pages = $this->load_all_pages();
1650 $apageid = $pages[$pageid]->nextpageid;
1651 while ($apageid != 0) {
1652 if ($jumpto == $apageid) {
1653 return true;
1654 }
1655 $apageid = $pages[$apageid]->nextpageid;
1656 }
1657 return false;
1658 }
1659
1660 /**
1661 * Returns the time a user has remaining on this lesson
1662 * @param int $starttime Starttime timestamp
1663 * @return string
1664 */
1665 public function time_remaining($starttime) {
1666 $timeleft = $starttime + $this->maxtime * 60 - time();
1667 $hours = floor($timeleft/3600);
1668 $timeleft = $timeleft - ($hours * 3600);
1669 $minutes = floor($timeleft/60);
1670 $secs = $timeleft - ($minutes * 60);
1671
1672 if ($minutes < 10) {
1673 $minutes = "0$minutes";
1674 }
1675 if ($secs < 10) {
1676 $secs = "0$secs";
1677 }
1678 $output = array();
1679 $output[] = $hours;
1680 $output[] = $minutes;
1681 $output[] = $secs;
1682 $output = implode(':', $output);
1683 return $output;
1684 }
1685
1686 /**
1687 * Interprets LESSON_CLUSTERJUMP jumpto value.
1688 *
1689 * This will select a page randomly
1690 * and the page selected will be inbetween a cluster page and end of cluter or end of lesson
1691 * and the page selected will be a page that has not been viewed already
1692 * and if any pages are within a branch table or end of branch then only 1 page within
1693 * the branch table or end of branch will be randomly selected (sub clustering).
1694 *
1695 * @param int $pageid Id of the current page from which we are jumping from.
1696 * @param int $userid Id of the user.
1697 * @return int The id of the next page.
1698 **/
1699 public function cluster_jump($pageid, $userid=null) {
1700 global $DB, $USER;
1701
1702 if ($userid===null) {
1703 $userid = $USER->id;
1704 }
1705 // get the number of retakes
1706 if (!$retakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->properties->id, "userid"=>$userid))) {
1707 $retakes = 0;
1708 }
1709 // get all the lesson_attempts aka what the user has seen
1710 $seenpages = array();
1711 if ($attempts = $this->get_attempts($retakes)) {
1712 foreach ($attempts as $attempt) {
1713 $seenpages[$attempt->pageid] = $attempt->pageid;
1714 }
2f67a9b3 1715
0a4abb73
SH
1716 }
1717
1718 // get the lesson pages
1719 $lessonpages = $this->load_all_pages();
1720 // find the start of the cluster
1721 while ($pageid != 0) { // this condition should not be satisfied... should be a cluster page
1722 if ($lessonpages[$pageid]->qtype == LESSON_PAGE_CLUSTER) {
1723 break;
1724 }
1725 $pageid = $lessonpages[$pageid]->prevpageid;
1726 }
1727
1728 $clusterpages = array();
1729 $clusterpages = $this->get_sub_pages_of($pageid, array(LESSON_PAGE_ENDOFCLUSTER));
1730 $unseen = array();
1731 foreach ($clusterpages as $key=>$cluster) {
1732 if ($cluster->type !== lesson_page::TYPE_QUESTION) {
1733 unset($clusterpages[$key]);
1734 } elseif ($cluster->is_unseen($seenpages)) {
1735 $unseen[] = $cluster;
1736 }
1737 }
1738
1739 if (count($unseen) > 0) {
1740 // it does not contain elements, then use exitjump, otherwise find out next page/branch
1741 $nextpage = $unseen[rand(0, count($unseen)-1)];
1742 if ($nextpage->qtype == LESSON_PAGE_BRANCHTABLE) {
1743 // if branch table, then pick a random page inside of it
1744 $branchpages = $this->get_sub_pages_of($nextpage->id, array(LESSON_PAGE_BRANCHTABLE, LESSON_PAGE_ENDOFBRANCH));
1745 return $branchpages[rand(0, count($branchpages)-1)]->id;
1746 } else { // otherwise, return the page's id
1747 return $nextpage->id;
1748 }
1749 } else {
1750 // seen all there is to see, leave the cluster
1751 if (end($clusterpages)->nextpageid == 0) {
1752 return LESSON_EOL;
1753 } else {
1754 $clusterendid = $pageid;
1755 while ($clusterendid != 0) { // this condition should not be satisfied... should be a cluster page
1756 if ($lessonpages[$clusterendid]->qtype == LESSON_PAGE_CLUSTER) {
1757 break;
1758 }
1759 $clusterendid = $lessonpages[$clusterendid]->prevpageid;
1760 }
1761 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $clusterendid, "lessonid" => $this->properties->id));
1762 if ($exitjump == LESSON_NEXTPAGE) {
1763 $exitjump = $lessonpages[$pageid]->nextpageid;
1764 }
1765 if ($exitjump == 0) {
1766 return LESSON_EOL;
1767 } else if (in_array($exitjump, array(LESSON_EOL, LESSON_PREVIOUSPAGE))) {
1768 return $exitjump;
1769 } else {
1770 if (!array_key_exists($exitjump, $lessonpages)) {
1771 $found = false;
1772 foreach ($lessonpages as $page) {
1773 if ($page->id === $clusterendid) {
1774 $found = true;
1775 } else if ($page->qtype == LESSON_PAGE_ENDOFCLUSTER) {
1776 $exitjump = $DB->get_field("lesson_answers", "jumpto", array("pageid" => $page->id, "lessonid" => $this->properties->id));
1777 break;
1778 }
1779 }
1780 }
1781 if (!array_key_exists($exitjump, $lessonpages)) {
1782 return LESSON_EOL;
1783 }
1784 return $exitjump;
1785 }
1786 }
1787 }
1788 }
1789
1790 /**
1791 * Finds all pages that appear to be a subtype of the provided pageid until
1792 * an end point specified within $ends is encountered or no more pages exist
1793 *
1794 * @param int $pageid
1795 * @param array $ends An array of LESSON_PAGE_* types that signify an end of
1796 * the subtype
1797 * @return array An array of specialised lesson_page objects
1798 */
1799 public function get_sub_pages_of($pageid, array $ends) {
1800 $lessonpages = $this->load_all_pages();
1801 $pageid = $lessonpages[$pageid]->nextpageid; // move to the first page after the branch table
1802 $pages = array();
1803
1804 while (true) {
1805 if ($pageid == 0 || in_array($lessonpages[$pageid]->qtype, $ends)) {
1806 break;
1807 }
1808 $pages[] = $lessonpages[$pageid];
1809 $pageid = $lessonpages[$pageid]->nextpageid;
1810 }
1811
1812 return $pages;
1813 }
1814
1815 /**
1816 * Checks to see if the specified page[id] is a subpage of a type specified in
1817 * the $types array, until either there are no more pages of we find a type
1818 * corrosponding to that of a type specified in $ends
1819 *
1820 * @param int $pageid The id of the page to check
1821 * @param array $types An array of types that would signify this page was a subpage
1822 * @param array $ends An array of types that mean this is not a subpage
1823 * @return bool
1824 */
1825 public function is_sub_page_of_type($pageid, array $types, array $ends) {
1826 $pages = $this->load_all_pages();
1827 $pageid = $pages[$pageid]->prevpageid; // move up one
1828
1829 array_unshift($ends, 0);
1830 // go up the pages till branch table
1831 while (true) {
1832 if ($pageid==0 || in_array($pages[$pageid]->qtype, $ends)) {
1833 return false;
1834 } else if (in_array($pages[$pageid]->qtype, $types)) {
1835 return true;
1836 }
1837 $pageid = $pages[$pageid]->prevpageid;
1838 }
1839 }
1840}
1841
1842/**
1843 * Abstract class representation of a page associated with a lesson.
1844 *
1845 * This class should MUST be extended by all specialised page types defined in
1846 * mod/lesson/pagetypes/.
1847 * There are a handful of abstract methods that need to be defined as well as
1848 * severl methods that can optionally be defined in order to make the page type
1849 * operate in the desired way
1850 *
1851 * Database properties
1852 * @property int $id The id of this lesson page
1853 * @property int $lessonid The id of the lesson this page belongs to
1854 * @property int $prevpageid The id of the page before this one
1855 * @property int $nextpageid The id of the next page in the page sequence
1856 * @property int $qtype Identifies the page type of this page
1857 * @property int $qoption Used to record page type specific options
1858 * @property int $layout Used to record page specific layout selections
1859 * @property int $display Used to record page specific display selections
1860 * @property int $timecreated Timestamp for when the page was created
1861 * @property int $timemodified Timestamp for when the page was last modified
1862 * @property string $title The title of this page
1863 * @property string $contents The rich content shown to describe the page
1864 * @property int $contentsformat The format of the contents field
1865 *
1866 * Calculated properties
1867 * @property-read array $answers An array of answers for this page
1868 * @property-read bool $displayinmenublock Toggles display in the left menu block
1869 * @property-read array $jumps An array containing all the jumps this page uses
1870 * @property-read lesson $lesson The lesson this page belongs to
1871 * @property-read int $type The type of the page [question | structure]
1872 * @property-read typeid The unique identifier for the page type
1873 * @property-read typestring The string that describes this page type
1874 *
1875 * @abstract
1876 * @copyright 2009 Sam Hemelryk
1877 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1878 */
1879abstract class lesson_page extends lesson_base {
1880
1881 /**
1882 * A reference to the lesson this page belongs to
1883 * @var lesson
1884 */
1885 protected $lesson = null;
1886 /**
1887 * Contains the answers to this lesson_page once loaded
1888 * @var null|array
1889 */
1890 protected $answers = null;
1891 /**
1892 * This sets the type of the page, can be one of the constants defined below
1893 * @var int
1894 */
1895 protected $type = 0;
1896
1897 /**
1898 * Constants used to identify the type of the page
1899 */
1900 const TYPE_QUESTION = 0;
1901 const TYPE_STRUCTURE = 1;
1902
1903 /**
1904 * This method should return the integer used to identify the page type within
1905 * the database and thoughout code. This maps back to the defines used in 1.x
1906 * @abstract
1907 * @return int
1908 */
1909 abstract protected function get_typeid();
1910 /**
1911 * This method should return the string that describes the pagetype
1912 * @abstract
1913 * @return string
1914 */
1915 abstract protected function get_typestring();
1916
1917 /**
1918 * This method gets called to display the page to the user taking the lesson
1919 * @abstract
1920 * @param object $renderer
1921 * @param object $attempt
1922 * @return string
1923 */
1924 abstract public function display($renderer, $attempt);
1925
1926 /**
1927 * Creates a new lesson_page within the database and returns the correct pagetype
1928 * object to use to interact with the new lesson
2f67a9b3 1929 *
0a4abb73
SH
1930 * @final
1931 * @static
1932 * @param object $properties
1933 * @param lesson $lesson
1934 * @return lesson_page Specialised object that extends lesson_page
1935 */
1936 final public static function create($properties, lesson $lesson, $context, $maxbytes) {
1937 global $DB;
1938 $newpage = new stdClass;
1939 $newpage->title = $properties->title;
1940 $newpage->contents = $properties->contents_editor['text'];
1941 $newpage->contentsformat = $properties->contents_editor['format'];
1942 $newpage->lessonid = $lesson->id;
1943 $newpage->timecreated = time();
1944 $newpage->qtype = $properties->qtype;
1945 $newpage->qoption = (isset($properties->qoption))?1:0;
1946 $newpage->layout = (isset($properties->layout))?1:0;
1947 $newpage->display = (isset($properties->display))?1:0;
1948 $newpage->prevpageid = 0; // this is a first page
1949 $newpage->nextpageid = 0; // this is the only page
1950
1951 if ($properties->pageid) {
1952 $prevpage = $DB->get_record("lesson_pages", array("id" => $properties->pageid), 'id, nextpageid');
1953 if (!$prevpage) {
1954 print_error('cannotfindpages', 'lesson');
1955 }
1956 $newpage->prevpageid = $prevpage->id;
1957 $newpage->nextpageid = $prevpage->nextpageid;
1958 } else {
1959 $nextpage = $DB->get_record('lesson_pages', array('lessonid'=>$lesson->id, 'prevpageid'=>0), 'id');
1960 if ($nextpage) {
1961 // This is the first page, there are existing pages put this at the start
1962 $newpage->nextpageid = $nextpage->id;
1963 }
1964 }
1965
1966 $newpage->id = $DB->insert_record("lesson_pages", $newpage);
1967
1968 $editor = new stdClass;
1969 $editor->id = $newpage->id;
1970 $editor->contents_editor = $properties->contents_editor;
1971 $editor = file_postupdate_standard_editor($editor, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'lesson_page_contents', $editor->id);
1972 $DB->update_record("lesson_pages", $editor);
1973
1974 if ($newpage->prevpageid > 0) {
1975 $DB->set_field("lesson_pages", "nextpageid", $newpage->id, array("id" => $newpage->prevpageid));
1976 }
1977 if ($newpage->nextpageid > 0) {
1978 $DB->set_field("lesson_pages", "prevpageid", $newpage->id, array("id" => $newpage->nextpageid));
1979 }
1980
1981 $page = lesson_page::load($newpage, $lesson);
1982 $page->create_answers($properties);
1983
1984 $lesson->add_message(get_string('insertedpage', 'lesson').': '.format_string($newpage->title, true), 'notifysuccess');
1985
1986 return $page;
1987 }
1988
1989 /**
1990 * This method loads a page object from the database and returns it as a
1991 * specialised object that extends lesson_page
1992 *
1993 * @final
1994 * @static
1995 * @param int $id
1996 * @param lesson $lesson
1997 * @return lesson_page Specialised lesson_page object
1998 */
1999 final public static function load($id, lesson $lesson) {
2000 global $DB;
2001
2002 if (is_object($id) && !empty($id->qtype)) {
2003 $page = $id;
2004 } else {
2005 $page = $DB->get_record("lesson_pages", array("id" => $id));
2006 if (!$page) {
2007 print_error('cannotfindpages', 'lesson');
2008 }
2009 }
2010 $manager = lesson_page_type_manager::get($lesson);
2011
2012 $class = 'lesson_page_type_'.$manager->get_page_type_idstring($page->qtype);
2013 if (!class_exists($class)) {
2014 $class = 'lesson_page';
2015 }
2016
2017 return new $class($page, $lesson);
2018 }
2019
2020 /**
2021 * Deletes a lesson_page from the database as well as any associated records.
2022 * @final
2023 * @return bool
2024 */
2025 final public function delete() {
2026 global $DB;
2027 // first delete all the associated records...
2028 $DB->delete_records("lesson_attempts", array("pageid" => $this->properties->id));
2029 // ...now delete the answers...
2030 $DB->delete_records("lesson_answers", array("pageid" => $this->properties->id));
2031 // ..and the page itself
2032 $DB->delete_records("lesson_pages", array("id" => $this->properties->id));
2033
2034 // repair the hole in the linkage
2035 if (!$this->properties->prevpageid && !$this->properties->nextpageid) {
2036 //This is the only page, no repair needed
2037 } elseif (!$this->properties->prevpageid) {
2038 // this is the first page...
2039 $page = $this->lesson->load_page($this->properties->nextpageid);
2040 $page->move(null, 0);
2041 } elseif (!$this->properties->nextpageid) {
2042 // this is the last page...
2043 $page = $this->lesson->load_page($this->properties->prevpageid);
2044 $page->move(0);
2045 } else {
2046 // page is in the middle...
2047 $prevpage = $this->lesson->load_page($this->properties->prevpageid);
2048 $nextpage = $this->lesson->load_page($this->properties->nextpageid);
2049
2050 $prevpage->move($nextpage->id);
2051 $nextpage->move(null, $prevpage->id);
2052 }
2053 return true;
2054 }
2055
2056 /**
2057 * Moves a page by updating its nextpageid and prevpageid values within
2058 * the database
2059 *
2060 * @final
2061 * @param int $nextpageid
2062 * @param int $prevpageid
2063 */
2064 final public function move($nextpageid=null, $prevpageid=null) {
2065 global $DB;
2066 if ($nextpageid === null) {
2067 $nextpageid = $this->properties->nextpageid;
2068 }
2069 if ($prevpageid === null) {
2070 $prevpageid = $this->properties->prevpageid;
2071 }
2072 $obj = new stdClass;
2073 $obj->id = $this->properties->id;
2074 $obj->prevpageid = $prevpageid;
2075 $obj->nextpageid = $nextpageid;
2076 $DB->update_record('lesson_pages', $obj);
2077 }
2078
2079 /**
2080 * Returns the answers that are associated with this page in the database
2081 *
2082 * @final
2083 * @return array
2084 */
2085 final public function get_answers() {
2086 global $DB;
2087 if ($this->answers === null) {
2088 $this->answers = array();
2089 $answers = $DB->get_records('lesson_answers', array('pageid'=>$this->properties->id, 'lessonid'=>$this->lesson->id), 'id');
2090 if (!$answers) {
2091 print_error('cannotfindanswer', 'lesson');
2092 }
2093 foreach ($answers as $answer) {
2094 $this->answers[count($this->answers)] = new lesson_page_answer($answer);
2095 }
2096 }
2097 return $this->answers;
2098 }
2099
2100 /**
2101 * Returns the lesson this page is associated with
2102 * @final
2103 * @return lesson
2104 */
2105 final protected function get_lesson() {
2106 return $this->lesson;
2107 }
2108
2109 /**
2110 * Returns the type of page this is. Not to be confused with page type
2111 * @final
2112 * @return int
2113 */
2114 final protected function get_type() {
2115 return $this->type;
2116 }
2117
2118 /**
2119 * Records an attempt at this page
2120 *
2121 * @final
2122 * @param stdClass $context
2123 * @return stdClass Returns the result of the attempt
2124 */
2125 final public function record_attempt($context) {
2126 global $DB, $USER, $OUTPUT;
2127
2128 /**
2129 * This should be overriden by each page type to actually check the response
2130 * against what ever custom criteria they have defined
2131 */
2132 $result = $this->check_answer();
2133
2134 $result->attemptsremaining = 0;
2135 $result->maxattemptsreached = false;
2136
2137 if ($result->noanswer) {
2138 $result->newpageid = $this->properties->id; // display same page again
2139 $result->feedback = get_string('noanswer', 'lesson');
2140 } else {
2141 if (!has_capability('mod/lesson:manage', $context)) {
2142 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2143 // record student's attempt
2144 $attempt = new stdClass;
2145 $attempt->lessonid = $this->lesson->id;
2146 $attempt->pageid = $this->properties->id;
2147 $attempt->userid = $USER->id;
2148 $attempt->answerid = $result->answerid;
2149 $attempt->retry = $nretakes;
2150 $attempt->correct = $result->correctanswer;
2151 if($result->userresponse !== null) {
2152 $attempt->useranswer = $result->userresponse;
2153 }
2154
2155 $attempt->timeseen = time();
2156 // if allow modattempts, then update the old attempt record, otherwise, insert new answer record
2157 if (isset($USER->modattempts[$this->lesson->id])) {
2158 $attempt->retry = $nretakes - 1; // they are going through on review, $nretakes will be too high
2159 }
2160
2161 $DB->insert_record("lesson_attempts", $attempt);
2162 // "number of attempts remaining" message if $this->lesson->maxattempts > 1
2163 // displaying of message(s) is at the end of page for more ergonomic display
2164 if (!$result->correctanswer && ($result->newpageid == 0)) {
2165 // wrong answer and student is stuck on this page - check how many attempts
2166 // the student has had at this page/question
2167 $nattempts = $DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id),"retry", $nretakes);
2168 // retreive the number of attempts left counter for displaying at bottom of feedback page
2169 if ($nattempts >= $this->lesson->maxattempts) {
2170 if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2171 $result->maxattemptsreached = true;
2172 }
2173 $result->newpageid = LESSON_NEXTPAGE;
2174 } else if ($this->lesson->maxattempts > 1) { // don't bother with message if only one attempt
2175 $result->attemptsremaining = $this->lesson->maxattempts - $nattempts;
2176 }
2177 }
2178 }
2179 // TODO: merge this code with the jump code below. Convert jumpto page into a proper page id
2180 if ($result->newpageid == 0) {
2181 $result->newpageid = $this->properties->id;
2182 } elseif ($result->newpageid == LESSON_NEXTPAGE) {
2183 $nextpage = $this->lesson->get_next_page($this->properties->nextpageid);
2184 if ($nextpage === false) {
2185 $result->newpageid = LESSON_EOL;
2186 } else {
2187 $result->newpageid = $nextpage->id;
2188 }
2189 }
2190
2191 // Determine default feedback if necessary
2192 if (empty($result->response)) {
2193 if (!$this->lesson->feedback && !$result->noanswer && !($this->lesson->review & !$result->correctanswer && !$result->isessayquestion)) {
2194 // These conditions have been met:
2195 // 1. The lesson manager has not supplied feedback to the student
2196 // 2. Not displaying default feedback
2197 // 3. The user did provide an answer
2198 // 4. We are not reviewing with an incorrect answer (and not reviewing an essay question)
2199
2200 $result->nodefaultresponse = true; // This will cause a redirect below
2201 } else if ($result->isessayquestion) {
2202 $result->response = get_string('defaultessayresponse', 'lesson');
2203 } else if ($result->correctanswer) {
2204 $result->response = get_string('thatsthecorrectanswer', 'lesson');
2205 } else {
2206 $result->response = get_string('thatsthewronganswer', 'lesson');
2207 }
2208 }
2209
2210 if ($result->response) {
2211 if ($this->lesson->review && !$result->correctanswer && !$result->isessayquestion) {
2212 $nretakes = $DB->count_records("lesson_grades", array("lessonid"=>$this->lesson->id, "userid"=>$USER->id));
2213 $qattempts = $DB->count_records("lesson_attempts", array("userid"=>$USER->id, "retry"=>$nretakes, "pageid"=>$this->properties->id));
2214 if ($qattempts == 1) {
2215 $result->feedback = get_string("firstwrong", "lesson");
2216 } else {
2217 $result->feedback = get_string("secondpluswrong", "lesson");
2218 }
2219 } else {
2220 $class = 'response';
2221 if ($result->correctanswer) {
2222 $class .= ' correct'; //CSS over-ride this if they exist (!important)
2223 } else if (!$result->isessayquestion) {
2224 $class .= ' incorrect'; //CSS over-ride this if they exist (!important)
2225 }
2226 $options = new stdClass;
2227 $options->noclean = true;
2228 $options->para = true;
2229 $result->feedback = $OUTPUT->box(format_text($this->properties->contents, FORMAT_MOODLE, $options), 'generalbox boxaligncenter');
2230 $result->feedback .= '<em>'.get_string("youranswer", "lesson").'</em> : '.format_text($result->studentanswer, FORMAT_MOODLE, $options);
2231 $result->feedback .= $OUTPUT->box(format_text($result->response, FORMAT_MOODLE, $options), $class);
2232 }
2233 }
2234 }
2235
2236 return $result;
2237 }
2238
2239 /**
2240 * Returns the string for a jump name
2241 *
2242 * @final
2243 * @param int $jumpto Jump code or page ID
2244 * @return string
2245 **/
2246 final protected function get_jump_name($jumpto) {
2247 global $DB;
2248 static $jumpnames = array();
2249
2250 if (!array_key_exists($jumpto, $jumpnames)) {
2251 if ($jumpto == 0) {
2252 $jumptitle = get_string('thispage', 'lesson');
2253 } elseif ($jumpto == LESSON_NEXTPAGE) {
2254 $jumptitle = get_string('nextpage', 'lesson');
2255 } elseif ($jumpto == LESSON_EOL) {
2256 $jumptitle = get_string('endoflesson', 'lesson');
2257 } elseif ($jumpto == LESSON_UNSEENBRANCHPAGE) {
2258 $jumptitle = get_string('unseenpageinbranch', 'lesson');
2259 } elseif ($jumpto == LESSON_PREVIOUSPAGE) {
2260 $jumptitle = get_string('previouspage', 'lesson');
2261 } elseif ($jumpto == LESSON_RANDOMPAGE) {
2262 $jumptitle = get_string('randompageinbranch', 'lesson');
2263 } elseif ($jumpto == LESSON_RANDOMBRANCH) {
2264 $jumptitle = get_string('randombranch', 'lesson');
2265 } elseif ($jumpto == LESSON_CLUSTERJUMP) {
2266 $jumptitle = get_string('clusterjump', 'lesson');
2267 } else {
2268 if (!$jumptitle = $DB->get_field('lesson_pages', 'title', array('id' => $jumpto))) {
2269 $jumptitle = '<strong>'.get_string('notdefined', 'lesson').'</strong>';
2270 }
2271 }
2272 $jumpnames[$jumpto] = format_string($jumptitle,true);
2273 }
2274
2275 return $jumpnames[$jumpto];
2276 }
2277
2278 /**
2279 * Construstor method
2280 * @param object $properties
2281 * @param lesson $lesson
2282 */
2283 public function __construct($properties, lesson $lesson) {
2284 parent::__construct($properties);
2285 $this->lesson = $lesson;
2286 }
2287
2288 /**
2289 * Returns the score for the attempt
2290 * This may be overriden by page types that require manual grading
2291 * @param array $answers
2292 * @param object $attempt
2293 * @return int
2294 */
2295 public function earned_score($answers, $attempt) {
2296 return $answers[$attempt->answerid]->score;
2297 }
2298
2299 /**
2300 * This is a callback method that can be override and gets called when ever a page
2301 * is viewed
2302 *
2303 * @param bool $canmanage True if the user has the manage cap
2304 * @return mixed
2305 */
2306 public function callback_on_view($canmanage) {
2307 return true;
2308 }
2309
2310 /**
2311 * Updates a lesson page and its answers within the database
2312 *
2313 * @param object $properties
2314 * @return bool
2315 */
2316 public function update($properties,$context, $maxbytes) {
2317 global $DB;
2318 $answers = $this->get_answers();
2319 $properties->id = $this->properties->id;
2320 $properties->lessonid = $this->lesson->id;
2321 if (empty($properties->qoption)) {
2322 $properties->qoption = '0';
2323 }
2324 $properties = file_postupdate_standard_editor($properties, 'contents', array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$maxbytes), $context, 'lesson_page_contents', $properties->id);
2325 $DB->update_record("lesson_pages", $properties);
2326
2327 for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2328 if (!array_key_exists($i, $this->answers)) {
2329 $this->answers[$i] = new stdClass;
2330 $this->answers[$i]->lessonid = $this->lesson->id;
2331 $this->answers[$i]->pageid = $this->id;
2332 $this->answers[$i]->timecreated = $this->timecreated;
2333 }
2334 if (!empty($properties->answer[$i])) {
2335 $this->answers[$i]->answer = format_text($properties->answer[$i], FORMAT_PLAIN);
2336 if (isset($properties->response[$i])) {
2337 $this->answers[$i]->response = format_text($properties->response[$i], FORMAT_PLAIN);
2338 }
2339 if (isset($properties->jumpto[$i])) {
2340 $this->answers[$i]->jumpto = $properties->jumpto[$i];
2341 }
2342 if ($this->lesson->custom && isset($properties->score[$i])) {
2343 $this->answers[$i]->score = $properties->score[$i];
2344 }
2345 if (!isset($this->answers[$i]->id)) {
2346 $this->answers[$i]->id = $DB->insert_record("lesson_answers", $this->answers[$i]);
2347 } else {
2348 $DB->update_record("lesson_answers", $this->answers[$i]->properties());
2349 }
2350
2351 } else {
2352 break;
2353 }
2354 }
2355 return true;
2356 }
2357
2358 /**
2359 * Can be set to true if the page requires a static link to create a new instance
2360 * instead of simply being included in the dropdown
2361 * @param int $previd
2362 * @return bool
2363 */
2364 public function add_page_link($previd) {
2365 return false;
2366 }
2367
2368 /**
2369 * Returns true if a page has been viewed before
2370 *
2371 * @param array|int $param Either an array of pages that have been seen or the
2372 * number of retakes a user has had
2373 * @return bool
2374 */
2375 public function is_unseen($param) {
2376 global $USER, $DB;
2377 if (is_array($param)) {
2378 $seenpages = $param;
2379 return (!array_key_exists($this->properties->id, $seenpages));
2380 } else {
2381 $nretakes = $param;
2382 if (!$DB->count_records("lesson_attempts", array("pageid"=>$this->properties->id, "userid"=>$USER->id, "retry"=>$nretakes))) {
2383 return true;
2384 }
2385 }
2386 return false;
2387 }
2f67a9b3 2388
0a4abb73
SH
2389 /**
2390 * Checks to see if a page has been answered previously
2391 * @param int $nretakes
2392 * @return bool
2393 */
2394 public function is_unanswered($nretakes) {
2395 global $DB, $USER;
2396 if (!$DB->count_records("lesson_attempts", array('pageid'=>$this->properties->id, 'userid'=>$USER->id, 'correct'=>1, 'retry'=>$nretakes))) {
2397 return true;
2398 }
2399 return false;
2400 }
2401
2402 /**
2403 * Creates answers within the database for this lesson_page. Usually only ever
2404 * called when creating a new page instance
2405 * @param object $properties
2406 * @return array
2407 */
2408 public function create_answers($properties) {
2409 global $DB;
2410 // now add the answers
2411 $newanswer = new stdClass;
2412 $newanswer->lessonid = $this->lesson->id;
2413 $newanswer->pageid = $this->properties->id;
2414 $newanswer->timecreated = $this->properties->timecreated;
2415
2416 $answers = array();
2417
2418 for ($i = 0; $i < $this->lesson->maxanswers; $i++) {
2419 $answer = clone($newanswer);
2420 if (!empty($properties->answer[$i])) {
2421 $answer->answer = format_text($properties->answer[$i], FORMAT_PLAIN);
2422 if (isset($properties->response[$i])) {
2423 $answer->response = format_text($properties->response[$i], FORMAT_PLAIN);
2424 }
2425 if (isset($properties->jumpto[$i])) {
2426 $answer->jumpto = $properties->jumpto[$i];
2427 }
2428 if ($this->lesson->custom && isset($properties->score[$i])) {
2429 $answer->score = $properties->score[$i];
2430 }
2431 $answer->id = $DB->insert_record("lesson_answers", $answer);
2432 $answers[$answer->id] = new lesson_page_answer($answer);
2433 } else {
2434 break;
2435 }
2436 }
2437
2438 $this->answers = $answers;
2439 return $answers;
2440 }
2f67a9b3 2441
0a4abb73
SH
2442 /**
2443 * This method MUST be overriden by all question page types, or page types that
2444 * wish to score a page.
2445 *
2446 * The structure of result should always be the same so it is a good idea when
2447 * overriding this method on a page type to call
2448 * <code>
2449 * $result = parent::check_answer();
2450 * </code>
2451 * before modifiying it as required.
2f67a9b3 2452 *
0a4abb73
SH
2453 * @return stdClass
2454 */
2455 public function check_answer() {
2456 $result = new stdClass;
2457 $result->answerid = 0;
2458 $result->noanswer = false;
2459 $result->correctanswer = false;
2460 $result->isessayquestion = false; // use this to turn off review button on essay questions
2461 $result->response = '';
2462 $result->newpageid = 0; // stay on the page
2463 $result->studentanswer = ''; // use this to store student's answer(s) in order to display it on feedback page
2464 $result->userresponse = null;
2465 $result->feedback = '';
2466 $result->nodefaultresponse = false; // Flag for redirecting when default feedback is turned off
2467 return $result;
2468 }
2469
2470 /**
2471 * True if the page uses a custom option
2f67a9b3 2472 *
0a4abb73 2473 * Should be override and set to true if the page uses a custom option.
2f67a9b3 2474 *
0a4abb73
SH
2475 * @return bool
2476 */
2477 public function has_option() {
2478 return false;
2479 }
2f67a9b3 2480
0a4abb73
SH
2481 /**
2482 * Returns the maximum number of answers for this page given the maximum number
2483 * of answers permitted by the lesson.
2484 *
2485 * @param int $default
2486 * @return int
2487 */
2488 public function max_answers($default) {
2489 return $default;
2490 }
2491
2492 /**
2493 * Returns the properties of this lesson page as an object
2494 * @return stdClass;
2495 */
2496 public function properties() {
2497 $properties = clone($this->properties);
2498 if ($this->answers === null) {
2499 $this->get_answers();
2500 }
2501 if (count($this->answers)>0) {
2502 $count = 0;
2503 foreach ($this->answers as $answer) {
2504 $properties->{'answer['.$count.']'} = $answer->answer;
2505 $properties->{'response['.$count.']'} = $answer->response;
2506 $properties->{'jumpto['.$count.']'} = $answer->jumpto;
2507 $properties->{'score['.$count.']'} = $answer->score;
2508 $count++;
2509 }
2510 }
2511 return $properties;
2512 }
2f67a9b3 2513
0a4abb73
SH
2514 /**
2515 * Returns an array of options to display whn choosing the jumpto for a page/answer
2516 * @static
2517 * @param int $pageid
2518 * @param lesson $lesson
2519 * @return array
2520 */
2521 public static function get_jumptooptions($pageid, lesson $lesson) {
2522 global $DB;
2523 $jump = array();
2524 $jump[0] = get_string("thispage", "lesson");
2525 $jump[LESSON_NEXTPAGE] = get_string("nextpage", "lesson");
2526 $jump[LESSON_PREVIOUSPAGE] = get_string("previouspage", "lesson");
2527 $jump[LESSON_EOL] = get_string("endoflesson", "lesson");
2528
2529 if ($pageid == 0) {
2530 return $jump;
2531 }
2f67a9b3 2532
0a4abb73
SH
2533 $pages = $lesson->load_all_pages();
2534 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))) {
2535 $jump[LESSON_UNSEENBRANCHPAGE] = get_string("unseenpageinbranch", "lesson");
2536 $jump[LESSON_RANDOMPAGE] = get_string("randompageinbranch", "lesson");
2537 }
2538 if($pages[$pageid]->qtype == LESSON_PAGE_CLUSTER || $lesson->is_sub_page_of_type($pageid, array(LESSON_PAGE_CLUSTER), array(LESSON_PAGE_ENDOFCLUSTER))) {
2539 $jump[LESSON_CLUSTERJUMP] = get_string("clusterjump", "lesson");
2540 }
2541 if (!optional_param('firstpage', 0, PARAM_INT)) {
2542 $apageid = $DB->get_field("lesson_pages", "id", array("lessonid" => $lesson->id, "prevpageid" => 0));
2543 while (true) {
2544 if ($apageid) {
2545 $title = $DB->get_field("lesson_pages", "title", array("id" => $apageid));
2546 $jump[$apageid] = strip_tags(format_string($title,true));
2547 $apageid = $DB->get_field("lesson_pages", "nextpageid", array("id" => $apageid));
2548 } else {
2549 // last page reached
2550 break;
2551 }
2552 }
2553 }
2554 return $jump;
2555 }
2556 /**
2557 * Returns the contents field for the page properly formatted and with plugin
2558 * file url's converted
2559 * @return string
2560 */
2561 public function get_contents() {
2562 global $PAGE;
2563 if (!empty($this->properties->contents)) {
2564 if (!isset($this->properties->contentsformat)) {
2565 $this->properties->contentsformat = FORMAT_HTML;
2566 }
2567 $context = get_context_instance(CONTEXT_MODULE, $PAGE->cm->id);
2568 return file_rewrite_pluginfile_urls($this->properties->contents, 'pluginfile.php', $context->id, 'lesson_page_contents', $this->properties->id);
2569 } else {
2570 return '';
2571 }
2572 }
2573
2574 /**
2575 * Set to true if this page should display in the menu block
2576 * @return bool
2577 */
2578 protected function get_displayinmenublock() {
2579 return false;
2580 }
2581
2582 /**
2583 * Get the string that describes the options of this page type
2584 * @return string
2585 */
2586 public function option_description_string() {
2587 return '';
2588 }
2589
2590 /**
2591 * Updates a table with the answers for this page
2592 * @param html_table $table
2593 * @return html_table
2594 */
2595 public function display_answers(html_table $table) {
2596 $answers = $this->get_answers();
2597 $i = 1;
2598 foreach ($answers as $answer) {
2599 $cells = array();
2600 $cells[] = "<span class=\"label\">".get_string("jump", "lesson")." $i<span>: ";
2601 $cells[] = $this->get_jump_name($answer->jumpto);
2602 $table->data[] = html_table_row::make($cells);
2603 if ($i === 1){
2604 $table->data[count($table->data)-1]->cells[0]->style = 'width:20%;';
2605 }
2606 $i++;
2607 }
2608 return $table;
2609 }
2610
2611 /**
2612 * Determines if this page should be grayed out on the management/report screens
2613 * @return int 0 or 1
2614 */
2615 protected function get_grayout() {
2616 return 0;
2617 }
2618
2619 /**
2620 * Adds stats for this page to the &pagestats object. This should be defined
2621 * for all page types that grade
2622 * @param array $pagestats
2623 * @param int $tries
2624 * @return bool
2625 */
2626 public function stats(array &$pagestats, $tries) {
2627 return true;
2628 }
2629
2630 /**
2631 * Formats the answers of this page for a report
2632 *
2633 * @param object $answerpage
2634 * @param object $answerdata
2635 * @param object $useranswer
2636 * @param array $pagestats
2637 * @param int $i Count of first level answers
2638 * @param int $n Count of second level answers
2639 * @return object The answer page for this
2640 */
2641 public function report_answers($answerpage, $answerdata, $useranswer, $pagestats, &$i, &$n) {
2642 $answers = $this->get_answers();
2643 $formattextdefoptions = new stdClass;
2644 $formattextdefoptions->para = false; //I'll use it widely in this page
2645 foreach ($answers as $answer) {
2646 $data = get_string('jumpsto', 'lesson', $this->get_jump_name($answer->jumpto));
2647 $answerdata->answers[] = array($data, "");
2648 $answerpage->answerdata = $answerdata;
2649 }
2650 return $answerpage;
2651 }
2652
2653 /**
2654 * Gets an array of the jumps used by the answers of this page
2655 *
2656 * @return array
2657 */
2658 public function get_jumps() {
2659 global $DB;
2660 $jumps = array();
2661 $params = array ("lessonid" => $this->lesson->id, "pageid" => $this->properties->id);
2662 if ($answers = $this->get_answers()) {
2663 foreach ($answers as $answer) {
2664 $jumps[] = $this->get_jump_name($answer->jumpto);
2665 }
2666 }
2667 return $jumps;
2668 }
2669 /**
2670 * Informs whether this page type require manual grading or not
2671 * @return bool
2672 */
2673 public function requires_manual_grading() {
2674 return false;
2675 }
2676
2677 /**
2678 * A callback method that allows a page to override the next page a user will
2679 * see during when this page is being completed.
2680 * @return false|int
2681 */
2682 public function override_next_page() {
2683 return false;
2684 }
2685
2686 /**
2687 * This method is used to determine if this page is a valid page
2688 *
2689 * @param array $validpages
2690 * @param array $pageviews
2691 * @return int The next page id to check
2692 */
2693 public function valid_page_and_view(&$validpages, &$pageviews) {
2694 $validpages[$this->properties->id] = 1;
2695 return $this->properties->nextpageid;
2696 }
2697}
2698
2699/**
2700 * Class used to represent an answer to a page
2701 *
2702 * @property int $id The ID of this answer in the database
2703 * @property int $lessonid The ID of the lesson this answer belongs to
2704 * @property int $pageid The ID of the page this answer belongs to
2705 * @property int $jumpto Identifies where the user goes upon completing a page with this answer
2706 * @property int $grade The grade this answer is worth
2707 * @property int $score The score this answer will give
2708 * @property int $flags Used to store options for the answer
2709 * @property int $timecreated A timestamp of when the answer was created
2710 * @property int $timemodified A timestamp of when the answer was modified
2711 * @property string $answer The answer itself
2712 * @property string $response The response the user sees if selecting this answer
2f67a9b3 2713 *
0a4abb73
SH
2714 * @copyright 2009 Sam Hemelryk
2715 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2716 */
2717class lesson_page_answer extends lesson_base {
2718
2719 /**
2720 * Loads an page answer from the DB
2721 *
2722 * @param int $id
2723 * @return lesson_page_answer
2724 */
2725 public static function load($id) {
2726 global $DB;
2727 $answer = $DB->get_record("lesson_answers", array("id" => $id));
2728 return new lesson_page_answer($answer);
2729 }
2730
2731 /**
2732 * Given an object of properties and a page created answer(s) and saves them
2733 * in the database.
2734 *
2735 * @param stdClass $properties
2736 * @param lesson_page $page
2737 * @return array
2738 */
2739 public static function create($properties, lesson_page $page) {
2740 return $page->create_answers($properties);
2741 }
2742
2743}
2744
2745/**
2746 * A management class for page types
2747 *
2748 * This class is responsible for managing the different pages. A manager object can
2749 * be retrieved by calling the following line of code:
2750 * <code>
2751 * $manager = lesson_page_type_manager::get($lesson);
2752 * </code>
2753 * The first time the page type manager is retrieved the it includes all of the
2754 * different page types located in mod/lesson/pagetypes.
2755 *
2756 * @copyright 2009 Sam Hemelryk
2757 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2758 */
2759class lesson_page_type_manager {
2760
2761 /**
2762 * An array of different page type classes
2763 * @var array
2764 */
2765 protected $types = array();
2766
2767 /**
2768 * Retrieves the lesson page type manager object
2769 *
2770 * If the object hasn't yet been created it is created here.
2771 *
2772 * @staticvar lesson_page_type_manager $pagetypemanager
2773 * @param lesson $lesson
2774 * @return lesson_page_type_manager
2775 */
2776 public static function get(lesson $lesson) {
2777 static $pagetypemanager;
2778 if (!($pagetypemanager instanceof lesson_page_type_manager)) {
2779 $pagetypemanager = new lesson_page_type_manager();
2780 $pagetypemanager->load_lesson_types($lesson);
2781 }
2782 return $pagetypemanager;
2783 }
2784
2785 /**
2786 * Finds and loads all lesson page types in mod/lesson/pagetypes
2f67a9b3 2787 *
0a4abb73
SH
2788 * @param lesson $lesson
2789 */
2790 public function load_lesson_types(lesson $lesson) {
2791 global $CFG;
2792 $basedir = $CFG->dirroot.'/mod/lesson/pagetypes/';
2793 $dir = dir($basedir);
2794 while (false !== ($entry = $dir->read())) {
2795 if (strpos($entry, '.')===0 || !preg_match('#^[a-zA-Z]+\.php#i', $entry)) {
2796 continue;
2797 }
2798 require_once($basedir.$entry);
2799 $class = 'lesson_page_type_'.strtok($entry,'.');
2800 if (class_exists($class)) {
2801 $pagetype = new $class(new stdClass, $lesson);
2802 $this->types[$pagetype->typeid] = $pagetype;
2803 }
2804 }
2f67a9b3 2805
0a4abb73
SH
2806 }
2807
2808 /**
2809 * Returns an array of strings to describe the loaded page types
2810 *
2811 * @param int $type Can be used to return JUST the string for the requested type
2812 * @return array
2813 */
2814 public function get_page_type_strings($type=null, $special=true) {
2815 $types = array();
2816 foreach ($this->types as $pagetype) {
2817 if (($type===null || $pagetype->type===$type) && ($special===true || $pagetype->is_standard())) {
2818 $types[$pagetype->typeid] = $pagetype->typestring;
2819 }
2820 }
2821 return $types;
2822 }
2823
2824 /**
2825 * Returns the basic string used to identify a page type provided with an id
2826 *
2827 * This string can be used to instantiate or identify the page type class.
2828 * If the page type id is unknown then 'unknown' is returned
2829 *
2830 * @param int $id
2831 * @return string
2832 */
2833 public function get_page_type_idstring($id) {
2834 foreach ($this->types as $pagetype) {
2835 if ((int)$pagetype->typeid === (int)$id) {
2836 return $pagetype->idstring;
2837 }
2838 }
2839 return 'unknown';
2840 }
2841
2842 /**
2843 * Loads a page for the provided lesson given it's id
2844 *
2845 * This function loads a page from the lesson when given both the lesson it belongs
2846 * to as well as the page's id.
2847 * If the page doesn't exist an error is thrown
2848 *
2849 * @param int $pageid The id of the page to load
2850 * @param lesson $lesson The lesson the page belongs to
2851 * @return lesson_page A class that extends lesson_page
2852 */
2853 public function load_page($pageid, lesson $lesson) {
2854 global $DB;
2855 if (!($page =$DB->get_record('lesson_pages', array('id'=>$pageid, 'lessonid'=>$lesson->id)))) {
2856 print_error('cannotfindpages', 'lesson');
2857 }
2858 $pagetype = get_class($this->types[$page->qtype]);
2859 $page = new $pagetype($page, $lesson);
2860 return $page;
2861 }
2862
2863 /**
2864 * This function loads ALL pages that belong to the lesson.
2865 *
2866 * @param lesson $lesson
2867 * @return array An array of lesson_page_type_*
2868 */
2869 public function load_all_pages(lesson $lesson) {
2870 global $DB;
2871 if (!($pages =$DB->get_records('lesson_pages', array('lessonid'=>$lesson->id)))) {
2872 print_error('cannotfindpages', 'lesson');
2873 }
2874 foreach ($pages as $key=>$page) {
2875 $pagetype = get_class($this->types[$page->qtype]);
2876 $pages[$key] = new $pagetype($page, $lesson);
2877 }
2878
2879 $orderedpages = array();
2880 $lastpageid = 0;
2881
2882 while (true) {
2883 foreach ($pages as $page) {
2884 if ((int)$page->prevpageid === (int)$lastpageid) {
2885 $orderedpages[$page->id] = $page;
2886 unset($pages[$page->id]);
2887 $lastpageid = $page->id;
2888 if ((int)$page->nextpageid===0) {
2889 break 2;
2890 } else {
2891 break 1;
2892 }
2893 }
2894 }
2895 }
2896
2897 return $orderedpages;
2898 }
2899
2900 /**
2901 * Fetchs an mform that can be used to create/edit an page
2902 *
2903 * @param int $type The id for the page type
2904 * @param array $arguments Any arguments to pass to the mform
2905 * @return lesson_add_page_form_base
2906 */
2907 public function get_page_form($type, $arguments) {
2908 $class = 'lesson_add_page_form_'.$this->get_page_type_idstring($type);
2909 if (!class_exists($class) || get_parent_class($class)!=='lesson_add_page_form_base') {
2910 debugging('Lesson page type unknown class requested '.$class, DEBUG_DEVELOPER);
2911 $class = 'lesson_add_page_form_selection';
2912 } else if ($class === 'lesson_add_page_form_unknown') {
2913 $class = 'lesson_add_page_form_selection';
2914 }
2915 return new $class(null, $arguments);
2916 }
2917
2918 /**
2919 * Returns an array of links to use as add page links
2920 * @param int $previd The id of the previous page
2921 * @return array
2922 */
2923 public function get_add_page_type_links($previd) {
227255b8
PS
2924 global $OUTPUT;
2925
0a4abb73
SH
2926 $links = array();
2927
2928 foreach ($this->types as $type) {
2929 if (($link = $type->add_page_link($previd)) instanceof html_link) {
227255b8 2930 $links[] = $OUTPUT->link($link);
0a4abb73
SH
2931 }
2932 }
2933
2934 return $links;
2935 }
2936}
2937
2938/**
2939 * Abstract class that page type's MUST inherit from.
2940 *
2941 * This is the abstract class that ALL add page type forms must extend.
2942 * You will notice that all but two of the methods this class contains are final.
2943 * Essentially the only thing that extending classes can do is extend custom_definition.
2944 * OR if it has a special requirement on creation it can extend construction_override
2945 *
2946 * @abstract
2947 * @copyright 2009 Sam Hemelryk
2948 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2949 */
2950abstract class lesson_add_page_form_base extends moodleform {
2951
2952 /**
2953 * This is the classic define that is used to identify this pagetype.
2954 * Will be one of LESSON_*
2955 * @var int
2956 */
2957 public $qtype;
2958
2959 /**
2960 * The simple string that describes the page type e.g. truefalse, multichoice
2961 * @var string
2962 */
2963 public $qtypestring;
2964
2965 /**
2966 * An array of options used in the htmleditor
2967 * @var array
2968 */
2969 protected $editoroptions = array();
2970
2971 /**
2972 * True if this is a standard page of false if it does something special.
2973 * Questions are standard pages, branch tables are not
2974 * @var bool
2975 */
2976 protected $standard = true;
2977
2978 /**
2979 * Each page type can and should override this to add any custom elements to
2980 * the basic form that they want
2981 */
2982 public function custom_definition() {}
2983
2984 /**
2985 * Sets the data for the form... but modifies if first for the editor then
2986 * calls the parent method
2987 *
2988 * @param stdClass $data An object containing properties to set
2989 * @param int $pageid
2990 */
2991 public final function set_data($data, $context=null, $pageid=null) {
2992 $data = file_prepare_standard_editor($data, 'contents', $this->editoroptions, $context, 'lesson_page_contents', $pageid);
2993 parent::set_data($data);
2994 }
2995
2996 /**
2997 * Used to determine if this is a standard page or a special page
2998 * @return bool
2999 */
3000 public final function is_standard() {
3001 return (bool)$this->standard;
3002 }
3003
3004 /**
3005 * Add the required basic elements to the form.
3006 *
3007 * This method adds the basic elements to the form including title and contents
3008 * and then calls custom_definition();
3009 */
3010 public final function definition() {
3011 $mform = $this->_form;
3012 $editoroptions = $this->_customdata['editoroptions'];
3013
3014 $mform->addElement('header', 'qtypeheading', get_string('addaquestionpage', 'lesson', get_string($this->qtypestring, 'lesson')));
3015
3016 $mform->addElement('hidden', 'id');
3017 $mform->setType('id', PARAM_INT);
3018
3019 $mform->addElement('hidden', 'pageid');
3020 $mform->setType('pageid', PARAM_INT);
3021
3022 if ($this->standard === true) {
3023 $mform->addElement('hidden', 'qtype');
3024 $mform->setType('qtype', PARAM_TEXT);
3025
3026 $mform->addElement('text', 'title', get_string("pagetitle", "lesson"), array('size'=>70));
3027 $mform->setType('title', PARAM_TEXT);
3028 $this->editoroptions = array('noclean'=>true, 'maxfiles'=>EDITOR_UNLIMITED_FILES, 'maxbytes'=>$this->_customdata['maxbytes']);
3029 $mform->addElement('editor', 'contents_editor', get_string("pagecontents", "lesson"), null, $this->editoroptions);
3030 $mform->setType('contents_editor', PARAM_CLEANHTML);
3031 }
3032
3033 $this->custom_definition();
3034
3035 if ($this->_customdata['edit'] === true) {
3036 $mform->addElement('hidden', 'edit', 1);
3037 $this->add_action_buttons(get_string('cancel'), get_string("savepage", "lesson"));
3038 } else {
3039 $this->add_action_buttons(get_string('cancel'), get_string("addaquestionpage", "lesson"));
3040 }
3041 }
3042
3043 /**
3044 * Convenience function: Adds a jumpto select element
3045 *
3046 * @param string $name
3047 * @param string|null $label
3048 * @param int $selected The page to select by default
3049 */
3050 protected final function add_jumpto($name, $label=null, $selected=LESSON_NEXTPAGE) {
3051 $title = get_string("jump", "lesson");
3052 if ($label === null) {
3053 $label = $title;
3054 }
3055 if (is_int($name)) {
3056 $name = "jumpto[$name]";
3057 }
3058 $this->_form->addElement('select', $name, $label, $this->_customdata['jumpto']);
3059 $this->_form->setDefault($name, $selected);
3060 $this->_form->setHelpButton($name, array("jumpto", $title, "lesson"));
3061 }
3062
3063 /**
3064 * Convenience function: Adds a score input element
3065 *
3066 * @param string $name
3067 * @param string|null $label
3068 * @param mixed $value The default value
3069 */
3070 protected final function add_score($name, $label=null, $value=null) {
3071 if ($label === null) {
3072 $label = get_string("score", "lesson");
3073 }
3074 if (is_int($name)) {
3075 $name = "score[$name]";
3076 }
3077 $this->_form->addElement('text', $name, $label, array('size'=>5));
3078 if ($value !== null) {
3079 $this->_form->setDefault($name, $value);
3080 }
3081 }
3082
3083 /**
3084 * Convenience function: Adds a textarea element
3085 *
3086 * @param string $name
3087 * @param int $count The count of the element to add
3088 * @param string|null $label
3089 */
3090 protected final function add_textarea($name, $count, $label) {
3091 $this->_form->addElement('textarea', $name.'['.$count.']', $label, array('rows'=>5, 'cols'=>70, 'width'=>630, 'height'=>300));
3092 }
3093 /**
3094 * Convenience function: Adds an answer textarea
3095 *
3096 * @param int $count The count of the element to add
3097 */
3098 protected final function add_answer($count) {
3099 $this->add_textarea('answer', $count, get_string('answer', 'lesson'));
3100 }
3101 /**
3102 * Convenience function: Adds an response textarea
3103 *
3104 * @param int $count The count of the element to add
3105 */
3106 protected final function add_response($count) {
3107 $this->add_textarea('response', $count, get_string('response', 'lesson'));
3108 }
3109
3110 /**
3111 * A function that gets called upon init of this object by the calling script.
3112 *
3113 * This can be used to process an immediate action if required. Currently it
3114 * is only used in special cases by non-standard page types.
3115 *
3116 * @return bool
3117 */
3118 public function construction_override() {
3119 return true;
3120 }
3121}