Proyectos de Subversion Moodle

Rev

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