1 |
efrain |
1 |
<?php
|
|
|
2 |
// This file is part of Moodle - http://moodle.org/
|
|
|
3 |
//
|
|
|
4 |
// Moodle is free software: you can redistribute it and/or modify
|
|
|
5 |
// it under the terms of the GNU General Public License as published by
|
|
|
6 |
// the Free Software Foundation, either version 3 of the License, or
|
|
|
7 |
// (at your option) any later version.
|
|
|
8 |
//
|
|
|
9 |
// Moodle is distributed in the hope that it will be useful,
|
|
|
10 |
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
11 |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
12 |
// GNU General Public License for more details.
|
|
|
13 |
//
|
|
|
14 |
// You should have received a copy of the GNU General Public License
|
|
|
15 |
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
|
|
16 |
|
|
|
17 |
/**
|
|
|
18 |
* @package mod_scorm
|
|
|
19 |
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
|
|
20 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
21 |
*/
|
|
|
22 |
defined('MOODLE_INTERNAL') || die();
|
|
|
23 |
|
|
|
24 |
/** SCORM_TYPE_LOCAL = local */
|
|
|
25 |
define('SCORM_TYPE_LOCAL', 'local');
|
|
|
26 |
/** SCORM_TYPE_LOCALSYNC = localsync */
|
|
|
27 |
define('SCORM_TYPE_LOCALSYNC', 'localsync');
|
|
|
28 |
/** SCORM_TYPE_EXTERNAL = external */
|
|
|
29 |
define('SCORM_TYPE_EXTERNAL', 'external');
|
|
|
30 |
/** SCORM_TYPE_AICCURL = external AICC url */
|
|
|
31 |
define('SCORM_TYPE_AICCURL', 'aiccurl');
|
|
|
32 |
|
|
|
33 |
define('SCORM_TOC_SIDE', 0);
|
|
|
34 |
define('SCORM_TOC_HIDDEN', 1);
|
|
|
35 |
define('SCORM_TOC_POPUP', 2);
|
|
|
36 |
define('SCORM_TOC_DISABLED', 3);
|
|
|
37 |
|
|
|
38 |
// Used to show/hide navigation buttons and set their position.
|
|
|
39 |
define('SCORM_NAV_DISABLED', 0);
|
|
|
40 |
define('SCORM_NAV_UNDER_CONTENT', 1);
|
|
|
41 |
define('SCORM_NAV_FLOATING', 2);
|
|
|
42 |
|
|
|
43 |
// Used to check what SCORM version is being used.
|
|
|
44 |
define('SCORM_12', 1);
|
|
|
45 |
define('SCORM_13', 2);
|
|
|
46 |
define('SCORM_AICC', 3);
|
|
|
47 |
|
|
|
48 |
// List of possible attemptstatusdisplay options.
|
|
|
49 |
define('SCORM_DISPLAY_ATTEMPTSTATUS_NO', 0);
|
|
|
50 |
define('SCORM_DISPLAY_ATTEMPTSTATUS_ALL', 1);
|
|
|
51 |
define('SCORM_DISPLAY_ATTEMPTSTATUS_MY', 2);
|
|
|
52 |
define('SCORM_DISPLAY_ATTEMPTSTATUS_ENTRY', 3);
|
|
|
53 |
|
|
|
54 |
define('SCORM_EVENT_TYPE_OPEN', 'open');
|
|
|
55 |
define('SCORM_EVENT_TYPE_CLOSE', 'close');
|
|
|
56 |
|
|
|
57 |
require_once(__DIR__ . '/deprecatedlib.php');
|
|
|
58 |
|
|
|
59 |
/**
|
|
|
60 |
* Return an array of status options
|
|
|
61 |
*
|
|
|
62 |
* Optionally with translated strings
|
|
|
63 |
*
|
|
|
64 |
* @param bool $with_strings (optional)
|
|
|
65 |
* @return array
|
|
|
66 |
*/
|
|
|
67 |
function scorm_status_options($withstrings = false) {
|
|
|
68 |
// Id's are important as they are bits.
|
|
|
69 |
$options = array(
|
|
|
70 |
2 => 'passed',
|
|
|
71 |
4 => 'completed'
|
|
|
72 |
);
|
|
|
73 |
|
|
|
74 |
if ($withstrings) {
|
|
|
75 |
foreach ($options as $key => $value) {
|
|
|
76 |
$options[$key] = get_string('completionstatus_'.$value, 'scorm');
|
|
|
77 |
}
|
|
|
78 |
}
|
|
|
79 |
|
|
|
80 |
return $options;
|
|
|
81 |
}
|
|
|
82 |
|
|
|
83 |
|
|
|
84 |
/**
|
|
|
85 |
* Given an object containing all the necessary data,
|
|
|
86 |
* (defined by the form in mod_form.php) this function
|
|
|
87 |
* will create a new instance and return the id number
|
|
|
88 |
* of the new instance.
|
|
|
89 |
*
|
|
|
90 |
* @global stdClass
|
|
|
91 |
* @global object
|
|
|
92 |
* @uses CONTEXT_MODULE
|
|
|
93 |
* @uses SCORM_TYPE_LOCAL
|
|
|
94 |
* @uses SCORM_TYPE_LOCALSYNC
|
|
|
95 |
* @uses SCORM_TYPE_EXTERNAL
|
|
|
96 |
* @param object $scorm Form data
|
|
|
97 |
* @param object $mform
|
|
|
98 |
* @return int new instance id
|
|
|
99 |
*/
|
|
|
100 |
function scorm_add_instance($scorm, $mform=null) {
|
|
|
101 |
global $CFG, $DB;
|
|
|
102 |
|
|
|
103 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
104 |
|
|
|
105 |
if (empty($scorm->timeopen)) {
|
|
|
106 |
$scorm->timeopen = 0;
|
|
|
107 |
}
|
|
|
108 |
if (empty($scorm->timeclose)) {
|
|
|
109 |
$scorm->timeclose = 0;
|
|
|
110 |
}
|
|
|
111 |
if (empty($scorm->completionstatusallscos)) {
|
|
|
112 |
$scorm->completionstatusallscos = 0;
|
|
|
113 |
}
|
|
|
114 |
$cmid = $scorm->coursemodule;
|
|
|
115 |
$cmidnumber = $scorm->cmidnumber;
|
|
|
116 |
$courseid = $scorm->course;
|
|
|
117 |
|
|
|
118 |
$context = context_module::instance($cmid);
|
|
|
119 |
|
|
|
120 |
$scorm = scorm_option2text($scorm);
|
|
|
121 |
$scorm->width = (int)str_replace('%', '', $scorm->width);
|
|
|
122 |
$scorm->height = (int)str_replace('%', '', $scorm->height);
|
|
|
123 |
|
|
|
124 |
if (!isset($scorm->whatgrade)) {
|
|
|
125 |
$scorm->whatgrade = 0;
|
|
|
126 |
}
|
|
|
127 |
|
|
|
128 |
$id = $DB->insert_record('scorm', $scorm);
|
|
|
129 |
|
|
|
130 |
// Update course module record - from now on this instance properly exists and all function may be used.
|
|
|
131 |
$DB->set_field('course_modules', 'instance', $id, array('id' => $cmid));
|
|
|
132 |
|
|
|
133 |
// Reload scorm instance.
|
|
|
134 |
$record = $DB->get_record('scorm', array('id' => $id));
|
|
|
135 |
|
|
|
136 |
// Store the package and verify.
|
|
|
137 |
if ($record->scormtype === SCORM_TYPE_LOCAL) {
|
|
|
138 |
if (!empty($scorm->packagefile)) {
|
|
|
139 |
$fs = get_file_storage();
|
|
|
140 |
$fs->delete_area_files($context->id, 'mod_scorm', 'package');
|
|
|
141 |
file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package',
|
|
|
142 |
0, array('subdirs' => 0, 'maxfiles' => 1));
|
|
|
143 |
// Get filename of zip that was uploaded.
|
|
|
144 |
$files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
|
|
|
145 |
$file = reset($files);
|
|
|
146 |
$filename = $file->get_filename();
|
|
|
147 |
if ($filename !== false) {
|
|
|
148 |
$record->reference = $filename;
|
|
|
149 |
}
|
|
|
150 |
}
|
|
|
151 |
|
|
|
152 |
} else if ($record->scormtype === SCORM_TYPE_LOCALSYNC) {
|
|
|
153 |
$record->reference = $scorm->packageurl;
|
|
|
154 |
} else if ($record->scormtype === SCORM_TYPE_EXTERNAL) {
|
|
|
155 |
$record->reference = $scorm->packageurl;
|
|
|
156 |
} else if ($record->scormtype === SCORM_TYPE_AICCURL) {
|
|
|
157 |
$record->reference = $scorm->packageurl;
|
|
|
158 |
$record->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it.
|
|
|
159 |
} else {
|
|
|
160 |
return false;
|
|
|
161 |
}
|
|
|
162 |
|
|
|
163 |
// Save reference.
|
|
|
164 |
$DB->update_record('scorm', $record);
|
|
|
165 |
|
|
|
166 |
// Extra fields required in grade related functions.
|
|
|
167 |
$record->course = $courseid;
|
|
|
168 |
$record->cmidnumber = $cmidnumber;
|
|
|
169 |
$record->cmid = $cmid;
|
|
|
170 |
|
|
|
171 |
scorm_parse($record, true);
|
|
|
172 |
|
|
|
173 |
scorm_grade_item_update($record);
|
|
|
174 |
scorm_update_calendar($record, $cmid);
|
|
|
175 |
if (!empty($scorm->completionexpected)) {
|
|
|
176 |
\core_completion\api::update_completion_date_event($cmid, 'scorm', $record, $scorm->completionexpected);
|
|
|
177 |
}
|
|
|
178 |
|
|
|
179 |
return $record->id;
|
|
|
180 |
}
|
|
|
181 |
|
|
|
182 |
/**
|
|
|
183 |
* Given an object containing all the necessary data,
|
|
|
184 |
* (defined by the form in mod_form.php) this function
|
|
|
185 |
* will update an existing instance with new data.
|
|
|
186 |
*
|
|
|
187 |
* @global stdClass
|
|
|
188 |
* @global object
|
|
|
189 |
* @uses CONTEXT_MODULE
|
|
|
190 |
* @uses SCORM_TYPE_LOCAL
|
|
|
191 |
* @uses SCORM_TYPE_LOCALSYNC
|
|
|
192 |
* @uses SCORM_TYPE_EXTERNAL
|
|
|
193 |
* @param object $scorm Form data
|
|
|
194 |
* @param object $mform
|
|
|
195 |
* @return bool
|
|
|
196 |
*/
|
|
|
197 |
function scorm_update_instance($scorm, $mform=null) {
|
|
|
198 |
global $CFG, $DB;
|
|
|
199 |
|
|
|
200 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
201 |
|
|
|
202 |
if (empty($scorm->timeopen)) {
|
|
|
203 |
$scorm->timeopen = 0;
|
|
|
204 |
}
|
|
|
205 |
if (empty($scorm->timeclose)) {
|
|
|
206 |
$scorm->timeclose = 0;
|
|
|
207 |
}
|
|
|
208 |
if (empty($scorm->completionstatusallscos)) {
|
|
|
209 |
$scorm->completionstatusallscos = 0;
|
|
|
210 |
}
|
|
|
211 |
|
|
|
212 |
$cmid = $scorm->coursemodule;
|
|
|
213 |
$cmidnumber = $scorm->cmidnumber;
|
|
|
214 |
$courseid = $scorm->course;
|
|
|
215 |
|
|
|
216 |
$scorm->id = $scorm->instance;
|
|
|
217 |
|
|
|
218 |
$context = context_module::instance($cmid);
|
|
|
219 |
|
|
|
220 |
if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
|
|
|
221 |
if (!empty($scorm->packagefile)) {
|
|
|
222 |
$fs = get_file_storage();
|
|
|
223 |
$fs->delete_area_files($context->id, 'mod_scorm', 'package');
|
|
|
224 |
file_save_draft_area_files($scorm->packagefile, $context->id, 'mod_scorm', 'package',
|
|
|
225 |
0, array('subdirs' => 0, 'maxfiles' => 1));
|
|
|
226 |
// Get filename of zip that was uploaded.
|
|
|
227 |
$files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
|
|
|
228 |
$file = reset($files);
|
|
|
229 |
$filename = $file->get_filename();
|
|
|
230 |
if ($filename !== false) {
|
|
|
231 |
$scorm->reference = $filename;
|
|
|
232 |
}
|
|
|
233 |
}
|
|
|
234 |
|
|
|
235 |
} else if ($scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
|
|
|
236 |
$scorm->reference = $scorm->packageurl;
|
|
|
237 |
} else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL) {
|
|
|
238 |
$scorm->reference = $scorm->packageurl;
|
|
|
239 |
} else if ($scorm->scormtype === SCORM_TYPE_AICCURL) {
|
|
|
240 |
$scorm->reference = $scorm->packageurl;
|
|
|
241 |
$scorm->hidetoc = SCORM_TOC_DISABLED; // TOC is useless for direct AICCURL so disable it.
|
|
|
242 |
} else {
|
|
|
243 |
return false;
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
$scorm = scorm_option2text($scorm);
|
|
|
247 |
$scorm->width = (int)str_replace('%', '', $scorm->width);
|
|
|
248 |
$scorm->height = (int)str_replace('%', '', $scorm->height);
|
|
|
249 |
$scorm->timemodified = time();
|
|
|
250 |
|
|
|
251 |
if (!isset($scorm->whatgrade)) {
|
|
|
252 |
$scorm->whatgrade = 0;
|
|
|
253 |
}
|
|
|
254 |
|
|
|
255 |
$DB->update_record('scorm', $scorm);
|
|
|
256 |
// We need to find this out before we blow away the form data.
|
|
|
257 |
$completionexpected = (!empty($scorm->completionexpected)) ? $scorm->completionexpected : null;
|
|
|
258 |
|
|
|
259 |
$scorm = $DB->get_record('scorm', array('id' => $scorm->id));
|
|
|
260 |
|
|
|
261 |
// Extra fields required in grade related functions.
|
|
|
262 |
$scorm->course = $courseid;
|
|
|
263 |
$scorm->idnumber = $cmidnumber;
|
|
|
264 |
$scorm->cmid = $cmid;
|
|
|
265 |
|
|
|
266 |
scorm_parse($scorm, (bool)$scorm->updatefreq);
|
|
|
267 |
|
|
|
268 |
scorm_grade_item_update($scorm);
|
|
|
269 |
scorm_update_grades($scorm);
|
|
|
270 |
scorm_update_calendar($scorm, $cmid);
|
|
|
271 |
\core_completion\api::update_completion_date_event($cmid, 'scorm', $scorm, $completionexpected);
|
|
|
272 |
|
|
|
273 |
return true;
|
|
|
274 |
}
|
|
|
275 |
|
|
|
276 |
/**
|
|
|
277 |
* Given an ID of an instance of this module,
|
|
|
278 |
* this function will permanently delete the instance
|
|
|
279 |
* and any data that depends on it.
|
|
|
280 |
*
|
|
|
281 |
* @global stdClass
|
|
|
282 |
* @global object
|
|
|
283 |
* @param int $id Scorm instance id
|
|
|
284 |
* @return boolean
|
|
|
285 |
*/
|
|
|
286 |
function scorm_delete_instance($id) {
|
|
|
287 |
global $CFG, $DB;
|
|
|
288 |
|
|
|
289 |
if (! $scorm = $DB->get_record('scorm', array('id' => $id))) {
|
|
|
290 |
return false;
|
|
|
291 |
}
|
|
|
292 |
|
|
|
293 |
$result = true;
|
|
|
294 |
|
|
|
295 |
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
|
|
|
296 |
// Delete any dependent records.
|
|
|
297 |
scorm_delete_tracks($scorm->id);
|
|
|
298 |
if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id))) {
|
|
|
299 |
foreach ($scoes as $sco) {
|
|
|
300 |
if (! $DB->delete_records('scorm_scoes_data', array('scoid' => $sco->id))) {
|
|
|
301 |
$result = false;
|
|
|
302 |
}
|
|
|
303 |
}
|
|
|
304 |
$DB->delete_records('scorm_scoes', array('scorm' => $scorm->id));
|
|
|
305 |
}
|
|
|
306 |
|
|
|
307 |
scorm_grade_item_delete($scorm);
|
|
|
308 |
|
|
|
309 |
// We must delete the module record after we delete the grade item.
|
|
|
310 |
if (! $DB->delete_records('scorm', array('id' => $scorm->id))) {
|
|
|
311 |
$result = false;
|
|
|
312 |
}
|
|
|
313 |
|
|
|
314 |
/*if (! $DB->delete_records('scorm_sequencing_controlmode', array('scormid'=>$scorm->id))) {
|
|
|
315 |
$result = false;
|
|
|
316 |
}
|
|
|
317 |
if (! $DB->delete_records('scorm_sequencing_rolluprules', array('scormid'=>$scorm->id))) {
|
|
|
318 |
$result = false;
|
|
|
319 |
}
|
|
|
320 |
if (! $DB->delete_records('scorm_sequencing_rolluprule', array('scormid'=>$scorm->id))) {
|
|
|
321 |
$result = false;
|
|
|
322 |
}
|
|
|
323 |
if (! $DB->delete_records('scorm_sequencing_rollupruleconditions', array('scormid'=>$scorm->id))) {
|
|
|
324 |
$result = false;
|
|
|
325 |
}
|
|
|
326 |
if (! $DB->delete_records('scorm_sequencing_rolluprulecondition', array('scormid'=>$scorm->id))) {
|
|
|
327 |
$result = false;
|
|
|
328 |
}
|
|
|
329 |
if (! $DB->delete_records('scorm_sequencing_rulecondition', array('scormid'=>$scorm->id))) {
|
|
|
330 |
$result = false;
|
|
|
331 |
}
|
|
|
332 |
if (! $DB->delete_records('scorm_sequencing_ruleconditions', array('scormid'=>$scorm->id))) {
|
|
|
333 |
$result = false;
|
|
|
334 |
}*/
|
|
|
335 |
|
|
|
336 |
return $result;
|
|
|
337 |
}
|
|
|
338 |
|
|
|
339 |
/**
|
|
|
340 |
* Return a small object with summary information about what a
|
|
|
341 |
* user has done with a given particular instance of this module
|
|
|
342 |
* Used for user activity reports.
|
|
|
343 |
*
|
|
|
344 |
* @param stdClass $course Course object
|
|
|
345 |
* @param stdClass $user User
|
|
|
346 |
* @param stdClass $mod
|
|
|
347 |
* @param stdClass $scorm The scorm
|
|
|
348 |
* @return mixed
|
|
|
349 |
*/
|
|
|
350 |
function scorm_user_outline($course, $user, $mod, $scorm) {
|
|
|
351 |
global $CFG;
|
|
|
352 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
353 |
|
|
|
354 |
require_once("$CFG->libdir/gradelib.php");
|
|
|
355 |
$grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id);
|
|
|
356 |
if (!empty($grades->items[0]->grades)) {
|
|
|
357 |
$grade = reset($grades->items[0]->grades);
|
|
|
358 |
$result = (object) [
|
|
|
359 |
'time' => grade_get_date_for_user_grade($grade, $user),
|
|
|
360 |
];
|
|
|
361 |
if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
|
|
|
362 |
$result->info = get_string('gradenoun') . ': '. $grade->str_long_grade;
|
|
|
363 |
} else {
|
|
|
364 |
$result->info = get_string('gradenoun') . ': ' . get_string('hidden', 'grades');
|
|
|
365 |
}
|
|
|
366 |
|
|
|
367 |
return $result;
|
|
|
368 |
}
|
|
|
369 |
return null;
|
|
|
370 |
}
|
|
|
371 |
|
|
|
372 |
/**
|
|
|
373 |
* Print a detailed representation of what a user has done with
|
|
|
374 |
* a given particular instance of this module, for user activity reports.
|
|
|
375 |
*
|
|
|
376 |
* @global stdClass
|
|
|
377 |
* @global object
|
|
|
378 |
* @param object $course
|
|
|
379 |
* @param object $user
|
|
|
380 |
* @param object $mod
|
|
|
381 |
* @param object $scorm
|
|
|
382 |
* @return boolean
|
|
|
383 |
*/
|
|
|
384 |
function scorm_user_complete($course, $user, $mod, $scorm) {
|
|
|
385 |
global $CFG, $DB, $OUTPUT;
|
|
|
386 |
require_once("$CFG->libdir/gradelib.php");
|
|
|
387 |
|
|
|
388 |
$liststyle = 'structlist';
|
|
|
389 |
$now = time();
|
|
|
390 |
$firstmodify = $now;
|
|
|
391 |
$lastmodify = 0;
|
|
|
392 |
$sometoreport = false;
|
|
|
393 |
$report = '';
|
|
|
394 |
|
|
|
395 |
// First Access and Last Access dates for SCOs.
|
|
|
396 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
397 |
$timetracks = scorm_get_sco_runtime($scorm->id, false, $user->id);
|
|
|
398 |
$firstmodify = $timetracks->start;
|
|
|
399 |
$lastmodify = $timetracks->finish;
|
|
|
400 |
|
|
|
401 |
$grades = grade_get_grades($course->id, 'mod', 'scorm', $scorm->id, $user->id);
|
|
|
402 |
if (!empty($grades->items[0]->grades)) {
|
|
|
403 |
$grade = reset($grades->items[0]->grades);
|
|
|
404 |
if (!$grade->hidden || has_capability('moodle/grade:viewhidden', context_course::instance($course->id))) {
|
|
|
405 |
echo $OUTPUT->container(get_string('gradenoun').': '.$grade->str_long_grade);
|
|
|
406 |
if ($grade->str_feedback) {
|
|
|
407 |
echo $OUTPUT->container(get_string('feedback').': '.$grade->str_feedback);
|
|
|
408 |
}
|
|
|
409 |
} else {
|
|
|
410 |
echo $OUTPUT->container(get_string('gradenoun') . ': ' . get_string('hidden', 'grades'));
|
|
|
411 |
}
|
|
|
412 |
}
|
|
|
413 |
|
|
|
414 |
if ($orgs = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.
|
|
|
415 |
$DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
|
|
|
416 |
$DB->sql_isempty('scorm_scoes', 'organization', false, false),
|
|
|
417 |
array($scorm->id), 'sortorder, id', 'id, identifier, title')) {
|
|
|
418 |
if (count($orgs) <= 1) {
|
|
|
419 |
unset($orgs);
|
|
|
420 |
$orgs = array();
|
|
|
421 |
$org = new stdClass();
|
|
|
422 |
$org->identifier = '';
|
|
|
423 |
$orgs[] = $org;
|
|
|
424 |
}
|
|
|
425 |
$report .= html_writer::start_div('mod-scorm');
|
|
|
426 |
foreach ($orgs as $org) {
|
|
|
427 |
$conditions = array();
|
|
|
428 |
$currentorg = '';
|
|
|
429 |
if (!empty($org->identifier)) {
|
|
|
430 |
$report .= html_writer::div($org->title, 'orgtitle');
|
|
|
431 |
$currentorg = $org->identifier;
|
|
|
432 |
$conditions['organization'] = $currentorg;
|
|
|
433 |
}
|
|
|
434 |
$report .= html_writer::start_tag('ul', array('id' => '0', 'class' => $liststyle));
|
|
|
435 |
$conditions['scorm'] = $scorm->id;
|
|
|
436 |
if ($scoes = $DB->get_records('scorm_scoes', $conditions, "sortorder, id")) {
|
|
|
437 |
// Drop keys so that we can access array sequentially.
|
|
|
438 |
$scoes = array_values($scoes);
|
|
|
439 |
$level = 0;
|
|
|
440 |
$sublist = 1;
|
|
|
441 |
$parents[$level] = '/';
|
|
|
442 |
foreach ($scoes as $pos => $sco) {
|
|
|
443 |
if ($parents[$level] != $sco->parent) {
|
|
|
444 |
if ($level > 0 && $parents[$level - 1] == $sco->parent) {
|
|
|
445 |
$report .= html_writer::end_tag('ul').html_writer::end_tag('li');
|
|
|
446 |
$level--;
|
|
|
447 |
} else {
|
|
|
448 |
$i = $level;
|
|
|
449 |
$closelist = '';
|
|
|
450 |
while (($i > 0) && ($parents[$level] != $sco->parent)) {
|
|
|
451 |
$closelist .= html_writer::end_tag('ul').html_writer::end_tag('li');
|
|
|
452 |
$i--;
|
|
|
453 |
}
|
|
|
454 |
if (($i == 0) && ($sco->parent != $currentorg)) {
|
|
|
455 |
$report .= html_writer::start_tag('li');
|
|
|
456 |
$report .= html_writer::start_tag('ul', array('id' => $sublist, 'class' => $liststyle));
|
|
|
457 |
$level++;
|
|
|
458 |
} else {
|
|
|
459 |
$report .= $closelist;
|
|
|
460 |
$level = $i;
|
|
|
461 |
}
|
|
|
462 |
$parents[$level] = $sco->parent;
|
|
|
463 |
}
|
|
|
464 |
}
|
|
|
465 |
$report .= html_writer::start_tag('li');
|
|
|
466 |
if (isset($scoes[$pos + 1])) {
|
|
|
467 |
$nextsco = $scoes[$pos + 1];
|
|
|
468 |
} else {
|
|
|
469 |
$nextsco = false;
|
|
|
470 |
}
|
|
|
471 |
if (($nextsco !== false) && ($sco->parent != $nextsco->parent) &&
|
|
|
472 |
(($level == 0) || (($level > 0) && ($nextsco->parent == $sco->identifier)))) {
|
|
|
473 |
$sublist++;
|
|
|
474 |
} else {
|
|
|
475 |
$report .= $OUTPUT->spacer(array("height" => "12", "width" => "13"));
|
|
|
476 |
}
|
|
|
477 |
|
|
|
478 |
if ($sco->launch) {
|
|
|
479 |
$score = '';
|
|
|
480 |
$totaltime = '';
|
|
|
481 |
if ($usertrack = scorm_get_tracks($sco->id, $user->id)) {
|
|
|
482 |
if ($usertrack->status == '') {
|
|
|
483 |
$usertrack->status = 'notattempted';
|
|
|
484 |
}
|
|
|
485 |
$strstatus = get_string($usertrack->status, 'scorm');
|
|
|
486 |
$report .= $OUTPUT->pix_icon($usertrack->status, $strstatus, 'scorm');
|
|
|
487 |
} else {
|
|
|
488 |
if ($sco->scormtype == 'sco') {
|
|
|
489 |
$report .= $OUTPUT->pix_icon('notattempted', get_string('notattempted', 'scorm'), 'scorm');
|
|
|
490 |
} else {
|
|
|
491 |
$report .= $OUTPUT->pix_icon('asset', get_string('asset', 'scorm'), 'scorm');
|
|
|
492 |
}
|
|
|
493 |
}
|
|
|
494 |
$report .= " $sco->title $score$totaltime".html_writer::end_tag('li');
|
|
|
495 |
if ($usertrack !== false) {
|
|
|
496 |
$sometoreport = true;
|
|
|
497 |
$report .= html_writer::start_tag('li').html_writer::start_tag('ul', array('class' => $liststyle));
|
|
|
498 |
foreach ($usertrack as $element => $value) {
|
|
|
499 |
if (substr($element, 0, 3) == 'cmi') {
|
|
|
500 |
$report .= html_writer::tag('li', s($element) . ' => ' . s($value));
|
|
|
501 |
}
|
|
|
502 |
}
|
|
|
503 |
$report .= html_writer::end_tag('ul').html_writer::end_tag('li');
|
|
|
504 |
}
|
|
|
505 |
} else {
|
|
|
506 |
$report .= " $sco->title".html_writer::end_tag('li');
|
|
|
507 |
}
|
|
|
508 |
}
|
|
|
509 |
for ($i = 0; $i < $level; $i++) {
|
|
|
510 |
$report .= html_writer::end_tag('ul').html_writer::end_tag('li');
|
|
|
511 |
}
|
|
|
512 |
}
|
|
|
513 |
$report .= html_writer::end_tag('ul').html_writer::empty_tag('br');
|
|
|
514 |
}
|
|
|
515 |
$report .= html_writer::end_div();
|
|
|
516 |
}
|
|
|
517 |
if ($sometoreport) {
|
|
|
518 |
if ($firstmodify < $now) {
|
|
|
519 |
$timeago = format_time($now - $firstmodify);
|
|
|
520 |
echo get_string('firstaccess', 'scorm').': '.userdate($firstmodify).' ('.$timeago.")".html_writer::empty_tag('br');
|
|
|
521 |
}
|
|
|
522 |
if ($lastmodify > 0) {
|
|
|
523 |
$timeago = format_time($now - $lastmodify);
|
|
|
524 |
echo get_string('lastaccess', 'scorm').': '.userdate($lastmodify).' ('.$timeago.")".html_writer::empty_tag('br');
|
|
|
525 |
}
|
|
|
526 |
echo get_string('report', 'scorm').":".html_writer::empty_tag('br');
|
|
|
527 |
echo $report;
|
|
|
528 |
} else {
|
|
|
529 |
print_string('noactivity', 'scorm');
|
|
|
530 |
}
|
|
|
531 |
|
|
|
532 |
return true;
|
|
|
533 |
}
|
|
|
534 |
|
|
|
535 |
/**
|
|
|
536 |
* Function to be run periodically according to the moodle Tasks API
|
|
|
537 |
* This function searches for things that need to be done, such
|
|
|
538 |
* as sending out mail, toggling flags etc ...
|
|
|
539 |
*
|
|
|
540 |
* @global stdClass
|
|
|
541 |
* @global object
|
|
|
542 |
* @return boolean
|
|
|
543 |
*/
|
|
|
544 |
function scorm_cron_scheduled_task () {
|
|
|
545 |
global $CFG, $DB;
|
|
|
546 |
|
|
|
547 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
548 |
|
|
|
549 |
$sitetimezone = core_date::get_server_timezone();
|
|
|
550 |
// Now see if there are any scorm updates to be done.
|
|
|
551 |
|
|
|
552 |
if (!isset($CFG->scorm_updatetimelast)) { // To catch the first time.
|
|
|
553 |
set_config('scorm_updatetimelast', 0);
|
|
|
554 |
}
|
|
|
555 |
|
|
|
556 |
$timenow = time();
|
|
|
557 |
$updatetime = usergetmidnight($timenow, $sitetimezone);
|
|
|
558 |
|
|
|
559 |
if ($CFG->scorm_updatetimelast < $updatetime and $timenow > $updatetime) {
|
|
|
560 |
|
|
|
561 |
set_config('scorm_updatetimelast', $timenow);
|
|
|
562 |
|
|
|
563 |
mtrace('Updating scorm packages which require daily update');// We are updating.
|
|
|
564 |
|
|
|
565 |
$scormsupdate = $DB->get_records('scorm', array('updatefreq' => SCORM_UPDATE_EVERYDAY));
|
|
|
566 |
foreach ($scormsupdate as $scormupdate) {
|
|
|
567 |
scorm_parse($scormupdate, true);
|
|
|
568 |
}
|
|
|
569 |
|
|
|
570 |
// Now clear out AICC session table with old session data.
|
|
|
571 |
$cfgscorm = get_config('scorm');
|
|
|
572 |
if (!empty($cfgscorm->allowaicchacp)) {
|
|
|
573 |
$expiretime = time() - ($cfgscorm->aicchacpkeepsessiondata * 24 * 60 * 60);
|
|
|
574 |
$DB->delete_records_select('scorm_aicc_session', 'timemodified < ?', array($expiretime));
|
|
|
575 |
}
|
|
|
576 |
}
|
|
|
577 |
|
|
|
578 |
return true;
|
|
|
579 |
}
|
|
|
580 |
|
|
|
581 |
/**
|
|
|
582 |
* Return grade for given user or all users.
|
|
|
583 |
*
|
|
|
584 |
* @global stdClass
|
|
|
585 |
* @global object
|
|
|
586 |
* @param int $scormid id of scorm
|
|
|
587 |
* @param int $userid optional user id, 0 means all users
|
|
|
588 |
* @return array array of grades, false if none
|
|
|
589 |
*/
|
|
|
590 |
function scorm_get_user_grades($scorm, $userid=0) {
|
|
|
591 |
global $CFG, $DB;
|
|
|
592 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
593 |
|
|
|
594 |
$grades = array();
|
|
|
595 |
if (empty($userid)) {
|
|
|
596 |
$sql = "SELECT DISTINCT userid
|
|
|
597 |
FROM {scorm_attempt}
|
|
|
598 |
WHERE scormid = ?";
|
|
|
599 |
$scousers = $DB->get_recordset_sql($sql, [$scorm->id]);
|
|
|
600 |
|
|
|
601 |
foreach ($scousers as $scouser) {
|
|
|
602 |
$grades[$scouser->userid] = new stdClass();
|
|
|
603 |
$grades[$scouser->userid]->id = $scouser->userid;
|
|
|
604 |
$grades[$scouser->userid]->userid = $scouser->userid;
|
|
|
605 |
$grades[$scouser->userid]->rawgrade = scorm_grade_user($scorm, $scouser->userid);
|
|
|
606 |
}
|
|
|
607 |
$scousers->close();
|
|
|
608 |
} else {
|
|
|
609 |
$preattempt = $DB->record_exists('scorm_attempt', ['scormid' => $scorm->id, 'userid' => $userid]);
|
|
|
610 |
if (!$preattempt) {
|
|
|
611 |
return false; // No attempt yet.
|
|
|
612 |
}
|
|
|
613 |
$grades[$userid] = new stdClass();
|
|
|
614 |
$grades[$userid]->id = $userid;
|
|
|
615 |
$grades[$userid]->userid = $userid;
|
|
|
616 |
$grades[$userid]->rawgrade = scorm_grade_user($scorm, $userid);
|
|
|
617 |
}
|
|
|
618 |
|
|
|
619 |
if (empty($grades)) {
|
|
|
620 |
return false;
|
|
|
621 |
}
|
|
|
622 |
|
|
|
623 |
return $grades;
|
|
|
624 |
}
|
|
|
625 |
|
|
|
626 |
/**
|
|
|
627 |
* Update grades in central gradebook
|
|
|
628 |
*
|
|
|
629 |
* @category grade
|
|
|
630 |
* @param object $scorm
|
|
|
631 |
* @param int $userid specific user only, 0 mean all
|
|
|
632 |
* @param bool $nullifnone
|
|
|
633 |
*/
|
|
|
634 |
function scorm_update_grades($scorm, $userid=0, $nullifnone=true) {
|
|
|
635 |
global $CFG;
|
|
|
636 |
require_once($CFG->libdir.'/gradelib.php');
|
|
|
637 |
require_once($CFG->libdir.'/completionlib.php');
|
|
|
638 |
|
|
|
639 |
if ($grades = scorm_get_user_grades($scorm, $userid)) {
|
|
|
640 |
scorm_grade_item_update($scorm, $grades);
|
|
|
641 |
// Set complete.
|
|
|
642 |
scorm_set_completion($scorm, $userid, COMPLETION_COMPLETE, $grades);
|
|
|
643 |
} else if ($userid and $nullifnone) {
|
|
|
644 |
$grade = new stdClass();
|
|
|
645 |
$grade->userid = $userid;
|
|
|
646 |
$grade->rawgrade = null;
|
|
|
647 |
scorm_grade_item_update($scorm, $grade);
|
|
|
648 |
// Set incomplete.
|
|
|
649 |
scorm_set_completion($scorm, $userid, COMPLETION_INCOMPLETE);
|
|
|
650 |
} else {
|
|
|
651 |
scorm_grade_item_update($scorm);
|
|
|
652 |
}
|
|
|
653 |
}
|
|
|
654 |
|
|
|
655 |
/**
|
|
|
656 |
* Update/create grade item for given scorm
|
|
|
657 |
*
|
|
|
658 |
* @category grade
|
|
|
659 |
* @uses GRADE_TYPE_VALUE
|
|
|
660 |
* @uses GRADE_TYPE_NONE
|
|
|
661 |
* @param object $scorm object with extra cmidnumber
|
|
|
662 |
* @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
|
|
|
663 |
* @return object grade_item
|
|
|
664 |
*/
|
|
|
665 |
function scorm_grade_item_update($scorm, $grades=null) {
|
|
|
666 |
global $CFG, $DB;
|
|
|
667 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
668 |
if (!function_exists('grade_update')) { // Workaround for buggy PHP versions.
|
|
|
669 |
require_once($CFG->libdir.'/gradelib.php');
|
|
|
670 |
}
|
|
|
671 |
|
|
|
672 |
$params = array('itemname' => $scorm->name);
|
|
|
673 |
if (isset($scorm->cmidnumber)) {
|
|
|
674 |
$params['idnumber'] = $scorm->cmidnumber;
|
|
|
675 |
}
|
|
|
676 |
|
|
|
677 |
if ($scorm->grademethod == GRADESCOES) {
|
|
|
678 |
$maxgrade = $DB->count_records_select('scorm_scoes', 'scorm = ? AND '.
|
|
|
679 |
$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id));
|
|
|
680 |
if ($maxgrade) {
|
|
|
681 |
$params['gradetype'] = GRADE_TYPE_VALUE;
|
|
|
682 |
$params['grademax'] = $maxgrade;
|
|
|
683 |
$params['grademin'] = 0;
|
|
|
684 |
} else {
|
|
|
685 |
$params['gradetype'] = GRADE_TYPE_NONE;
|
|
|
686 |
}
|
|
|
687 |
} else {
|
|
|
688 |
$params['gradetype'] = GRADE_TYPE_VALUE;
|
|
|
689 |
$params['grademax'] = $scorm->maxgrade;
|
|
|
690 |
$params['grademin'] = 0;
|
|
|
691 |
}
|
|
|
692 |
|
|
|
693 |
if ($grades === 'reset') {
|
|
|
694 |
$params['reset'] = true;
|
|
|
695 |
$grades = null;
|
|
|
696 |
}
|
|
|
697 |
|
|
|
698 |
return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, $grades, $params);
|
|
|
699 |
}
|
|
|
700 |
|
|
|
701 |
/**
|
|
|
702 |
* Delete grade item for given scorm
|
|
|
703 |
*
|
|
|
704 |
* @category grade
|
|
|
705 |
* @param object $scorm object
|
|
|
706 |
* @return object grade_item
|
|
|
707 |
*/
|
|
|
708 |
function scorm_grade_item_delete($scorm) {
|
|
|
709 |
global $CFG;
|
|
|
710 |
require_once($CFG->libdir.'/gradelib.php');
|
|
|
711 |
|
|
|
712 |
return grade_update('mod/scorm', $scorm->course, 'mod', 'scorm', $scorm->id, 0, null, array('deleted' => 1));
|
|
|
713 |
}
|
|
|
714 |
|
|
|
715 |
/**
|
|
|
716 |
* List the actions that correspond to a view of this module.
|
|
|
717 |
* This is used by the participation report.
|
|
|
718 |
*
|
|
|
719 |
* Note: This is not used by new logging system. Event with
|
|
|
720 |
* crud = 'r' and edulevel = LEVEL_PARTICIPATING will
|
|
|
721 |
* be considered as view action.
|
|
|
722 |
*
|
|
|
723 |
* @return array
|
|
|
724 |
*/
|
|
|
725 |
function scorm_get_view_actions() {
|
|
|
726 |
return array('pre-view', 'view', 'view all', 'report');
|
|
|
727 |
}
|
|
|
728 |
|
|
|
729 |
/**
|
|
|
730 |
* List the actions that correspond to a post of this module.
|
|
|
731 |
* This is used by the participation report.
|
|
|
732 |
*
|
|
|
733 |
* Note: This is not used by new logging system. Event with
|
|
|
734 |
* crud = ('c' || 'u' || 'd') and edulevel = LEVEL_PARTICIPATING
|
|
|
735 |
* will be considered as post action.
|
|
|
736 |
*
|
|
|
737 |
* @return array
|
|
|
738 |
*/
|
|
|
739 |
function scorm_get_post_actions() {
|
|
|
740 |
return array();
|
|
|
741 |
}
|
|
|
742 |
|
|
|
743 |
/**
|
|
|
744 |
* @param object $scorm
|
|
|
745 |
* @return object $scorm
|
|
|
746 |
*/
|
|
|
747 |
function scorm_option2text($scorm) {
|
|
|
748 |
$scormpopoupoptions = scorm_get_popup_options_array();
|
|
|
749 |
|
|
|
750 |
if (isset($scorm->popup)) {
|
|
|
751 |
if ($scorm->popup == 1) {
|
|
|
752 |
$optionlist = array();
|
|
|
753 |
foreach ($scormpopoupoptions as $name => $option) {
|
|
|
754 |
if (isset($scorm->$name)) {
|
|
|
755 |
$optionlist[] = $name.'='.$scorm->$name;
|
|
|
756 |
} else {
|
|
|
757 |
$optionlist[] = $name.'=0';
|
|
|
758 |
}
|
|
|
759 |
}
|
|
|
760 |
$scorm->options = implode(',', $optionlist);
|
|
|
761 |
} else {
|
|
|
762 |
$scorm->options = '';
|
|
|
763 |
}
|
|
|
764 |
} else {
|
|
|
765 |
$scorm->popup = 0;
|
|
|
766 |
$scorm->options = '';
|
|
|
767 |
}
|
|
|
768 |
return $scorm;
|
|
|
769 |
}
|
|
|
770 |
|
|
|
771 |
/**
|
|
|
772 |
* Implementation of the function for printing the form elements that control
|
|
|
773 |
* whether the course reset functionality affects the scorm.
|
|
|
774 |
*
|
|
|
775 |
* @param MoodleQuickForm $mform form passed by reference
|
|
|
776 |
*/
|
|
|
777 |
function scorm_reset_course_form_definition(&$mform) {
|
|
|
778 |
$mform->addElement('header', 'scormheader', get_string('modulenameplural', 'scorm'));
|
|
|
779 |
$mform->addElement('advcheckbox', 'reset_scorm', get_string('deleteallattempts', 'scorm'));
|
|
|
780 |
}
|
|
|
781 |
|
|
|
782 |
/**
|
|
|
783 |
* Course reset form defaults.
|
|
|
784 |
*
|
|
|
785 |
* @return array
|
|
|
786 |
*/
|
|
|
787 |
function scorm_reset_course_form_defaults($course) {
|
|
|
788 |
return array('reset_scorm' => 1);
|
|
|
789 |
}
|
|
|
790 |
|
|
|
791 |
/**
|
|
|
792 |
* Removes all grades from gradebook
|
|
|
793 |
*
|
|
|
794 |
* @global stdClass
|
|
|
795 |
* @global object
|
|
|
796 |
* @param int $courseid
|
|
|
797 |
* @param string optional type
|
|
|
798 |
*/
|
|
|
799 |
function scorm_reset_gradebook($courseid, $type='') {
|
|
|
800 |
global $CFG, $DB;
|
|
|
801 |
|
|
|
802 |
$sql = "SELECT s.*, cm.idnumber as cmidnumber, s.course as courseid
|
|
|
803 |
FROM {scorm} s, {course_modules} cm, {modules} m
|
|
|
804 |
WHERE m.name='scorm' AND m.id=cm.module AND cm.instance=s.id AND s.course=?";
|
|
|
805 |
|
|
|
806 |
if ($scorms = $DB->get_records_sql($sql, array($courseid))) {
|
|
|
807 |
foreach ($scorms as $scorm) {
|
|
|
808 |
scorm_grade_item_update($scorm, 'reset');
|
|
|
809 |
}
|
|
|
810 |
}
|
|
|
811 |
}
|
|
|
812 |
|
|
|
813 |
/**
|
|
|
814 |
* Actual implementation of the reset course functionality, delete all the
|
|
|
815 |
* scorm attempts for course $data->courseid.
|
|
|
816 |
*
|
|
|
817 |
* @global stdClass
|
|
|
818 |
* @global object
|
|
|
819 |
* @param object $data the data submitted from the reset course.
|
|
|
820 |
* @return array status array
|
|
|
821 |
*/
|
|
|
822 |
function scorm_reset_userdata($data) {
|
|
|
823 |
global $DB, $CFG;
|
|
|
824 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
825 |
|
|
|
826 |
$componentstr = get_string('modulenameplural', 'scorm');
|
|
|
827 |
$status = [];
|
|
|
828 |
|
|
|
829 |
if (!empty($data->reset_scorm)) {
|
|
|
830 |
|
|
|
831 |
$scorms = $DB->get_recordset('scorm', ['course' => $data->courseid]);
|
|
|
832 |
foreach ($scorms as $scorm) {
|
|
|
833 |
scorm_delete_tracks($scorm->id);
|
|
|
834 |
}
|
|
|
835 |
$scorms->close();
|
|
|
836 |
|
|
|
837 |
// Remove all grades from gradebook.
|
|
|
838 |
if (empty($data->reset_gradebook_grades)) {
|
|
|
839 |
scorm_reset_gradebook($data->courseid);
|
|
|
840 |
}
|
|
|
841 |
|
|
|
842 |
$status[] = ['component' => $componentstr, 'item' => get_string('deleteallattempts', 'scorm'), 'error' => false];
|
|
|
843 |
}
|
|
|
844 |
|
|
|
845 |
// Any changes to the list of dates that needs to be rolled should be same during course restore and course reset.
|
|
|
846 |
// See MDL-9367.
|
|
|
847 |
shift_course_mod_dates('scorm', array('timeopen', 'timeclose'), $data->timeshift, $data->courseid);
|
|
|
848 |
$status[] = ['component' => $componentstr, 'item' => get_string('datechanged'), 'error' => false];
|
|
|
849 |
|
|
|
850 |
return $status;
|
|
|
851 |
}
|
|
|
852 |
|
|
|
853 |
/**
|
|
|
854 |
* Lists all file areas current user may browse
|
|
|
855 |
*
|
|
|
856 |
* @param object $course
|
|
|
857 |
* @param object $cm
|
|
|
858 |
* @param object $context
|
|
|
859 |
* @return array
|
|
|
860 |
*/
|
|
|
861 |
function scorm_get_file_areas($course, $cm, $context) {
|
|
|
862 |
$areas = array();
|
|
|
863 |
$areas['content'] = get_string('areacontent', 'scorm');
|
|
|
864 |
$areas['package'] = get_string('areapackage', 'scorm');
|
|
|
865 |
return $areas;
|
|
|
866 |
}
|
|
|
867 |
|
|
|
868 |
/**
|
|
|
869 |
* File browsing support for SCORM file areas
|
|
|
870 |
*
|
|
|
871 |
* @package mod_scorm
|
|
|
872 |
* @category files
|
|
|
873 |
* @param file_browser $browser file browser instance
|
|
|
874 |
* @param array $areas file areas
|
|
|
875 |
* @param stdClass $course course object
|
|
|
876 |
* @param stdClass $cm course module object
|
|
|
877 |
* @param stdClass $context context object
|
|
|
878 |
* @param string $filearea file area
|
|
|
879 |
* @param int $itemid item ID
|
|
|
880 |
* @param string $filepath file path
|
|
|
881 |
* @param string $filename file name
|
|
|
882 |
* @return file_info instance or null if not found
|
|
|
883 |
*/
|
|
|
884 |
function scorm_get_file_info($browser, $areas, $course, $cm, $context, $filearea, $itemid, $filepath, $filename) {
|
|
|
885 |
global $CFG;
|
|
|
886 |
|
|
|
887 |
if (!has_capability('moodle/course:managefiles', $context)) {
|
|
|
888 |
return null;
|
|
|
889 |
}
|
|
|
890 |
|
|
|
891 |
// No writing for now!
|
|
|
892 |
|
|
|
893 |
$fs = get_file_storage();
|
|
|
894 |
|
|
|
895 |
if ($filearea === 'content') {
|
|
|
896 |
|
|
|
897 |
$filepath = is_null($filepath) ? '/' : $filepath;
|
|
|
898 |
$filename = is_null($filename) ? '.' : $filename;
|
|
|
899 |
|
|
|
900 |
$urlbase = $CFG->wwwroot.'/pluginfile.php';
|
|
|
901 |
if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'content', 0, $filepath, $filename)) {
|
|
|
902 |
if ($filepath === '/' and $filename === '.') {
|
|
|
903 |
$storedfile = new virtual_root_file($context->id, 'mod_scorm', 'content', 0);
|
|
|
904 |
} else {
|
|
|
905 |
// Not found.
|
|
|
906 |
return null;
|
|
|
907 |
}
|
|
|
908 |
}
|
|
|
909 |
require_once("$CFG->dirroot/mod/scorm/locallib.php");
|
|
|
910 |
return new scorm_package_file_info($browser, $context, $storedfile, $urlbase, $areas[$filearea], true, true, false, false);
|
|
|
911 |
|
|
|
912 |
} else if ($filearea === 'package') {
|
|
|
913 |
$filepath = is_null($filepath) ? '/' : $filepath;
|
|
|
914 |
$filename = is_null($filename) ? '.' : $filename;
|
|
|
915 |
|
|
|
916 |
$urlbase = $CFG->wwwroot.'/pluginfile.php';
|
|
|
917 |
if (!$storedfile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, $filepath, $filename)) {
|
|
|
918 |
if ($filepath === '/' and $filename === '.') {
|
|
|
919 |
$storedfile = new virtual_root_file($context->id, 'mod_scorm', 'package', 0);
|
|
|
920 |
} else {
|
|
|
921 |
// Not found.
|
|
|
922 |
return null;
|
|
|
923 |
}
|
|
|
924 |
}
|
|
|
925 |
return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false);
|
|
|
926 |
}
|
|
|
927 |
|
|
|
928 |
// Scorm_intro handled in file_browser.
|
|
|
929 |
|
|
|
930 |
return false;
|
|
|
931 |
}
|
|
|
932 |
|
|
|
933 |
/**
|
|
|
934 |
* Serves scorm content, introduction images and packages. Implements needed access control ;-)
|
|
|
935 |
*
|
|
|
936 |
* @package mod_scorm
|
|
|
937 |
* @category files
|
|
|
938 |
* @param stdClass $course course object
|
|
|
939 |
* @param stdClass $cm course module object
|
|
|
940 |
* @param stdClass $context context object
|
|
|
941 |
* @param string $filearea file area
|
|
|
942 |
* @param array $args extra arguments
|
|
|
943 |
* @param bool $forcedownload whether or not force download
|
|
|
944 |
* @param array $options additional options affecting the file serving
|
|
|
945 |
* @return bool false if file not found, does not return if found - just send the file
|
|
|
946 |
*/
|
|
|
947 |
function scorm_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
|
|
|
948 |
global $CFG, $DB;
|
|
|
949 |
|
|
|
950 |
if ($context->contextlevel != CONTEXT_MODULE) {
|
|
|
951 |
return false;
|
|
|
952 |
}
|
|
|
953 |
|
|
|
954 |
require_login($course, true, $cm);
|
|
|
955 |
|
|
|
956 |
$canmanageactivity = has_capability('moodle/course:manageactivities', $context);
|
|
|
957 |
$lifetime = null;
|
|
|
958 |
|
|
|
959 |
// Check SCORM availability.
|
|
|
960 |
if (!$canmanageactivity) {
|
|
|
961 |
require_once($CFG->dirroot.'/mod/scorm/locallib.php');
|
|
|
962 |
|
|
|
963 |
$scorm = $DB->get_record('scorm', array('id' => $cm->instance), 'id, timeopen, timeclose', MUST_EXIST);
|
|
|
964 |
list($available, $warnings) = scorm_get_availability_status($scorm);
|
|
|
965 |
if (!$available) {
|
|
|
966 |
return false;
|
|
|
967 |
}
|
|
|
968 |
}
|
|
|
969 |
|
|
|
970 |
if ($filearea === 'content') {
|
|
|
971 |
$revision = (int)array_shift($args); // Prevents caching problems - ignored here.
|
|
|
972 |
$relativepath = implode('/', $args);
|
|
|
973 |
$fullpath = "/$context->id/mod_scorm/content/0/$relativepath";
|
|
|
974 |
$options['immutable'] = true; // Add immutable option, $relativepath changes on file update.
|
|
|
975 |
|
|
|
976 |
} else if ($filearea === 'package') {
|
|
|
977 |
// Check if the global setting for disabling package downloads is enabled.
|
|
|
978 |
$protectpackagedownloads = get_config('scorm', 'protectpackagedownloads');
|
|
|
979 |
if ($protectpackagedownloads and !$canmanageactivity) {
|
|
|
980 |
return false;
|
|
|
981 |
}
|
|
|
982 |
$revision = (int)array_shift($args); // Prevents caching problems - ignored here.
|
|
|
983 |
$relativepath = implode('/', $args);
|
|
|
984 |
$fullpath = "/$context->id/mod_scorm/package/0/$relativepath";
|
|
|
985 |
$lifetime = 0; // No caching here.
|
|
|
986 |
|
|
|
987 |
} else if ($filearea === 'imsmanifest') { // This isn't a real filearea, it's a url parameter for this type of package.
|
|
|
988 |
$revision = (int)array_shift($args); // Prevents caching problems - ignored here.
|
|
|
989 |
$relativepath = implode('/', $args);
|
|
|
990 |
|
|
|
991 |
// Get imsmanifest file.
|
|
|
992 |
$fs = get_file_storage();
|
|
|
993 |
$files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, '', false);
|
|
|
994 |
$file = reset($files);
|
|
|
995 |
|
|
|
996 |
// Check that the package file is an imsmanifest.xml file - if not then this method is not allowed.
|
|
|
997 |
$packagefilename = $file->get_filename();
|
|
|
998 |
if (strtolower($packagefilename) !== 'imsmanifest.xml') {
|
|
|
999 |
return false;
|
|
|
1000 |
}
|
|
|
1001 |
|
|
|
1002 |
$file->send_relative_file($relativepath);
|
|
|
1003 |
} else {
|
|
|
1004 |
return false;
|
|
|
1005 |
}
|
|
|
1006 |
|
|
|
1007 |
$fs = get_file_storage();
|
|
|
1008 |
if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
|
|
|
1009 |
if ($filearea === 'content') { // Return file not found straight away to improve performance.
|
|
|
1010 |
send_header_404();
|
|
|
1011 |
die;
|
|
|
1012 |
}
|
|
|
1013 |
return false;
|
|
|
1014 |
}
|
|
|
1015 |
|
|
|
1016 |
// Allow SVG files to be loaded within SCORM content, instead of forcing download.
|
|
|
1017 |
$options['dontforcesvgdownload'] = true;
|
|
|
1018 |
|
|
|
1019 |
// Finally send the file.
|
|
|
1020 |
send_stored_file($file, $lifetime, 0, false, $options);
|
|
|
1021 |
}
|
|
|
1022 |
|
|
|
1023 |
/**
|
|
|
1024 |
* @uses FEATURE_GROUPS
|
|
|
1025 |
* @uses FEATURE_GROUPINGS
|
|
|
1026 |
* @uses FEATURE_MOD_INTRO
|
|
|
1027 |
* @uses FEATURE_COMPLETION_TRACKS_VIEWS
|
|
|
1028 |
* @uses FEATURE_COMPLETION_HAS_RULES
|
|
|
1029 |
* @uses FEATURE_GRADE_HAS_GRADE
|
|
|
1030 |
* @uses FEATURE_GRADE_OUTCOMES
|
|
|
1031 |
* @param string $feature FEATURE_xx constant for requested feature
|
|
|
1032 |
* @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose.
|
|
|
1033 |
*/
|
|
|
1034 |
function scorm_supports($feature) {
|
|
|
1035 |
switch($feature) {
|
|
|
1036 |
case FEATURE_GROUPS: return true;
|
|
|
1037 |
case FEATURE_GROUPINGS: return true;
|
|
|
1038 |
case FEATURE_MOD_INTRO: return true;
|
|
|
1039 |
case FEATURE_COMPLETION_TRACKS_VIEWS: return true;
|
|
|
1040 |
case FEATURE_COMPLETION_HAS_RULES: return true;
|
|
|
1041 |
case FEATURE_GRADE_HAS_GRADE: return true;
|
|
|
1042 |
case FEATURE_GRADE_OUTCOMES: return true;
|
|
|
1043 |
case FEATURE_BACKUP_MOODLE2: return true;
|
|
|
1044 |
case FEATURE_SHOW_DESCRIPTION: return true;
|
|
|
1045 |
case FEATURE_MOD_PURPOSE:
|
|
|
1046 |
return MOD_PURPOSE_INTERACTIVECONTENT;
|
|
|
1047 |
|
|
|
1048 |
default: return null;
|
|
|
1049 |
}
|
|
|
1050 |
}
|
|
|
1051 |
|
|
|
1052 |
/**
|
|
|
1053 |
* Get the filename for a temp log file
|
|
|
1054 |
*
|
|
|
1055 |
* @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
|
|
|
1056 |
* @param integer $scoid - scoid of object this log entry is for
|
|
|
1057 |
* @return string The filename as an absolute path
|
|
|
1058 |
*/
|
|
|
1059 |
function scorm_debug_log_filename($type, $scoid) {
|
|
|
1060 |
global $CFG, $USER;
|
|
|
1061 |
|
|
|
1062 |
$logpath = $CFG->tempdir.'/scormlogs';
|
|
|
1063 |
$logfile = $logpath.'/'.$type.'debug_'.$USER->id.'_'.$scoid.'.log';
|
|
|
1064 |
return $logfile;
|
|
|
1065 |
}
|
|
|
1066 |
|
|
|
1067 |
/**
|
|
|
1068 |
* writes log output to a temp log file
|
|
|
1069 |
*
|
|
|
1070 |
* @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
|
|
|
1071 |
* @param string $text - text to be written to file.
|
|
|
1072 |
* @param integer $scoid - scoid of object this log entry is for.
|
|
|
1073 |
*/
|
|
|
1074 |
function scorm_debug_log_write($type, $text, $scoid) {
|
|
|
1075 |
global $CFG;
|
|
|
1076 |
|
|
|
1077 |
$debugenablelog = get_config('scorm', 'allowapidebug');
|
|
|
1078 |
if (!$debugenablelog || empty($text)) {
|
|
|
1079 |
return;
|
|
|
1080 |
}
|
|
|
1081 |
if (make_temp_directory('scormlogs/')) {
|
|
|
1082 |
$logfile = scorm_debug_log_filename($type, $scoid);
|
|
|
1083 |
@file_put_contents($logfile, date('Y/m/d H:i:s O')." DEBUG $text\r\n", FILE_APPEND);
|
|
|
1084 |
@chmod($logfile, $CFG->filepermissions);
|
|
|
1085 |
}
|
|
|
1086 |
}
|
|
|
1087 |
|
|
|
1088 |
/**
|
|
|
1089 |
* Remove debug log file
|
|
|
1090 |
*
|
|
|
1091 |
* @param string $type - type of log(aicc,scorm12,scorm13) used as prefix for filename
|
|
|
1092 |
* @param integer $scoid - scoid of object this log entry is for
|
|
|
1093 |
* @return boolean True if the file is successfully deleted, false otherwise
|
|
|
1094 |
*/
|
|
|
1095 |
function scorm_debug_log_remove($type, $scoid) {
|
|
|
1096 |
|
|
|
1097 |
$debugenablelog = get_config('scorm', 'allowapidebug');
|
|
|
1098 |
$logfile = scorm_debug_log_filename($type, $scoid);
|
|
|
1099 |
if (!$debugenablelog || !file_exists($logfile)) {
|
|
|
1100 |
return false;
|
|
|
1101 |
}
|
|
|
1102 |
|
|
|
1103 |
return @unlink($logfile);
|
|
|
1104 |
}
|
|
|
1105 |
|
|
|
1106 |
/**
|
|
|
1107 |
* @deprecated since Moodle 3.3, when the block_course_overview block was removed.
|
|
|
1108 |
*/
|
|
|
1109 |
function scorm_print_overview() {
|
|
|
1110 |
throw new coding_exception('scorm_print_overview() can not be used any more and is obsolete.');
|
|
|
1111 |
}
|
|
|
1112 |
|
|
|
1113 |
/**
|
|
|
1114 |
* Return a list of page types
|
|
|
1115 |
* @param string $pagetype current page type
|
|
|
1116 |
* @param stdClass $parentcontext Block's parent context
|
|
|
1117 |
* @param stdClass $currentcontext Current context of block
|
|
|
1118 |
*/
|
|
|
1119 |
function scorm_page_type_list($pagetype, $parentcontext, $currentcontext) {
|
|
|
1120 |
$modulepagetype = array('mod-scorm-*' => get_string('page-mod-scorm-x', 'scorm'));
|
|
|
1121 |
return $modulepagetype;
|
|
|
1122 |
}
|
|
|
1123 |
|
|
|
1124 |
/**
|
|
|
1125 |
* Returns the SCORM version used.
|
|
|
1126 |
* @param string $scormversion comes from $scorm->version
|
|
|
1127 |
* @param string $version one of the defined vars SCORM_12, SCORM_13, SCORM_AICC (or empty)
|
|
|
1128 |
* @return Scorm version.
|
|
|
1129 |
*/
|
|
|
1130 |
function scorm_version_check($scormversion, $version='') {
|
|
|
1131 |
$scormversion = trim(strtolower($scormversion));
|
|
|
1132 |
if (empty($version) || $version == SCORM_12) {
|
|
|
1133 |
if ($scormversion == 'scorm_12' || $scormversion == 'scorm_1.2') {
|
|
|
1134 |
return SCORM_12;
|
|
|
1135 |
}
|
|
|
1136 |
if (!empty($version)) {
|
|
|
1137 |
return false;
|
|
|
1138 |
}
|
|
|
1139 |
}
|
|
|
1140 |
if (empty($version) || $version == SCORM_13) {
|
|
|
1141 |
if ($scormversion == 'scorm_13' || $scormversion == 'scorm_1.3') {
|
|
|
1142 |
return SCORM_13;
|
|
|
1143 |
}
|
|
|
1144 |
if (!empty($version)) {
|
|
|
1145 |
return false;
|
|
|
1146 |
}
|
|
|
1147 |
}
|
|
|
1148 |
if (empty($version) || $version == SCORM_AICC) {
|
|
|
1149 |
if (strpos($scormversion, 'aicc')) {
|
|
|
1150 |
return SCORM_AICC;
|
|
|
1151 |
}
|
|
|
1152 |
if (!empty($version)) {
|
|
|
1153 |
return false;
|
|
|
1154 |
}
|
|
|
1155 |
}
|
|
|
1156 |
return false;
|
|
|
1157 |
}
|
|
|
1158 |
|
|
|
1159 |
/**
|
|
|
1160 |
* Register the ability to handle drag and drop file uploads
|
|
|
1161 |
* @return array containing details of the files / types the mod can handle
|
|
|
1162 |
*/
|
|
|
1163 |
function scorm_dndupload_register() {
|
|
|
1164 |
return array('files' => array(
|
|
|
1165 |
array('extension' => 'zip', 'message' => get_string('dnduploadscorm', 'scorm'))
|
|
|
1166 |
));
|
|
|
1167 |
}
|
|
|
1168 |
|
|
|
1169 |
/**
|
|
|
1170 |
* Handle a file that has been uploaded
|
|
|
1171 |
* @param object $uploadinfo details of the file / content that has been uploaded
|
|
|
1172 |
* @return int instance id of the newly created mod
|
|
|
1173 |
*/
|
|
|
1174 |
function scorm_dndupload_handle($uploadinfo) {
|
|
|
1175 |
|
|
|
1176 |
$context = context_module::instance($uploadinfo->coursemodule);
|
|
|
1177 |
file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_scorm', 'package', 0);
|
|
|
1178 |
$fs = get_file_storage();
|
|
|
1179 |
$files = $fs->get_area_files($context->id, 'mod_scorm', 'package', 0, 'sortorder, itemid, filepath, filename', false);
|
|
|
1180 |
$file = reset($files);
|
|
|
1181 |
|
|
|
1182 |
// Validate the file, make sure it's a valid SCORM package!
|
|
|
1183 |
$errors = scorm_validate_package($file);
|
|
|
1184 |
if (!empty($errors)) {
|
|
|
1185 |
return false;
|
|
|
1186 |
}
|
|
|
1187 |
// Create a default scorm object to pass to scorm_add_instance()!
|
|
|
1188 |
$scorm = get_config('scorm');
|
|
|
1189 |
$scorm->course = $uploadinfo->course->id;
|
|
|
1190 |
$scorm->coursemodule = $uploadinfo->coursemodule;
|
|
|
1191 |
$scorm->cmidnumber = '';
|
|
|
1192 |
$scorm->name = $uploadinfo->displayname;
|
|
|
1193 |
$scorm->scormtype = SCORM_TYPE_LOCAL;
|
|
|
1194 |
$scorm->reference = $file->get_filename();
|
|
|
1195 |
$scorm->intro = '';
|
|
|
1196 |
$scorm->width = $scorm->framewidth;
|
|
|
1197 |
$scorm->height = $scorm->frameheight;
|
|
|
1198 |
|
|
|
1199 |
return scorm_add_instance($scorm, null);
|
|
|
1200 |
}
|
|
|
1201 |
|
|
|
1202 |
/**
|
|
|
1203 |
* Sets activity completion state
|
|
|
1204 |
*
|
|
|
1205 |
* @param object $scorm object
|
|
|
1206 |
* @param int $userid User ID
|
|
|
1207 |
* @param int $completionstate Completion state
|
|
|
1208 |
* @param array $grades grades array of users with grades - used when $userid = 0
|
|
|
1209 |
*/
|
|
|
1210 |
function scorm_set_completion($scorm, $userid, $completionstate = COMPLETION_COMPLETE, $grades = array()) {
|
|
|
1211 |
$course = new stdClass();
|
|
|
1212 |
$course->id = $scorm->course;
|
|
|
1213 |
$completion = new completion_info($course);
|
|
|
1214 |
|
|
|
1215 |
// Check if completion is enabled site-wide, or for the course.
|
|
|
1216 |
if (!$completion->is_enabled()) {
|
|
|
1217 |
return;
|
|
|
1218 |
}
|
|
|
1219 |
|
|
|
1220 |
$cm = get_coursemodule_from_instance('scorm', $scorm->id, $scorm->course);
|
|
|
1221 |
if (empty($cm) || !$completion->is_enabled($cm)) {
|
|
|
1222 |
return;
|
|
|
1223 |
}
|
|
|
1224 |
|
|
|
1225 |
if (empty($userid)) { // We need to get all the relevant users from $grades param.
|
|
|
1226 |
foreach ($grades as $grade) {
|
|
|
1227 |
$completion->update_state($cm, $completionstate, $grade->userid);
|
|
|
1228 |
}
|
|
|
1229 |
} else {
|
|
|
1230 |
$completion->update_state($cm, $completionstate, $userid);
|
|
|
1231 |
}
|
|
|
1232 |
}
|
|
|
1233 |
|
|
|
1234 |
/**
|
|
|
1235 |
* Check that a Zip file contains a valid SCORM package
|
|
|
1236 |
*
|
|
|
1237 |
* @param $file stored_file a Zip file.
|
|
|
1238 |
* @return array empty if no issue is found. Array of error message otherwise
|
|
|
1239 |
*/
|
|
|
1240 |
function scorm_validate_package($file) {
|
|
|
1241 |
$packer = get_file_packer('application/zip');
|
|
|
1242 |
$errors = array();
|
|
|
1243 |
if ($file->is_external_file()) { // Get zip file so we can check it is correct.
|
|
|
1244 |
$file->import_external_file_contents();
|
|
|
1245 |
}
|
|
|
1246 |
$filelist = $file->list_files($packer);
|
|
|
1247 |
|
|
|
1248 |
if (!is_array($filelist)) {
|
|
|
1249 |
$errors['packagefile'] = get_string('badarchive', 'scorm');
|
|
|
1250 |
} else {
|
|
|
1251 |
$aiccfound = false;
|
|
|
1252 |
$badmanifestpresent = false;
|
|
|
1253 |
foreach ($filelist as $info) {
|
|
|
1254 |
if ($info->pathname == 'imsmanifest.xml') {
|
|
|
1255 |
return array();
|
|
|
1256 |
} else if (strpos($info->pathname, 'imsmanifest.xml') !== false) {
|
|
|
1257 |
// This package has an imsmanifest file inside a folder of the package.
|
|
|
1258 |
$badmanifestpresent = true;
|
|
|
1259 |
}
|
|
|
1260 |
if (preg_match('/\.cst$/', $info->pathname)) {
|
|
|
1261 |
return array();
|
|
|
1262 |
}
|
|
|
1263 |
}
|
|
|
1264 |
if (!$aiccfound) {
|
|
|
1265 |
if ($badmanifestpresent) {
|
|
|
1266 |
$errors['packagefile'] = get_string('badimsmanifestlocation', 'scorm');
|
|
|
1267 |
} else {
|
|
|
1268 |
$errors['packagefile'] = get_string('nomanifest', 'scorm');
|
|
|
1269 |
}
|
|
|
1270 |
}
|
|
|
1271 |
}
|
|
|
1272 |
return $errors;
|
|
|
1273 |
}
|
|
|
1274 |
|
|
|
1275 |
/**
|
|
|
1276 |
* Check and set the correct mode and attempt when entering a SCORM package.
|
|
|
1277 |
*
|
|
|
1278 |
* @param object $scorm object
|
|
|
1279 |
* @param string $newattempt should a new attempt be generated here.
|
|
|
1280 |
* @param int $attempt the attempt number this is for.
|
|
|
1281 |
* @param int $userid the userid of the user.
|
|
|
1282 |
* @param string $mode the current mode that has been selected.
|
|
|
1283 |
*/
|
|
|
1284 |
function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) {
|
|
|
1285 |
global $DB;
|
|
|
1286 |
|
|
|
1287 |
if (($mode == 'browse')) {
|
|
|
1288 |
if ($scorm->hidebrowse == 1) {
|
|
|
1289 |
// Prevent Browse mode if hidebrowse is set.
|
|
|
1290 |
$mode = 'normal';
|
|
|
1291 |
} else {
|
|
|
1292 |
// We don't need to check attempts as browse mode is set.
|
|
|
1293 |
return;
|
|
|
1294 |
}
|
|
|
1295 |
}
|
|
|
1296 |
|
|
|
1297 |
if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS) {
|
|
|
1298 |
// This SCORM is configured to force a new attempt on every re-entry.
|
|
|
1299 |
$newattempt = 'on';
|
|
|
1300 |
$mode = 'normal';
|
|
|
1301 |
if ($attempt == 1) {
|
|
|
1302 |
// Check if the user has any existing data or if this is really the first attempt.
|
|
|
1303 |
$exists = $DB->record_exists('scorm_attempt', ['userid' => $userid, 'scormid' => $scorm->id]);
|
|
|
1304 |
if (!$exists) {
|
|
|
1305 |
// No records yet - Attempt should == 1.
|
|
|
1306 |
return;
|
|
|
1307 |
}
|
|
|
1308 |
}
|
|
|
1309 |
$attempt++;
|
|
|
1310 |
|
|
|
1311 |
return;
|
|
|
1312 |
}
|
|
|
1313 |
// Check if the scorm module is incomplete (used to validate user request to start a new attempt).
|
|
|
1314 |
$incomplete = true;
|
|
|
1315 |
|
|
|
1316 |
// Note - in SCORM_13 the cmi-core.lesson_status field was split into
|
|
|
1317 |
// 'cmi.completion_status' and 'cmi.success_status'.
|
|
|
1318 |
// 'cmi.completion_status' can only contain values 'completed', 'incomplete', 'not attempted' or 'unknown'.
|
|
|
1319 |
// This means the values 'passed' or 'failed' will never be reported for a track in SCORM_13 and
|
|
|
1320 |
// the only status that will be treated as complete is 'completed'.
|
|
|
1321 |
|
|
|
1322 |
$completionelements = array(
|
|
|
1323 |
SCORM_12 => 'cmi.core.lesson_status',
|
|
|
1324 |
SCORM_13 => 'cmi.completion_status',
|
|
|
1325 |
SCORM_AICC => 'cmi.core.lesson_status'
|
|
|
1326 |
);
|
|
|
1327 |
$scormversion = scorm_version_check($scorm->version);
|
|
|
1328 |
if($scormversion===false) {
|
|
|
1329 |
$scormversion = SCORM_12;
|
|
|
1330 |
}
|
|
|
1331 |
$completionelement = $completionelements[$scormversion];
|
|
|
1332 |
|
|
|
1333 |
$sql = "SELECT sc.id, sub.value
|
|
|
1334 |
FROM {scorm_scoes} sc
|
|
|
1335 |
LEFT JOIN (SELECT v.scoid, v.value
|
|
|
1336 |
FROM {scorm_attempt} a
|
|
|
1337 |
JOIN {scorm_scoes_value} v ON a.id = v.attemptid
|
|
|
1338 |
JOIN {scorm_element} e on e.id = v.elementid AND e.element = :element
|
|
|
1339 |
WHERE a.userid = :userid AND a.attempt = :attempt AND a.scormid = :scormid) sub ON sub.scoid = sc.id
|
|
|
1340 |
WHERE sc.scormtype = 'sco' AND sc.scorm = :scormid2";
|
|
|
1341 |
$tracks = $DB->get_recordset_sql($sql, ['userid' => $userid, 'attempt' => $attempt,
|
|
|
1342 |
'element' => $completionelement, 'scormid' => $scorm->id,
|
|
|
1343 |
'scormid2' => $scorm->id]);
|
|
|
1344 |
|
|
|
1345 |
foreach ($tracks as $track) {
|
|
|
1346 |
if (($track->value == 'completed') || ($track->value == 'passed') || ($track->value == 'failed')) {
|
|
|
1347 |
$incomplete = false;
|
|
|
1348 |
} else {
|
|
|
1349 |
$incomplete = true;
|
|
|
1350 |
break; // Found an incomplete sco, so the result as a whole is incomplete.
|
|
|
1351 |
}
|
|
|
1352 |
}
|
|
|
1353 |
$tracks->close();
|
|
|
1354 |
|
|
|
1355 |
// Validate user request to start a new attempt.
|
|
|
1356 |
if ($incomplete === true) {
|
|
|
1357 |
// The option to start a new attempt should never have been presented. Force false.
|
|
|
1358 |
$newattempt = 'off';
|
|
|
1359 |
} else if (!empty($scorm->forcenewattempt)) {
|
|
|
1360 |
// A new attempt should be forced for already completed attempts.
|
|
|
1361 |
$newattempt = 'on';
|
|
|
1362 |
}
|
|
|
1363 |
|
|
|
1364 |
if (($newattempt == 'on') && (($attempt < $scorm->maxattempt) || ($scorm->maxattempt == 0))) {
|
|
|
1365 |
$attempt++;
|
|
|
1366 |
$mode = 'normal';
|
|
|
1367 |
} else { // Check if review mode should be set.
|
|
|
1368 |
if ($incomplete === true) {
|
|
|
1369 |
$mode = 'normal';
|
|
|
1370 |
} else {
|
|
|
1371 |
$mode = 'review';
|
|
|
1372 |
}
|
|
|
1373 |
}
|
|
|
1374 |
}
|
|
|
1375 |
|
|
|
1376 |
/**
|
|
|
1377 |
* Trigger the course_module_viewed event.
|
|
|
1378 |
*
|
|
|
1379 |
* @param stdClass $scorm scorm object
|
|
|
1380 |
* @param stdClass $course course object
|
|
|
1381 |
* @param stdClass $cm course module object
|
|
|
1382 |
* @param stdClass $context context object
|
|
|
1383 |
* @since Moodle 3.0
|
|
|
1384 |
*/
|
|
|
1385 |
function scorm_view($scorm, $course, $cm, $context) {
|
|
|
1386 |
|
|
|
1387 |
// Trigger course_module_viewed event.
|
|
|
1388 |
$params = array(
|
|
|
1389 |
'context' => $context,
|
|
|
1390 |
'objectid' => $scorm->id
|
|
|
1391 |
);
|
|
|
1392 |
|
|
|
1393 |
$event = \mod_scorm\event\course_module_viewed::create($params);
|
|
|
1394 |
$event->add_record_snapshot('course_modules', $cm);
|
|
|
1395 |
$event->add_record_snapshot('course', $course);
|
|
|
1396 |
$event->add_record_snapshot('scorm', $scorm);
|
|
|
1397 |
$event->trigger();
|
|
|
1398 |
}
|
|
|
1399 |
|
|
|
1400 |
/**
|
|
|
1401 |
* Check if the module has any update that affects the current user since a given time.
|
|
|
1402 |
*
|
|
|
1403 |
* @param cm_info $cm course module data
|
|
|
1404 |
* @param int $from the time to check updates from
|
|
|
1405 |
* @param array $filter if we need to check only specific updates
|
|
|
1406 |
* @return stdClass an object with the different type of areas indicating if they were updated or not
|
|
|
1407 |
* @since Moodle 3.2
|
|
|
1408 |
*/
|
|
|
1409 |
function scorm_check_updates_since(cm_info $cm, $from, $filter = array()) {
|
|
|
1410 |
global $DB, $USER, $CFG;
|
|
|
1411 |
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
|
|
|
1412 |
|
|
|
1413 |
$scorm = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
|
|
|
1414 |
$updates = new stdClass();
|
|
|
1415 |
list($available, $warnings) = scorm_get_availability_status($scorm, true, $cm->context);
|
|
|
1416 |
if (!$available) {
|
|
|
1417 |
return $updates;
|
|
|
1418 |
}
|
|
|
1419 |
$updates = course_check_module_updates_since($cm, $from, array('package'), $filter);
|
|
|
1420 |
|
|
|
1421 |
$updates->tracks = (object) array('updated' => false);
|
|
|
1422 |
$sql = "SELECT v.id
|
|
|
1423 |
FROM {scorm_scoes_value} v
|
|
|
1424 |
JOIN {scorm_attempt} a ON a.id = v.attemptid
|
|
|
1425 |
WHERE a.scormid = :scormid AND v.timemodified > :timemodified";
|
|
|
1426 |
$params = ['scormid' => $scorm->id, 'timemodified' => $from, 'userid' => $USER->id];
|
|
|
1427 |
$tracks = $DB->get_records_sql($sql ." AND a.userid = :userid", $params);
|
|
|
1428 |
if (!empty($tracks)) {
|
|
|
1429 |
$updates->tracks->updated = true;
|
|
|
1430 |
$updates->tracks->itemids = array_keys($tracks);
|
|
|
1431 |
}
|
|
|
1432 |
|
|
|
1433 |
// Now, teachers should see other students updates.
|
|
|
1434 |
if (has_capability('mod/scorm:viewreport', $cm->context)) {
|
|
|
1435 |
$params = ['scormid' => $scorm->id, 'timemodified' => $from];
|
|
|
1436 |
|
|
|
1437 |
if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
|
|
|
1438 |
$groupusers = array_keys(groups_get_activity_shared_group_members($cm));
|
|
|
1439 |
if (empty($groupusers)) {
|
|
|
1440 |
return $updates;
|
|
|
1441 |
}
|
|
|
1442 |
list($insql, $inparams) = $DB->get_in_or_equal($groupusers, SQL_PARAMS_NAMED);
|
|
|
1443 |
$sql .= ' AND userid ' . $insql;
|
|
|
1444 |
$params = array_merge($params, $inparams);
|
|
|
1445 |
}
|
|
|
1446 |
|
|
|
1447 |
$updates->usertracks = (object) array('updated' => false);
|
|
|
1448 |
|
|
|
1449 |
$tracks = $DB->get_records_sql($sql, $params);
|
|
|
1450 |
if (!empty($tracks)) {
|
|
|
1451 |
$updates->usertracks->updated = true;
|
|
|
1452 |
$updates->usertracks->itemids = array_keys($tracks);
|
|
|
1453 |
}
|
|
|
1454 |
}
|
|
|
1455 |
return $updates;
|
|
|
1456 |
}
|
|
|
1457 |
|
|
|
1458 |
/**
|
|
|
1459 |
* Get icon mapping for font-awesome.
|
|
|
1460 |
*/
|
|
|
1461 |
function mod_scorm_get_fontawesome_icon_map() {
|
|
|
1462 |
return [
|
|
|
1463 |
'mod_scorm:assetc' => 'fa-file-archive-o',
|
|
|
1464 |
'mod_scorm:asset' => 'fa-file-archive-o',
|
|
|
1465 |
'mod_scorm:browsed' => 'fa-book',
|
|
|
1466 |
'mod_scorm:completed' => 'fa-check-square-o',
|
|
|
1467 |
'mod_scorm:failed' => 'fa-times',
|
|
|
1468 |
'mod_scorm:incomplete' => 'fa-pencil-square-o',
|
|
|
1469 |
'mod_scorm:minus' => 'fa-minus',
|
|
|
1470 |
'mod_scorm:notattempted' => 'fa-square-o',
|
|
|
1471 |
'mod_scorm:passed' => 'fa-check',
|
|
|
1472 |
'mod_scorm:plus' => 'fa-plus',
|
|
|
1473 |
'mod_scorm:popdown' => 'fa-window-close-o',
|
|
|
1474 |
'mod_scorm:popup' => 'fa-window-restore',
|
|
|
1475 |
'mod_scorm:suspend' => 'fa-pause',
|
|
|
1476 |
'mod_scorm:wait' => 'fa-clock-o',
|
|
|
1477 |
];
|
|
|
1478 |
}
|
|
|
1479 |
|
|
|
1480 |
/**
|
|
|
1481 |
* This standard function will check all instances of this module
|
|
|
1482 |
* and make sure there are up-to-date events created for each of them.
|
|
|
1483 |
* If courseid = 0, then every scorm event in the site is checked, else
|
|
|
1484 |
* only scorm events belonging to the course specified are checked.
|
|
|
1485 |
*
|
|
|
1486 |
* @param int $courseid
|
|
|
1487 |
* @param int|stdClass $instance scorm module instance or ID.
|
|
|
1488 |
* @param int|stdClass $cm Course module object or ID.
|
|
|
1489 |
* @return bool
|
|
|
1490 |
*/
|
|
|
1491 |
function scorm_refresh_events($courseid = 0, $instance = null, $cm = null) {
|
|
|
1492 |
global $CFG, $DB;
|
|
|
1493 |
|
|
|
1494 |
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
|
|
|
1495 |
|
|
|
1496 |
// If we have instance information then we can just update the one event instead of updating all events.
|
|
|
1497 |
if (isset($instance)) {
|
|
|
1498 |
if (!is_object($instance)) {
|
|
|
1499 |
$instance = $DB->get_record('scorm', array('id' => $instance), '*', MUST_EXIST);
|
|
|
1500 |
}
|
|
|
1501 |
if (isset($cm)) {
|
|
|
1502 |
if (!is_object($cm)) {
|
|
|
1503 |
$cm = (object)array('id' => $cm);
|
|
|
1504 |
}
|
|
|
1505 |
} else {
|
|
|
1506 |
$cm = get_coursemodule_from_instance('scorm', $instance->id);
|
|
|
1507 |
}
|
|
|
1508 |
scorm_update_calendar($instance, $cm->id);
|
|
|
1509 |
return true;
|
|
|
1510 |
}
|
|
|
1511 |
|
|
|
1512 |
if ($courseid) {
|
|
|
1513 |
// Make sure that the course id is numeric.
|
|
|
1514 |
if (!is_numeric($courseid)) {
|
|
|
1515 |
return false;
|
|
|
1516 |
}
|
|
|
1517 |
if (!$scorms = $DB->get_records('scorm', array('course' => $courseid))) {
|
|
|
1518 |
return false;
|
|
|
1519 |
}
|
|
|
1520 |
} else {
|
|
|
1521 |
if (!$scorms = $DB->get_records('scorm')) {
|
|
|
1522 |
return false;
|
|
|
1523 |
}
|
|
|
1524 |
}
|
|
|
1525 |
|
|
|
1526 |
foreach ($scorms as $scorm) {
|
|
|
1527 |
$cm = get_coursemodule_from_instance('scorm', $scorm->id);
|
|
|
1528 |
scorm_update_calendar($scorm, $cm->id);
|
|
|
1529 |
}
|
|
|
1530 |
|
|
|
1531 |
return true;
|
|
|
1532 |
}
|
|
|
1533 |
|
|
|
1534 |
/**
|
|
|
1535 |
* This function receives a calendar event and returns the action associated with it, or null if there is none.
|
|
|
1536 |
*
|
|
|
1537 |
* This is used by block_myoverview in order to display the event appropriately. If null is returned then the event
|
|
|
1538 |
* is not displayed on the block.
|
|
|
1539 |
*
|
|
|
1540 |
* @param calendar_event $event
|
|
|
1541 |
* @param \core_calendar\action_factory $factory
|
|
|
1542 |
* @param int $userid User id override
|
|
|
1543 |
* @return \core_calendar\local\event\entities\action_interface|null
|
|
|
1544 |
*/
|
|
|
1545 |
function mod_scorm_core_calendar_provide_event_action(calendar_event $event,
|
|
|
1546 |
\core_calendar\action_factory $factory, $userid = null) {
|
|
|
1547 |
global $CFG, $USER;
|
|
|
1548 |
|
|
|
1549 |
require_once($CFG->dirroot . '/mod/scorm/locallib.php');
|
|
|
1550 |
|
|
|
1551 |
if (empty($userid)) {
|
|
|
1552 |
$userid = $USER->id;
|
|
|
1553 |
}
|
|
|
1554 |
|
|
|
1555 |
$cm = get_fast_modinfo($event->courseid, $userid)->instances['scorm'][$event->instance];
|
|
|
1556 |
|
|
|
1557 |
if (has_capability('mod/scorm:viewreport', $cm->context, $userid)) {
|
|
|
1558 |
// Teachers do not need to be reminded to complete a scorm.
|
|
|
1559 |
return null;
|
|
|
1560 |
}
|
|
|
1561 |
|
|
|
1562 |
$completion = new \completion_info($cm->get_course());
|
|
|
1563 |
|
|
|
1564 |
$completiondata = $completion->get_data($cm, false, $userid);
|
|
|
1565 |
|
|
|
1566 |
if ($completiondata->completionstate != COMPLETION_INCOMPLETE) {
|
|
|
1567 |
return null;
|
|
|
1568 |
}
|
|
|
1569 |
|
|
|
1570 |
if (!empty($cm->customdata['timeclose']) && $cm->customdata['timeclose'] < time()) {
|
|
|
1571 |
// The scorm has closed so the user can no longer submit anything.
|
|
|
1572 |
return null;
|
|
|
1573 |
}
|
|
|
1574 |
|
|
|
1575 |
// Restore scorm object from cached values in $cm, we only need id, timeclose and timeopen.
|
|
|
1576 |
$customdata = $cm->customdata ?: [];
|
|
|
1577 |
$customdata['id'] = $cm->instance;
|
|
|
1578 |
$scorm = (object)($customdata + ['timeclose' => 0, 'timeopen' => 0]);
|
|
|
1579 |
|
|
|
1580 |
// Check that the SCORM activity is open.
|
|
|
1581 |
list($actionable, $warnings) = scorm_get_availability_status($scorm, false, null, $userid);
|
|
|
1582 |
|
|
|
1583 |
return $factory->create_instance(
|
|
|
1584 |
get_string('enter', 'scorm'),
|
|
|
1585 |
new \moodle_url('/mod/scorm/view.php', array('id' => $cm->id)),
|
|
|
1586 |
1,
|
|
|
1587 |
$actionable
|
|
|
1588 |
);
|
|
|
1589 |
}
|
|
|
1590 |
|
|
|
1591 |
/**
|
|
|
1592 |
* Add a get_coursemodule_info function in case any SCORM type wants to add 'extra' information
|
|
|
1593 |
* for the course (see resource).
|
|
|
1594 |
*
|
|
|
1595 |
* Given a course_module object, this function returns any "extra" information that may be needed
|
|
|
1596 |
* when printing this activity in a course listing. See get_array_of_activities() in course/lib.php.
|
|
|
1597 |
*
|
|
|
1598 |
* @param stdClass $coursemodule The coursemodule object (record).
|
|
|
1599 |
* @return cached_cm_info An object on information that the courses
|
|
|
1600 |
* will know about (most noticeably, an icon).
|
|
|
1601 |
*/
|
|
|
1602 |
function scorm_get_coursemodule_info($coursemodule) {
|
|
|
1603 |
global $DB;
|
|
|
1604 |
|
|
|
1605 |
$dbparams = ['id' => $coursemodule->instance];
|
|
|
1606 |
$fields = 'id, name, intro, introformat, completionstatusrequired, completionscorerequired, completionstatusallscos, '.
|
|
|
1607 |
'timeopen, timeclose';
|
|
|
1608 |
if (!$scorm = $DB->get_record('scorm', $dbparams, $fields)) {
|
|
|
1609 |
return false;
|
|
|
1610 |
}
|
|
|
1611 |
|
|
|
1612 |
$result = new cached_cm_info();
|
|
|
1613 |
$result->name = $scorm->name;
|
|
|
1614 |
|
|
|
1615 |
if ($coursemodule->showdescription) {
|
|
|
1616 |
// Convert intro to html. Do not filter cached version, filters run at display time.
|
|
|
1617 |
$result->content = format_module_intro('scorm', $scorm, $coursemodule->id, false);
|
|
|
1618 |
}
|
|
|
1619 |
|
|
|
1620 |
// Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
|
|
|
1621 |
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
|
|
|
1622 |
$result->customdata['customcompletionrules']['completionstatusrequired'] = $scorm->completionstatusrequired;
|
|
|
1623 |
$result->customdata['customcompletionrules']['completionscorerequired'] = $scorm->completionscorerequired;
|
|
|
1624 |
$result->customdata['customcompletionrules']['completionstatusallscos'] = $scorm->completionstatusallscos;
|
|
|
1625 |
}
|
|
|
1626 |
// Populate some other values that can be used in calendar or on dashboard.
|
|
|
1627 |
if ($scorm->timeopen) {
|
|
|
1628 |
$result->customdata['timeopen'] = $scorm->timeopen;
|
|
|
1629 |
}
|
|
|
1630 |
if ($scorm->timeclose) {
|
|
|
1631 |
$result->customdata['timeclose'] = $scorm->timeclose;
|
|
|
1632 |
}
|
|
|
1633 |
|
|
|
1634 |
return $result;
|
|
|
1635 |
}
|
|
|
1636 |
|
|
|
1637 |
/**
|
|
|
1638 |
* Callback which returns human-readable strings describing the active completion custom rules for the module instance.
|
|
|
1639 |
*
|
|
|
1640 |
* @param cm_info|stdClass $cm object with fields ->completion and ->customdata['customcompletionrules']
|
|
|
1641 |
* @return array $descriptions the array of descriptions for the custom rules.
|
|
|
1642 |
*/
|
|
|
1643 |
function mod_scorm_get_completion_active_rule_descriptions($cm) {
|
|
|
1644 |
// Values will be present in cm_info, and we assume these are up to date.
|
|
|
1645 |
if (empty($cm->customdata['customcompletionrules'])
|
|
|
1646 |
|| $cm->completion != COMPLETION_TRACKING_AUTOMATIC) {
|
|
|
1647 |
return [];
|
|
|
1648 |
}
|
|
|
1649 |
|
|
|
1650 |
$descriptions = [];
|
|
|
1651 |
foreach ($cm->customdata['customcompletionrules'] as $key => $val) {
|
|
|
1652 |
switch ($key) {
|
|
|
1653 |
case 'completionstatusrequired':
|
|
|
1654 |
if (!is_null($val)) {
|
|
|
1655 |
// Determine the selected statuses using a bitwise operation.
|
|
|
1656 |
$cvalues = array();
|
|
|
1657 |
foreach (scorm_status_options(true) as $bit => $string) {
|
|
|
1658 |
if (($val & $bit) == $bit) {
|
|
|
1659 |
$cvalues[] = $string;
|
|
|
1660 |
}
|
|
|
1661 |
}
|
|
|
1662 |
$statusstring = implode(', ', $cvalues);
|
|
|
1663 |
$descriptions[] = get_string('completionstatusrequireddesc', 'scorm', $statusstring);
|
|
|
1664 |
}
|
|
|
1665 |
break;
|
|
|
1666 |
case 'completionscorerequired':
|
|
|
1667 |
if (!is_null($val)) {
|
|
|
1668 |
$descriptions[] = get_string('completionscorerequireddesc', 'scorm', $val);
|
|
|
1669 |
}
|
|
|
1670 |
break;
|
|
|
1671 |
case 'completionstatusallscos':
|
|
|
1672 |
if (!empty($val)) {
|
|
|
1673 |
$descriptions[] = get_string('completionstatusallscos', 'scorm');
|
|
|
1674 |
}
|
|
|
1675 |
break;
|
|
|
1676 |
default:
|
|
|
1677 |
break;
|
|
|
1678 |
}
|
|
|
1679 |
}
|
|
|
1680 |
return $descriptions;
|
|
|
1681 |
}
|
|
|
1682 |
|
|
|
1683 |
/**
|
|
|
1684 |
* This function will update the scorm module according to the
|
|
|
1685 |
* event that has been modified.
|
|
|
1686 |
*
|
|
|
1687 |
* It will set the timeopen or timeclose value of the scorm instance
|
|
|
1688 |
* according to the type of event provided.
|
|
|
1689 |
*
|
|
|
1690 |
* @throws \moodle_exception
|
|
|
1691 |
* @param \calendar_event $event
|
|
|
1692 |
* @param stdClass $scorm The module instance to get the range from
|
|
|
1693 |
*/
|
|
|
1694 |
function mod_scorm_core_calendar_event_timestart_updated(\calendar_event $event, \stdClass $scorm) {
|
|
|
1695 |
global $DB;
|
|
|
1696 |
|
|
|
1697 |
if (empty($event->instance) || $event->modulename != 'scorm') {
|
|
|
1698 |
return;
|
|
|
1699 |
}
|
|
|
1700 |
|
|
|
1701 |
if ($event->instance != $scorm->id) {
|
|
|
1702 |
return;
|
|
|
1703 |
}
|
|
|
1704 |
|
|
|
1705 |
if (!in_array($event->eventtype, [SCORM_EVENT_TYPE_OPEN, SCORM_EVENT_TYPE_CLOSE])) {
|
|
|
1706 |
return;
|
|
|
1707 |
}
|
|
|
1708 |
|
|
|
1709 |
$courseid = $event->courseid;
|
|
|
1710 |
$modulename = $event->modulename;
|
|
|
1711 |
$instanceid = $event->instance;
|
|
|
1712 |
$modified = false;
|
|
|
1713 |
|
|
|
1714 |
$coursemodule = get_fast_modinfo($courseid)->instances[$modulename][$instanceid];
|
|
|
1715 |
$context = context_module::instance($coursemodule->id);
|
|
|
1716 |
|
|
|
1717 |
// The user does not have the capability to modify this activity.
|
|
|
1718 |
if (!has_capability('moodle/course:manageactivities', $context)) {
|
|
|
1719 |
return;
|
|
|
1720 |
}
|
|
|
1721 |
|
|
|
1722 |
if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
|
|
|
1723 |
// If the event is for the scorm activity opening then we should
|
|
|
1724 |
// set the start time of the scorm activity to be the new start
|
|
|
1725 |
// time of the event.
|
|
|
1726 |
if ($scorm->timeopen != $event->timestart) {
|
|
|
1727 |
$scorm->timeopen = $event->timestart;
|
|
|
1728 |
$scorm->timemodified = time();
|
|
|
1729 |
$modified = true;
|
|
|
1730 |
}
|
|
|
1731 |
} else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
|
|
|
1732 |
// If the event is for the scorm activity closing then we should
|
|
|
1733 |
// set the end time of the scorm activity to be the new start
|
|
|
1734 |
// time of the event.
|
|
|
1735 |
if ($scorm->timeclose != $event->timestart) {
|
|
|
1736 |
$scorm->timeclose = $event->timestart;
|
|
|
1737 |
$modified = true;
|
|
|
1738 |
}
|
|
|
1739 |
}
|
|
|
1740 |
|
|
|
1741 |
if ($modified) {
|
|
|
1742 |
$scorm->timemodified = time();
|
|
|
1743 |
$DB->update_record('scorm', $scorm);
|
|
|
1744 |
$event = \core\event\course_module_updated::create_from_cm($coursemodule, $context);
|
|
|
1745 |
$event->trigger();
|
|
|
1746 |
}
|
|
|
1747 |
}
|
|
|
1748 |
|
|
|
1749 |
/**
|
|
|
1750 |
* This function calculates the minimum and maximum cutoff values for the timestart of
|
|
|
1751 |
* the given event.
|
|
|
1752 |
*
|
|
|
1753 |
* It will return an array with two values, the first being the minimum cutoff value and
|
|
|
1754 |
* the second being the maximum cutoff value. Either or both values can be null, which
|
|
|
1755 |
* indicates there is no minimum or maximum, respectively.
|
|
|
1756 |
*
|
|
|
1757 |
* If a cutoff is required then the function must return an array containing the cutoff
|
|
|
1758 |
* timestamp and error string to display to the user if the cutoff value is violated.
|
|
|
1759 |
*
|
|
|
1760 |
* A minimum and maximum cutoff return value will look like:
|
|
|
1761 |
* [
|
|
|
1762 |
* [1505704373, 'The date must be after this date'],
|
|
|
1763 |
* [1506741172, 'The date must be before this date']
|
|
|
1764 |
* ]
|
|
|
1765 |
*
|
|
|
1766 |
* @param \calendar_event $event The calendar event to get the time range for
|
|
|
1767 |
* @param \stdClass $instance The module instance to get the range from
|
|
|
1768 |
* @return array Returns an array with min and max date.
|
|
|
1769 |
*/
|
|
|
1770 |
function mod_scorm_core_calendar_get_valid_event_timestart_range(\calendar_event $event, \stdClass $instance) {
|
|
|
1771 |
$mindate = null;
|
|
|
1772 |
$maxdate = null;
|
|
|
1773 |
|
|
|
1774 |
if ($event->eventtype == SCORM_EVENT_TYPE_OPEN) {
|
|
|
1775 |
// The start time of the open event can't be equal to or after the
|
|
|
1776 |
// close time of the scorm activity.
|
|
|
1777 |
if (!empty($instance->timeclose)) {
|
|
|
1778 |
$maxdate = [
|
|
|
1779 |
$instance->timeclose,
|
|
|
1780 |
get_string('openafterclose', 'scorm')
|
|
|
1781 |
];
|
|
|
1782 |
}
|
|
|
1783 |
} else if ($event->eventtype == SCORM_EVENT_TYPE_CLOSE) {
|
|
|
1784 |
// The start time of the close event can't be equal to or earlier than the
|
|
|
1785 |
// open time of the scorm activity.
|
|
|
1786 |
if (!empty($instance->timeopen)) {
|
|
|
1787 |
$mindate = [
|
|
|
1788 |
$instance->timeopen,
|
|
|
1789 |
get_string('closebeforeopen', 'scorm')
|
|
|
1790 |
];
|
|
|
1791 |
}
|
|
|
1792 |
}
|
|
|
1793 |
|
|
|
1794 |
return [$mindate, $maxdate];
|
|
|
1795 |
}
|
|
|
1796 |
|
|
|
1797 |
/**
|
|
|
1798 |
* Given an array with a file path, it returns the itemid and the filepath for the defined filearea.
|
|
|
1799 |
*
|
|
|
1800 |
* @param string $filearea The filearea.
|
|
|
1801 |
* @param array $args The path (the part after the filearea and before the filename).
|
|
|
1802 |
* @return array The itemid and the filepath inside the $args path, for the defined filearea.
|
|
|
1803 |
*/
|
|
|
1804 |
function mod_scorm_get_path_from_pluginfile(string $filearea, array $args): array {
|
|
|
1805 |
// SCORM never has an itemid (the number represents the revision but it's not stored in database).
|
|
|
1806 |
array_shift($args);
|
|
|
1807 |
|
|
|
1808 |
// Get the filepath.
|
|
|
1809 |
if (empty($args)) {
|
|
|
1810 |
$filepath = '/';
|
|
|
1811 |
} else {
|
|
|
1812 |
$filepath = '/' . implode('/', $args) . '/';
|
|
|
1813 |
}
|
|
|
1814 |
|
|
|
1815 |
return [
|
|
|
1816 |
'itemid' => 0,
|
|
|
1817 |
'filepath' => $filepath,
|
|
|
1818 |
];
|
|
|
1819 |
}
|
|
|
1820 |
|
|
|
1821 |
/**
|
|
|
1822 |
* Callback to fetch the activity event type lang string.
|
|
|
1823 |
*
|
|
|
1824 |
* @param string $eventtype The event type.
|
|
|
1825 |
* @return lang_string The event type lang string.
|
|
|
1826 |
*/
|
|
|
1827 |
function mod_scorm_core_calendar_get_event_action_string(string $eventtype): string {
|
|
|
1828 |
$modulename = get_string('modulename', 'scorm');
|
|
|
1829 |
|
|
|
1830 |
switch ($eventtype) {
|
|
|
1831 |
case SCORM_EVENT_TYPE_OPEN:
|
|
|
1832 |
$identifier = 'calendarstart';
|
|
|
1833 |
break;
|
|
|
1834 |
case SCORM_EVENT_TYPE_CLOSE:
|
|
|
1835 |
$identifier = 'calendarend';
|
|
|
1836 |
break;
|
|
|
1837 |
default:
|
|
|
1838 |
return get_string('requiresaction', 'calendar', $modulename);
|
|
|
1839 |
}
|
|
|
1840 |
|
|
|
1841 |
return get_string($identifier, 'scorm', $modulename);
|
|
|
1842 |
}
|
|
|
1843 |
|
|
|
1844 |
/**
|
|
|
1845 |
* This function extends the settings navigation block for the site.
|
|
|
1846 |
*
|
|
|
1847 |
* It is safe to rely on PAGE here as we will only ever be within the module
|
|
|
1848 |
* context when this is called
|
|
|
1849 |
*
|
|
|
1850 |
* @param settings_navigation $settings navigation_node object.
|
|
|
1851 |
* @param navigation_node $scormnode navigation_node object.
|
|
|
1852 |
* @return void
|
|
|
1853 |
*/
|
|
|
1854 |
function scorm_extend_settings_navigation(settings_navigation $settings, navigation_node $scormnode): void {
|
|
|
1855 |
if (has_capability('mod/scorm:viewreport', $settings->get_page()->cm->context)) {
|
|
|
1856 |
$url = new moodle_url('/mod/scorm/report.php', ['id' => $settings->get_page()->cm->id]);
|
|
|
1857 |
$scormnode->add(get_string("reports", "scorm"), $url, navigation_node::TYPE_CUSTOM, null, 'scormreport');
|
|
|
1858 |
}
|
|
|
1859 |
}
|