Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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