SCORM MDL-28111 tidy up formatting of code
[moodle.git] / mod / scorm / locallib.php
CommitLineData
e5dd8e3b 1<?php
f7b5c6aa
DM
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/>.
f69db63e 16
9528568b 17require_once("$CFG->dirroot/mod/scorm/lib.php");
64f93798 18require_once("$CFG->libdir/filelib.php");
9528568b 19
a30b6819 20/// Constants and settings for module scorm
a679d64d 21define('UPDATE_NEVER', '0');
22define('UPDATE_ONCHANGE', '1');
23define('UPDATE_EVERYDAY', '2');
24define('UPDATE_EVERYTIME', '3');
25
b3659259 26define('SCO_ALL', 0);
27define('SCO_DATA', 1);
28define('SCO_ONLY', 2);
a30b6819 29
30define('GRADESCOES', '0');
31define('GRADEHIGHEST', '1');
32define('GRADEAVERAGE', '2');
33define('GRADESUM', '3');
a30b6819 34
35define('HIGHESTATTEMPT', '0');
36define('AVERAGEATTEMPT', '1');
37define('FIRSTATTEMPT', '2');
38define('LASTATTEMPT', '3');
a30b6819 39
7cc7cf58
DM
40define('TOCJSLINK', 1);
41define('TOCFULLURL', 2);
42
9528568b 43/// Local Library of functions for module scorm
a30b6819 44
64f93798
PS
45/**
46 * @package mod-scorm
47 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
48 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49 */
50class scorm_package_file_info extends file_info_stored {
51 public function get_parent() {
52 if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
53 return $this->browser->get_file_info($this->context);
54 }
55 return parent::get_parent();
56 }
57 public function get_visible_name() {
58 if ($this->lf->get_filepath() === '/' and $this->lf->get_filename() === '.') {
59 return $this->topvisiblename;
60 }
61 return parent::get_visible_name();
62 }
63}
64
30fc6e2d 65/**
66 * Returns an array of the popup options for SCORM and each options default value
7554f671 67 *
30fc6e2d 68 * @return array an array of popup options as the key and their defaults as the value
69 */
70function scorm_get_popup_options_array(){
71 global $CFG;
72 $cfg_scorm = get_config('scorm');
7554f671 73
74 return array('resizable'=> isset($cfg_scorm->resizable) ? $cfg_scorm->resizable : 0,
75 'scrollbars'=> isset($cfg_scorm->scrollbars) ? $cfg_scorm->scrollbars : 0,
76 'directories'=> isset($cfg_scorm->directories) ? $cfg_scorm->directories : 0,
1adc77e6 77 'location'=> isset($cfg_scorm->location) ? $cfg_scorm->location : 0,
7554f671 78 'menubar'=> isset($cfg_scorm->menubar) ? $cfg_scorm->menubar : 0,
79 'toolbar'=> isset($cfg_scorm->toolbar) ? $cfg_scorm->toolbar : 0,
80 'status'=> isset($cfg_scorm->status) ? $cfg_scorm->status : 0);
30fc6e2d 81}
82
83/**
84 * Returns an array of the array of what grade options
7554f671 85 *
30fc6e2d 86 * @return array an array of what grade options
87 */
88function scorm_get_grade_method_array(){
89 return array (GRADESCOES => get_string('gradescoes', 'scorm'),
90 GRADEHIGHEST => get_string('gradehighest', 'scorm'),
91 GRADEAVERAGE => get_string('gradeaverage', 'scorm'),
7554f671 92 GRADESUM => get_string('gradesum', 'scorm'));
30fc6e2d 93}
94
95/**
96 * Returns an array of the array of what grade options
7554f671 97 *
30fc6e2d 98 * @return array an array of what grade options
99 */
100function scorm_get_what_grade_array(){
101 return array (HIGHESTATTEMPT => get_string('highestattempt', 'scorm'),
102 AVERAGEATTEMPT => get_string('averageattempt', 'scorm'),
103 FIRSTATTEMPT => get_string('firstattempt', 'scorm'),
104 LASTATTEMPT => get_string('lastattempt', 'scorm'));
105}
106
107/**
108 * Returns an array of the array of skip view options
7554f671 109 *
30fc6e2d 110 * @return array an array of skip view options
111 */
112function scorm_get_skip_view_array(){
113 return array(0 => get_string('never'),
114 1 => get_string('firstaccess','scorm'),
115 2 => get_string('always'));
116}
117
118/**
119 * Returns an array of the array of hide table of contents options
7554f671 120 *
30fc6e2d 121 * @return array an array of hide table of contents options
122 */
123function scorm_get_hidetoc_array(){
124 return array(0 =>get_string('sided','scorm'),
125 1 => get_string('hidden','scorm'),
126 2 => get_string('popupmenu','scorm'));
127}
128
129/**
130 * Returns an array of the array of update frequency options
7554f671 131 *
30fc6e2d 132 * @return array an array of update frequency options
133 */
134function scorm_get_updatefreq_array(){
135 return array(0 => get_string('never'),
136 1 => get_string('onchanges','scorm'),
137 2 => get_string('everyday','scorm'),
138 3 => get_string('everytime','scorm'));
139}
140
141/**
142 * Returns an array of the array of popup display options
7554f671 143 *
30fc6e2d 144 * @return array an array of popup display options
145 */
146function scorm_get_popup_display_array(){
d67eb434 147 return array(0 => get_string('currentwindow', 'scorm'),
30fc6e2d 148 1 => get_string('popup', 'scorm'));
149}
150
151/**
152 * Returns an array of the array of attempt options
7554f671 153 *
30fc6e2d 154 * @return array an array of attempt options
155 */
156function scorm_get_attempts_array(){
157 $attempts = array(0 => get_string('nolimit','scorm'),
158 1 => get_string('attempt1','scorm'));
7554f671 159
30fc6e2d 160 for ($i=2; $i<=6; $i++) {
161 $attempts[$i] = get_string('attemptsx','scorm', $i);
162 }
7554f671 163
30fc6e2d 164 return $attempts;
165}
9528568b 166/**
167 * Extracts scrom package, sets up all variables.
168 * Called whenever scorm changes
169 * @param object $scorm instance - fields are updated and changes saved into database
170 * @param bool $full force full update if true
171 * @return void
172 */
173function scorm_parse($scorm, $full) {
174 global $CFG, $DB;
30fc6e2d 175 $cfg_scorm = get_config('scorm');
8aee93f1 176
9528568b 177 if (!isset($scorm->cmid)) {
178 $cm = get_coursemodule_from_instance('scorm', $scorm->id);
179 $scorm->cmid = $cm->id;
180 }
181 $context = get_context_instance(CONTEXT_MODULE, $scorm->cmid);
182 $newhash = $scorm->sha1hash;
a30b6819 183
9528568b 184 if ($scorm->scormtype === SCORM_TYPE_LOCAL or $scorm->scormtype === SCORM_TYPE_LOCALSYNC) {
a30b6819 185
9528568b 186 $fs = get_file_storage();
187 $packagefile = false;
f69db63e 188
9528568b 189 if ($scorm->scormtype === SCORM_TYPE_LOCAL) {
64f93798 190 if ($packagefile = $fs->get_file($context->id, 'mod_scorm', 'package', 0, '/', $scorm->reference)) {
9528568b 191 $newhash = $packagefile->get_contenthash();
192 } else {
193 $newhash = null;
194 }
195 } else {
30fc6e2d 196 if (!$cfg_scorm->allowtypelocalsync) {
9528568b 197 // sorry - localsync disabled
198 return;
199 }
200 if ($scorm->reference !== '' and (!$full or $scorm->sha1hash !== sha1($scorm->reference))) {
64f93798
PS
201 $fs->delete_area_files($context->id, 'mod_scorm', 'package');
202 $file_record = array('contextid'=>$context->id, 'component'=>'mod_scorm', 'filearea'=>'package', 'itemid'=>0, 'filepath'=>'/');
60b5a2fe 203 if ($packagefile = $fs->create_file_from_url($file_record, $scorm->reference, array('calctimeout' => true))) {
9528568b 204 $newhash = sha1($scorm->reference);
5c1ac70c 205 } else {
9528568b 206 $newhash = null;
5c1ac70c 207 }
f69db63e 208 }
209 }
f69db63e 210
9528568b 211 if ($packagefile) {
212 if (!$full and $packagefile and $scorm->sha1hash === $newhash) {
213 if (strpos($scorm->version, 'SCORM') !== false) {
64f93798 214 if ($fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
9528568b 215 // no need to update
216 return;
e4aa175a 217 }
9528568b 218 } else if (strpos($scorm->version, 'AICC') !== false) {
219 // TODO: add more sanity checks - something really exists in scorm_content area
220 return;
221 }
222 }
223
224 // now extract files
64f93798 225 $fs->delete_area_files($context->id, 'mod_scorm', 'content');
9528568b 226
227 $packer = get_file_packer('application/zip');
64f93798 228 $packagefile->extract_to_storage($packer, $context->id, 'mod_scorm', 'content', 0, '/');
9528568b 229
230 } else if (!$full) {
231 return;
232 }
233
234
64f93798 235 if ($manifest = $fs->get_file($context->id, 'mod_scorm', 'content', 0, '/', 'imsmanifest.xml')) {
9528568b 236 require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
237 // SCORM
238 if (!scorm_parse_scorm($scorm, $manifest)) {
239 $scorm->version = 'ERROR';
240 }
241 } else {
242 require_once("$CFG->dirroot/mod/scorm/datamodels/aicclib.php");
243 // AICC
244 if (!scorm_parse_aicc($scorm)) {
245 $scorm->version = 'ERROR';
246 }
247 }
248
30fc6e2d 249 } else if ($scorm->scormtype === SCORM_TYPE_EXTERNAL and $cfg_scorm->allowtypeexternal) {
9528568b 250 if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
251 return;
252 }
253 require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
254 // SCORM only, AICC can not be external
255 if (!scorm_parse_scorm($scorm, $scorm->reference)) {
256 $scorm->version = 'ERROR';
257 }
258 $newhash = sha1($scorm->reference);
259
30fc6e2d 260 } else if ($scorm->scormtype === SCORM_TYPE_IMSREPOSITORY and !empty($CFG->repositoryactivate) and $cfg_scorm->allowtypeimsrepository) {
9528568b 261 if (!$full and $scorm->sha1hash === sha1($scorm->reference)) {
262 return;
263 }
264 require_once("$CFG->dirroot/mod/scorm/datamodels/scormlib.php");
265 if (!scorm_parse_scorm($scorm, $CFG->repository.substr($scorm->reference,1).'/imsmanifest.xml')) {
266 $scorm->version = 'ERROR';
267 }
268 $newhash = sha1($scorm->reference);
269
e4aa175a 270 } else {
9528568b 271 // sorry, disabled type
272 return;
e4aa175a 273 }
9528568b 274
275 $scorm->revision++;
276 $scorm->sha1hash = $newhash;
277 $DB->update_record('scorm', $scorm);
e4aa175a 278}
279
9528568b 280
2b3447c3 281function scorm_array_search($item, $needle, $haystacks, $strict=false) {
282 if (!empty($haystacks)) {
283 foreach ($haystacks as $key => $element) {
284 if ($strict) {
285 if ($element->{$item} === $needle) {
286 return $key;
287 }
288 } else {
289 if ($element->{$item} == $needle) {
290 return $key;
e4aa175a 291 }
292 }
e4aa175a 293 }
294 }
2b3447c3 295 return false;
e4aa175a 296}
297
2b3447c3 298function scorm_repeater($what, $times) {
299 if ($times <= 0) {
300 return null;
301 }
302 $return = '';
303 for ($i=0; $i<$times;$i++) {
304 $return .= $what;
305 }
306 return $return;
307}
e4aa175a 308
2b3447c3 309function scorm_external_link($link) {
310// check if a link is external
311 $result = false;
312 $link = strtolower($link);
313 if (substr($link,0,7) == 'http://') {
314 $result = true;
315 } else if (substr($link,0,8) == 'https://') {
316 $result = true;
317 } else if (substr($link,0,4) == 'www.') {
318 $result = true;
319 }
320 return $result;
e4aa175a 321}
322
b3659259 323/**
324* Returns an object containing all datas relative to the given sco ID
325*
326* @param integer $id The sco ID
327* @return mixed (false if sco id does not exists)
328*/
bd3523a5 329
b3659259 330function scorm_get_sco($id,$what=SCO_ALL) {
bf347041 331 global $DB;
332
333 if ($sco = $DB->get_record('scorm_scoes', array('id'=>$id))) {
b3659259 334 $sco = ($what == SCO_DATA) ? new stdClass() : $sco;
bf347041 335 if (($what != SCO_ONLY) && ($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id)))) {
b3659259 336 foreach ($scodatas as $scodata) {
c31f631b 337 $sco->{$scodata->name} = $scodata->value;
b3659259 338 }
bf347041 339 } else if (($what != SCO_ONLY) && (!($scodatas = $DB->get_records('scorm_scoes_data', array('scoid'=>$id))))) {
9528568b 340 $sco->parameters = '';
b3659259 341 }
342 return $sco;
343 } else {
344 return false;
345 }
346}
82605bea 347
348/**
349* Returns an object (array) containing all the scoes data related to the given sco ID
350*
351* @param integer $id The sco ID
352* @param integer $organisation an organisation ID - defaults to false if not required
353* @return mixed (false if there are no scoes or an array)
354*/
355
356function scorm_get_scoes($id,$organisation=false) {
b44c704f 357 global $DB;
358
82605bea 359 $organizationsql = '';
b44c704f 360 $queryarray = array('scorm'=>$id);
82605bea 361 if (!empty($organisation)) {
b44c704f 362 $queryarray['organization'] = $organisation;
82605bea 363 }
b44c704f 364 if ($scoes = $DB->get_records('scorm_scoes', $queryarray, 'id ASC')) {
82605bea 365 // drop keys so that it is a simple array as expected
366 $scoes = array_values($scoes);
367 foreach ($scoes as $sco) {
b44c704f 368 if ($scodatas = $DB->get_records('scorm_scoes_data',array('scoid'=>$sco->id))) {
82605bea 369 foreach ($scodatas as $scodata) {
2fd0e9fe 370 $sco->{$scodata->name} = $scodata->value;
82605bea 371 }
372 }
373 }
374 return $scoes;
375 } else {
376 return false;
377 }
378}
379
6381fa56 380function scorm_insert_track($userid,$scormid,$scoid,$attempt,$element,$value,$forcecompleted=false) {
86996ffe 381 global $DB, $CFG;
bf347041 382
e4aa175a 383 $id = null;
6381fa56 384
385 if ($forcecompleted) {
386 //TODO - this could be broadened to encompass SCORM 2004 in future
387 if (($element == 'cmi.core.lesson_status') && ($value == 'incomplete')) {
388 if ($track = $DB->get_record_select('scorm_scoes_track','userid=? AND scormid=? AND scoid=? AND attempt=? AND element=\'cmi.core.score.raw\'', array($userid, $scormid, $scoid, $attempt))) {
389 $value = 'completed';
390 }
391 }
392 if ($element == 'cmi.core.score.raw') {
393 if ($tracktest = $DB->get_record_select('scorm_scoes_track','userid=? AND scormid=? AND scoid=? AND attempt=? AND element=\'cmi.core.lesson_status\'', array($userid, $scormid, $scoid, $attempt))) {
394 if ($tracktest->value == "incomplete") {
395 $tracktest->value = "completed";
dd88de0e 396 $DB->update_record('scorm_scoes_track',$tracktest);
6381fa56 397 }
398 }
399 }
400 }
401
bf347041 402 if ($track = $DB->get_record('scorm_scoes_track',array('userid'=>$userid, 'scormid'=>$scormid, 'scoid'=>$scoid, 'attempt'=>$attempt, 'element'=>$element))) {
d1818fdc 403 if ($element != 'x.start.time' ) { //don't update x.start.time - keep the original value.
454f5aee 404 $track->value = $value;
d1818fdc 405 $track->timemodified = time();
dd88de0e
PS
406 $DB->update_record('scorm_scoes_track',$track);
407 $id = $track->id;
d1818fdc 408 }
e4aa175a 409 } else {
410 $track->userid = $userid;
411 $track->scormid = $scormid;
412 $track->scoid = $scoid;
413 $track->attempt = $attempt;
414 $track->element = $element;
454f5aee 415 $track->value = $value;
e4aa175a 416 $track->timemodified = time();
bf347041 417 $id = $DB->insert_record('scorm_scoes_track',$track);
e4aa175a 418 }
9528568b 419
9528568b 420 if (strstr($element, '.score.raw') ||
a0b36684 421 (($element == 'cmi.core.lesson_status' || $element == 'cmi.completion_status') && ($track->value == 'completed' || $track->value == 'passed'))) {
422 $scorm = $DB->get_record('scorm', array('id' => $scormid));
86996ffe 423 include_once($CFG->dirroot.'/mod/scorm/lib.php');
7f7946fd 424 scorm_update_grades($scorm, $userid);
d23121ab 425 }
9528568b 426
e4aa175a 427 return $id;
428}
429
e4aa175a 430function scorm_get_tracks($scoid,$userid,$attempt='') {
e4aa175a 431/// Gets all tracks of specified sco and user
bf347041 432 global $CFG, $DB;
e4aa175a 433
434 if (empty($attempt)) {
bf347041 435 if ($scormid = $DB->get_field('scorm_scoes','scorm', array('id'=>$scoid))) {
e4aa175a 436 $attempt = scorm_get_last_attempt($scormid,$userid);
437 } else {
438 $attempt = 1;
439 }
440 }
bf347041 441 if ($tracks = $DB->get_records('scorm_scoes_track', array('userid'=>$userid, 'scoid'=>$scoid, 'attempt'=>$attempt),'element ASC')) {
39790bd8 442 $usertrack = new stdClass();
e4aa175a 443 $usertrack->userid = $userid;
9528568b 444 $usertrack->scoid = $scoid;
a30b6819 445 // Defined in order to unify scorm1.2 and scorm2004
e4aa175a 446 $usertrack->score_raw = '';
e4aa175a 447 $usertrack->status = '';
e4aa175a 448 $usertrack->total_time = '00:00:00';
449 $usertrack->session_time = '00:00:00';
450 $usertrack->timemodified = 0;
451 foreach ($tracks as $track) {
452 $element = $track->element;
453 $usertrack->{$element} = $track->value;
454 switch ($element) {
f69db63e 455 case 'cmi.core.lesson_status':
456 case 'cmi.completion_status':
457 if ($track->value == 'not attempted') {
458 $track->value = 'notattempted';
9528568b 459 }
f69db63e 460 $usertrack->status = $track->value;
9528568b 461 break;
e4aa175a 462 case 'cmi.core.score.raw':
463 case 'cmi.score.raw':
0d55dd4d 464 $usertrack->score_raw = (float) sprintf('%2.2f', $track->value);
9528568b 465 break;
e4aa175a 466 case 'cmi.core.session_time':
467 case 'cmi.session_time':
468 $usertrack->session_time = $track->value;
9528568b 469 break;
e4aa175a 470 case 'cmi.core.total_time':
471 case 'cmi.total_time':
472 $usertrack->total_time = $track->value;
9528568b 473 break;
474 }
e4aa175a 475 if (isset($track->timemodified) && ($track->timemodified > $usertrack->timemodified)) {
476 $usertrack->timemodified = $track->timemodified;
9528568b 477 }
3505e82b 478 }
9528568b 479 if (is_array($usertrack)) {
07b905ae 480 ksort($usertrack);
481 }
e4aa175a 482 return $usertrack;
483 } else {
484 return false;
485 }
486}
487
51c1c4e1 488
489/* Find the start and finsh time for a a given SCO attempt
490 *
491 * @param int $scormid SCORM Id
492 * @param int $scoid SCO Id
493 * @param int $userid User Id
494 * @param int $attemt Attempt Id
495 *
496 * @return object start and finsh time EPOC secods
497 *
498 */
499function scorm_get_sco_runtime($scormid, $scoid, $userid, $attempt=1) {
4454447d 500 global $DB;
51c1c4e1 501
39790bd8 502 $timedata = new stdClass();
51c1c4e1 503 $sql = !empty($scoid) ? "userid=$userid AND scormid=$scormid AND scoid=$scoid AND attempt=$attempt" : "userid=$userid AND scormid=$scormid AND attempt=$attempt";
504 $tracks = $DB->get_records_select('scorm_scoes_track',"$sql ORDER BY timemodified ASC");
505 if ($tracks) {
506 $tracks = array_values($tracks);
507 }
508
c86a91d5 509 if ($tracks) {
51c1c4e1 510 $timedata->start = $tracks[0]->timemodified;
511 }
512 else {
513 $timedata->start = false;
514 }
515 if ($tracks && $track = array_pop($tracks)) {
516 $timedata->finish = $track->timemodified;
517 }
518 else {
519 $timedata->finish = $timedata->start;
520 }
521 return $timedata;
522}
523
524
2b3447c3 525function scorm_get_user_data($userid) {
bf347041 526 global $DB;
2b3447c3 527/// Gets user info required to display the table of scorm results
528/// for report.php
e4aa175a 529
629f5021 530 return $DB->get_record('user', array('id'=>$userid), user_picture::fields());
2b3447c3 531}
e4aa175a 532
c0f6b608 533function scorm_grade_user_attempt($scorm, $userid, $attempt=1) {
bf347041 534 global $DB;
9528568b 535 $attemptscore = NULL;
a30b6819 536 $attemptscore->scoes = 0;
537 $attemptscore->values = 0;
538 $attemptscore->max = 0;
539 $attemptscore->sum = 0;
540 $attemptscore->lastmodify = 0;
9528568b 541
bf347041 542 if (!$scoes = $DB->get_records('scorm_scoes', array('scorm'=>$scorm->id))) {
a30b6819 543 return NULL;
e4aa175a 544 }
e4aa175a 545
9528568b 546 foreach ($scoes as $sco) {
2b3447c3 547 if ($userdata=scorm_get_tracks($sco->id, $userid,$attempt)) {
548 if (($userdata->status == 'completed') || ($userdata->status == 'passed')) {
a30b6819 549 $attemptscore->scoes++;
9528568b 550 }
5c2aa157 551 if (!empty($userdata->score_raw) || (isset($scorm->type) && $scorm->type=='sco' && isset($userdata->score_raw))) {
a30b6819 552 $attemptscore->values++;
553 $attemptscore->sum += $userdata->score_raw;
554 $attemptscore->max = ($userdata->score_raw > $attemptscore->max)?$userdata->score_raw:$attemptscore->max;
555 if (isset($userdata->timemodified) && ($userdata->timemodified > $attemptscore->lastmodify)) {
556 $attemptscore->lastmodify = $userdata->timemodified;
557 } else {
558 $attemptscore->lastmodify = 0;
559 }
9528568b 560 }
561 }
e4aa175a 562 }
03751efe 563 switch ($scorm->grademethod) {
a30b6819 564 case GRADEHIGHEST:
0d55dd4d 565 $score = (float) $attemptscore->max;
9528568b 566 break;
a30b6819 567 case GRADEAVERAGE:
568 if ($attemptscore->values > 0) {
569 $score = $attemptscore->sum/$attemptscore->values;
5c1ac70c 570 } else {
a30b6819 571 $score = 0;
9528568b 572 }
573 break;
a30b6819 574 case GRADESUM:
575 $score = $attemptscore->sum;
9528568b 576 break;
a30b6819 577 case GRADESCOES:
578 $score = $attemptscore->scoes;
7ef16bf9 579 break;
580 default:
581 $score = $attemptscore->max; // Remote Learner GRADEHIGHEST is default
5c1ac70c 582 }
a30b6819 583
c0f6b608 584 return $score;
a30b6819 585}
586
8c5516dc 587function scorm_grade_user($scorm, $userid) {
a30b6819 588
03751efe 589 // ensure we dont grade user beyond $scorm->maxattempt settings
7ce6eb87 590 $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
591 if($scorm->maxattempt != 0 && $lastattempt >= $scorm->maxattempt){
592 $lastattempt = $scorm->maxattempt;
593 }
594
03751efe 595 switch ($scorm->whatgrade) {
a30b6819 596 case FIRSTATTEMPT:
8c5516dc 597 return scorm_grade_user_attempt($scorm, $userid, 1);
9528568b 598 break;
a30b6819 599 case LASTATTEMPT:
8c5516dc 600 return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
a30b6819 601 break;
602 case HIGHESTATTEMPT:
a30b6819 603 $maxscore = 0;
a30b6819 604 for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
8c5516dc
DM
605 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
606 $maxscore = $attemptscore > $maxscore ? $attemptscore: $maxscore;
a30b6819 607 }
8c5516dc
DM
608 return $maxscore;
609
a30b6819 610 break;
611 case AVERAGEATTEMPT:
b1ca5d31 612 $attemptcount = scorm_get_attempt_count($userid, $scorm, true);
b1ca5d31
DM
613 if (empty($attemptcount)) {
614 return 0;
615 } else {
616 $attemptcount = count($attemptcount);
617 }
a30b6819 618 $lastattempt = scorm_get_last_attempt($scorm->id, $userid);
619 $sumscore = 0;
620 for ($attempt = 1; $attempt <= $lastattempt; $attempt++) {
8c5516dc
DM
621 $attemptscore = scorm_grade_user_attempt($scorm, $userid, $attempt);
622 $sumscore += $attemptscore;
a30b6819 623 }
624
b1ca5d31 625 return round($sumscore / $attemptcount);
a30b6819 626 break;
627 }
e4aa175a 628}
629
8e45ba45 630function scorm_count_launchable($scormid,$organization='') {
bf347041 631 global $DB;
632
633 $sqlorganization = '';
634 $params = array($scormid);
8e45ba45 635 if (!empty($organization)) {
bf347041 636 $sqlorganization = " AND organization=?";
637 $params[] = $organization;
8e45ba45 638 }
84ab3289 639 return $DB->count_records_select('scorm_scoes',"scorm = ? $sqlorganization AND ".$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), $params);
e4aa175a 640}
641
2b3447c3 642function scorm_get_last_attempt($scormid, $userid) {
bf347041 643 global $DB;
644
2b3447c3 645/// Find the last attempt number for the given user id and scorm id
bf347041 646 if ($lastattempt = $DB->get_record('scorm_scoes_track', array('userid'=>$userid, 'scormid'=>$scormid), 'max(attempt) as a')) {
2b3447c3 647 if (empty($lastattempt->a)) {
648 return '1';
649 } else {
650 return $lastattempt->a;
e4aa175a 651 }
6381fa56 652 } else {
653 return false;
e4aa175a 654 }
e4aa175a 655}
656
2276208a
DM
657function scorm_get_last_completed_attempt($scormid, $userid) {
658 global $DB;
659
660/// Find the last attempt number for the given user id and scorm id
ed98c4e9 661 if ($lastattempt = $DB->get_record_select('scorm_scoes_track', "userid = ? AND scormid = ? AND (value='completed' OR value='passed')", array($userid, $scormid), 'max(attempt) as a')) {
2276208a
DM
662 if (empty($lastattempt->a)) {
663 return '1';
664 } else {
665 return $lastattempt->a;
666 }
667 } else {
668 return false;
669 }
670}
671
e4aa175a 672function scorm_course_format_display($user,$course) {
f2a1963c 673 global $CFG, $DB, $PAGE, $OUTPUT;
e4aa175a 674
675 $strupdate = get_string('update');
77bf0c29 676 $context = get_context_instance(CONTEXT_COURSE,$course->id);
e4aa175a 677
678 echo '<div class="mod-scorm">';
679 if ($scorms = get_all_instances_in_course('scorm', $course)) {
9528568b 680 // The module SCORM activity with the least id is the course
e4aa175a 681 $scorm = current($scorms);
682 if (! $cm = get_coursemodule_from_instance('scorm', $scorm->id, $course->id)) {
08b56f93 683 print_error('invalidcoursemodule');
e4aa175a 684 }
685 $colspan = '';
686 $headertext = '<table width="100%"><tr><td class="title">'.get_string('name').': <b>'.format_string($scorm->name).'</b>';
2b3447c3 687 if (has_capability('moodle/course:manageactivities', $context)) {
830dd6e9 688 if ($PAGE->user_is_editing()) {
e4aa175a 689 // Display update icon
690 $path = $CFG->wwwroot.'/course';
691 $headertext .= '<span class="commands">'.
692 '<a title="'.$strupdate.'" href="'.$path.'/mod.php?update='.$cm->id.'&amp;sesskey='.sesskey().'">'.
b5d0cafc 693 '<img src="'.$OUTPUT->pix_url('t/edit') . '" class="iconsmall" alt="'.$strupdate.'" /></a></span>';
e4aa175a 694 }
695 $headertext .= '</td>';
696 // Display report link
bf347041 697 $trackedusers = $DB->get_record('scorm_scoes_track', array('scormid'=>$scorm->id), 'count(distinct(userid)) as c');
e4aa175a 698 if ($trackedusers->c > 0) {
699 $headertext .= '<td class="reportlink">'.
4aea3cc7 700 '<a href="'.$CFG->wwwroot.'/mod/scorm/report.php?id='.$cm->id.'">'.
e4aa175a 701 get_string('viewallreports','scorm',$trackedusers->c).'</a>';
702 } else {
703 $headertext .= '<td class="reportlink">'.get_string('noreports','scorm');
704 }
705 $colspan = ' colspan="2"';
9528568b 706 }
ac3668bf 707 $headertext .= '</td></tr><tr><td'.$colspan.'>'.get_string('summary').':<br />'.format_module_intro('scorm', $scorm, $scorm->coursemodule).'</td></tr></table>';
6aff538a 708 echo $OUTPUT->box($headertext,'generalbox boxwidthwide');
39340358 709 scorm_view_display($user, $scorm, 'view.php?id='.$course->id, $cm);
e4aa175a 710 } else {
0d699c24 711 if (has_capability('moodle/course:update', $context)) {
e4aa175a 712 // Create a new activity
444ff001
DM
713 $url = new moodle_url('/course/mod.php', array('id'=>$course->id, 'section'=>'0', 'sesskey'=>sesskey(),'add'=>'scorm'));
714 redirect($url);
e4aa175a 715 } else {
6aff538a 716 echo $OUTPUT->notification('Could not find a scorm course here');
e4aa175a 717 }
718 }
719 echo '</div>';
720}
721
39340358 722function scorm_view_display ($user, $scorm, $action, $cm) {
4eeec9ac 723 global $CFG, $DB, $PAGE, $OUTPUT;
ab3b00e1 724
9528568b 725 if ($scorm->updatefreq == UPDATE_EVERYTIME) {
726 scorm_parse($scorm, false);
ab3b00e1 727 }
728
e4aa175a 729 $organization = optional_param('organization', '', PARAM_INT);
730
6381fa56 731 if($scorm->displaycoursestructure == 1) {
30bce58e 732 echo $OUTPUT->box_start('generalbox boxaligncenter toc');
e4aa175a 733?>
29a43013 734 <div class="structurehead"><?php print_string('contents','scorm') ?></div>
e4aa175a 735<?php
6381fa56 736 }
e4aa175a 737 if (empty($organization)) {
738 $organization = $scorm->launch;
739 }
2e3c31bc 740 if ($orgs = $DB->get_records_select_menu('scorm_scoes', 'scorm = ? AND '.
84ab3289
DM
741 $DB->sql_isempty('scorm_scoes', 'launch', false, true).' AND '.
742 $DB->sql_isempty('scorm_scoes', 'organization', false, false),
743 array($scorm->id),'id','id,title')) {
e4aa175a 744 if (count($orgs) > 1) {
edc28287 745 $select = new single_select(new moodle_url($action), 'organization', $orgs, $organization, null);
2e3c31bc 746 $select->label = get_string('organizations','scorm');
edc28287
PS
747 $select->class = 'scorm-center';
748 echo $OUTPUT->render($select);
e4aa175a 749 }
750 }
751 $orgidentifier = '';
b3659259 752 if ($sco = scorm_get_sco($organization, SCO_ONLY)) {
753 if (($sco->organization == '') && ($sco->launch == '')) {
754 $orgidentifier = $sco->identifier;
e4aa175a 755 } else {
b3659259 756 $orgidentifier = $sco->organization;
e4aa175a 757 }
758 }
28ffbff3 759
2b3447c3 760 $scorm->version = strtolower(clean_param($scorm->version, PARAM_SAFEDIR)); // Just to be safe
dbe7e6f6 761 if (!file_exists($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php')) {
762 $scorm->version = 'scorm_12';
763 }
2b3447c3 764 require_once($CFG->dirroot.'/mod/scorm/datamodels/'.$scorm->version.'lib.php');
765
334d8b94 766 $result = scorm_get_toc($user,$scorm,$cm->id,TOCFULLURL,$orgidentifier);
e4aa175a 767 $incomplete = $result->incomplete;
28ffbff3 768
6381fa56 769 // do we want the TOC to be displayed?
770 if($scorm->displaycoursestructure == 1) {
771 echo $result->toc;
6aff538a 772 echo $OUTPUT->box_end();
6381fa56 773 }
7554f671 774
6381fa56 775 // is this the first attempt ?
b1ca5d31 776 $attemptcount = scorm_get_attempt_count($user->id, $scorm);
7554f671 777
6381fa56 778 // do not give the player launch FORM if the SCORM object is locked after the final attempt
779 if ($scorm->lastattemptlock == 0 || $result->attemptleft > 0) {
e4aa175a 780?>
52a9a9b5 781 <div class="scorm-center">
4675994a 782 <form id="theform" method="post" action="<?php echo $CFG->wwwroot ?>/mod/scorm/player.php">
e4aa175a 783 <?php
e4aa175a 784 if ($scorm->hidebrowse == 0) {
8949f8df 785 print_string('mode','scorm');
e4aa175a 786 echo ': <input type="radio" id="b" name="mode" value="browse" /><label for="b">'.get_string('browse','scorm').'</label>'."\n";
76ea4fb4 787 echo '<input type="radio" id="n" name="mode" value="normal" checked="checked" /><label for="n">'.get_string('normal','scorm')."</label>\n";
e4aa175a 788 } else {
76ea4fb4 789 echo '<input type="hidden" name="mode" value="normal" />'."\n";
e4aa175a 790 }
6381fa56 791 if ($scorm->forcenewattempt == 1) {
792 if ($incomplete === false) {
793 echo '<input type="hidden" name="newattempt" value="on" />'."\n";
794 }
773a7468 795 } elseif (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
e4aa175a 796?>
6381fa56 797 <br />
798 <input type="checkbox" id="a" name="newattempt" />
799 <label for="a"><?php print_string('newattempt','scorm') ?></label>
e4aa175a 800<?php
801 }
802 ?>
803 <br />
4675994a 804 <input type="hidden" name="scoid"/>
3d00d6ab 805 <input type="hidden" name="cm" value="<?php echo $cm->id ?>"/>
e4aa175a 806 <input type="hidden" name="currentorg" value="<?php echo $orgidentifier ?>" />
2b600d16 807 <input type="submit" value="<?php print_string('enter','scorm') ?>" />
e4aa175a 808 </form>
809 </div>
810<?php
6381fa56 811 }
e4aa175a 812}
6280b17a 813
489797d9 814function scorm_simple_play($scorm,$user, $context) {
bf347041 815 global $DB;
816
9316beca 817 $result = false;
818
819 if ($scorm->updatefreq == UPDATE_EVERYTIME) {
820 scorm_parse($scorm, false);
821 }
64f93798 822 if (has_capability('mod/scorm:viewreport', $context)) { //if this user can view reports, don't skipview so they can see links to reports.
489797d9
DM
823 return $result;
824 }
9316beca 825
84ab3289 826 $scoes = $DB->get_records_select('scorm_scoes', 'scorm = ? AND '.$DB->sql_isnotempty('scorm_scoes', 'launch', false, true), array($scorm->id), 'id', 'id');
9316beca 827
7325b016 828 if ($scoes) {
9316beca 829 if ($scorm->skipview >= 1) {
830 $sco = current($scoes);
831 if (scorm_get_tracks($sco->id,$user->id) === false) {
832 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
833 $result = true;
834 } else if ($scorm->skipview == 2) {
835 header('Location: player.php?a='.$scorm->id.'&scoid='.$sco->id);
836 $result = true;
837 }
838 }
839 }
840 return $result;
28ffbff3 841}
d8c9d8a1 842
843function scorm_get_count_users($scormid, $groupingid=null) {
bf347041 844 global $CFG, $DB;
9528568b 845
98da6021 846 if (!empty($groupingid)) {
d8c9d8a1 847 $sql = "SELECT COUNT(DISTINCT st.userid)
bf347041 848 FROM {scorm_scoes_track} st
849 INNER JOIN {groups_members} gm ON st.userid = gm.userid
9528568b 850 INNER JOIN {groupings_groups} gg ON gm.groupid = gg.groupid
bf347041 851 WHERE st.scormid = ? AND gg.groupingid = ?
d8c9d8a1 852 ";
bf347041 853 $params = array($scormid, $groupingid);
d8c9d8a1 854 } else {
855 $sql = "SELECT COUNT(DISTINCT st.userid)
9528568b 856 FROM {scorm_scoes_track} st
bf347041 857 WHERE st.scormid = ?
d8c9d8a1 858 ";
bf347041 859 $params = array($scormid);
d8c9d8a1 860 }
9528568b 861
bf347041 862 return ($DB->count_records_sql($sql, $params));
d8c9d8a1 863}
864
527af457 865/**
866* Build up the JavaScript representation of an array element
867*
868* @param string $sversion SCORM API version
869* @param array $userdata User track data
870* @param string $element_name Name of array element to get values for
871* @param array $children list of sub elements of this array element that also need instantiating
872* @return None
7554f671 873*/
527af457 874function scorm_reconstitute_array_element($sversion, $userdata, $element_name, $children) {
875 // reconstitute comments_from_learner and comments_from_lms
876 $current = '';
bf34ac0f 877 $current_subelement = '';
878 $current_sub = '';
527af457 879 $count = 0;
bf34ac0f 880 $count_sub = 0;
6b635d44
DM
881 $scormseperator = '_';
882 if ($sversion == 'scorm_13') { //scorm 1.3 elements use a . instead of an _
883 $scormseperator = '.';
884 }
527af457 885 // filter out the ones we want
886 $element_list = array();
887 foreach($userdata as $element => $value){
888 if (substr($element,0,strlen($element_name)) == $element_name) {
889 $element_list[$element] = $value;
890 }
891 }
7554f671 892
527af457 893 // sort elements in .n array order
894 uksort($element_list, "scorm_element_cmp");
7554f671 895
527af457 896 // generate JavaScript
897 foreach($element_list as $element => $value){
898 if ($sversion == 'scorm_13') {
b6293233 899 $element = preg_replace('/\.(\d+)\./', ".N\$1.", $element);
527af457 900 preg_match('/\.(N\d+)\./', $element, $matches);
901 } else {
b6293233 902 $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
527af457 903 preg_match('/\_(\d+)\./', $element, $matches);
904 }
905 if (count($matches) > 0 && $current != $matches[1]) {
bf34ac0f 906 if ($count_sub > 0) {
6b635d44 907 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
bf34ac0f 908 }
7554f671 909 $current = $matches[1];
527af457 910 $count++;
bf34ac0f 911 $current_subelement = '';
912 $current_sub = '';
913 $count_sub = 0;
527af457 914 $end = strpos($element,$matches[1])+strlen($matches[1]);
915 $subelement = substr($element,0,$end);
916 echo ' '.$subelement." = new Object();\n";
917 // now add the children
918 foreach ($children as $child) {
919 echo ' '.$subelement.".".$child." = new Object();\n";
920 echo ' '.$subelement.".".$child."._children = ".$child."_children;\n";
921 }
922 }
7554f671 923
bf34ac0f 924 // now - flesh out the second level elements if there are any
925 if ($sversion == 'scorm_13') {
b6293233 926 $element = preg_replace('/(.*?\.N\d+\..*?)\.(\d+)\./', "\$1.N\$2.", $element);
bf34ac0f 927 preg_match('/.*?\.N\d+\.(.*?)\.(N\d+)\./', $element, $matches);
928 } else {
b6293233 929 $element = preg_replace('/(.*?\_\d+\..*?)\.(\d+)\./', "\$1_\$2.", $element);
bf34ac0f 930 preg_match('/.*?\_\d+\.(.*?)\_(\d+)\./', $element, $matches);
931 }
7554f671 932
bf34ac0f 933 // check the sub element type
934 if (count($matches) > 0 && $current_subelement != $matches[1]) {
935 if ($count_sub > 0) {
03e9a122 936 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
bf34ac0f 937 }
938 $current_subelement = $matches[1];
939 $current_sub = '';
940 $count_sub = 0;
941 $end = strpos($element,$matches[1])+strlen($matches[1]);
942 $subelement = substr($element,0,$end);
943 echo ' '.$subelement." = new Object();\n";
944 }
7554f671 945
bf34ac0f 946 // now check the subelement subscript
947 if (count($matches) > 0 && $current_sub != $matches[2]) {
7554f671 948 $current_sub = $matches[2];
bf34ac0f 949 $count_sub++;
950 $end = strrpos($element,$matches[2])+strlen($matches[2]);
951 $subelement = substr($element,0,$end);
952 echo ' '.$subelement." = new Object();\n";
953 }
7554f671 954
527af457 955 echo ' '.$element.' = \''.$value."';\n";
956 }
bf34ac0f 957 if ($count_sub > 0) {
6b635d44 958 echo ' '.$element_name.$scormseperator.$current.'.'.$current_subelement.'._count = '.$count_sub.";\n";
bf34ac0f 959 }
527af457 960 if ($count > 0) {
961 echo ' '.$element_name.'._count = '.$count.";\n";
962 }
963}
964
965/**
966* Build up the JavaScript representation of an array element
967*
968* @param string $a left array element
969* @param string $b right array element
970* @return comparator - 0,1,-1
7554f671 971*/
527af457 972function scorm_element_cmp($a, $b) {
bf34ac0f 973 preg_match('/.*?(\d+)\./', $a, $matches);
527af457 974 $left = intval($matches[1]);
bf34ac0f 975 preg_match('/.?(\d+)\./', $b, $matches);
527af457 976 $right = intval($matches[1]);
977 if ($left < $right) {
978 return -1; // smaller
979 } elseif ($left > $right) {
980 return 1; // bigger
981 } else {
bf34ac0f 982 // look for a second level qualifier eg cmi.interactions_0.correct_responses_0.pattern
983 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $a, $matches)) {
984 $leftterm = intval($matches[2]);
985 $left = intval($matches[3]);
986 if (preg_match('/.*?(\d+)\.(.*?)\.(\d+)\./', $b, $matches)) {
987 $rightterm = intval($matches[2]);
988 $right = intval($matches[3]);
989 if ($leftterm < $rightterm) {
990 return -1; // smaller
991 } elseif ($leftterm > $rightterm) {
992 return 1; // bigger
993 } else {
994 if ($left < $right) {
995 return -1; // smaller
996 } elseif ($left > $right) {
997 return 1; // bigger
998 }
999 }
1000 }
1001 }
1002 // fall back for no second level matches or second level matches are equal
527af457 1003 return 0; // equal to
1004 }
1005}
6381fa56 1006
1007/**
1008* Generate the user attempt status string
1009*
1010* @param object $user Current context user
1011* @param object $scorm a moodle scrom object - mdl_scorm
1012* @return string - Attempt status string
7554f671 1013*/
6381fa56 1014function scorm_get_attempt_status($user, $scorm) {
1015 global $DB;
7554f671 1016
b1ca5d31 1017 $attempts = scorm_get_attempt_count($user->id, $scorm, true);
6381fa56 1018 if(empty($attempts)) {
1019 $attemptcount = 0;
1020 } else {
1021 $attemptcount = count($attempts);
1022 }
7554f671 1023
6381fa56 1024 $result = '<p>'.get_string('noattemptsallowed', 'scorm').': ';
1025 if ($scorm->maxattempt > 0) {
4f4a8acf 1026 $result .= $scorm->maxattempt . '<br />';
6381fa56 1027 } else {
4f4a8acf 1028 $result .= get_string('unlimited').'<br />';
6381fa56 1029 }
4f4a8acf 1030 $result .= get_string('noattemptsmade', 'scorm').': ' . $attemptcount . '<br />';
6381fa56 1031
b1ca5d31
DM
1032 if ($scorm->maxattempt == 1) {
1033 switch ($scorm->grademethod) {
1034 case GRADEHIGHEST:
1035 $grademethod = get_string('gradehighest', 'scorm');
1036 break;
1037 case GRADEAVERAGE:
1038 $grademethod = get_string('gradeaverage', 'scorm');
1039 break;
1040 case GRADESUM:
1041 $grademethod = get_string('gradesum', 'scorm');
1042 break;
1043 case GRADESCOES:
1044 $grademethod = get_string('gradescoes', 'scorm');
1045 break;
1046 }
1047 } else {
1048 switch ($scorm->whatgrade) {
1049 case HIGHESTATTEMPT:
1050 $grademethod = get_string('highestattempt', 'scorm');
1051 break;
1052 case AVERAGEATTEMPT:
1053 $grademethod = get_string('averageattempt', 'scorm');
1054 break;
1055 case FIRSTATTEMPT:
1056 $grademethod = get_string('firstattempt', 'scorm');
1057 break;
1058 case LASTATTEMPT:
1059 $grademethod = get_string('lastattempt', 'scorm');
1060 break;
1061 }
1062 }
7554f671 1063
6381fa56 1064 if(!empty($attempts)) {
b1ca5d31 1065 $i = 1;
6381fa56 1066 foreach($attempts as $attempt) {
1067 $gradereported = scorm_grade_user_attempt($scorm, $user->id, $attempt->attemptnumber);
4f4a8acf
DM
1068 if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1069 $gradereported = $gradereported/$scorm->maxgrade;
1070 $gradereported = number_format($gradereported*100, 0) .'%';
1071 }
1072 $result .= get_string('gradeforattempt', 'scorm').' ' . $i . ': ' . $gradereported .'<br />';
b1ca5d31 1073 $i++;
6381fa56 1074 }
1075 }
b1ca5d31 1076 $calculatedgrade = scorm_grade_user($scorm, $user->id);
4f4a8acf
DM
1077 if ($scorm->grademethod !== GRADESCOES && !empty($scorm->maxgrade)) {
1078 $calculatedgrade = $calculatedgrade/$scorm->maxgrade;
1079 $calculatedgrade = number_format($calculatedgrade*100, 0) .'%';
1080 }
6381fa56 1081 $result .= get_string('grademethod', 'scorm'). ': ' . $grademethod;
1082 if(empty($attempts)) {
4f4a8acf 1083 $result .= '<br />' . get_string('gradereported','scorm') . ': ' . get_string('none') . '<br />';
6381fa56 1084 } else {
4f4a8acf 1085 $result .= '<br />' . get_string('gradereported','scorm') . ': ' . $calculatedgrade . '<br />';
6381fa56 1086 }
1087 $result .= '</p>';
1088 if ($attemptcount >= $scorm->maxattempt and $scorm->maxattempt > 0) {
1089 $result .= '<p><font color="#cc0000">'.get_string('exceededmaxattempts','scorm').'</font></p>';
1090 }
1091 return $result;
1092}
1093
1094/**
1095* Get SCORM attempt count
1096*
1097* @param object $user Current context user
1098* @param object $scorm a moodle scrom object - mdl_scorm
e24445c7 1099* @param bool $attempts return the list of attempts
6381fa56 1100* @return int - no. of attempts so far
7554f671 1101*/
b1ca5d31 1102function scorm_get_attempt_count($userid, $scorm, $attempts_only=false) {
6381fa56 1103 global $DB;
1104 $attemptcount = 0;
1105 $element = 'cmi.core.score.raw';
78b203ca
DM
1106 if ($scorm->grademethod == GRADESCOES) {
1107 $element = 'cmi.core.lesson_status';
1108 }
6381fa56 1109 if ($scorm->version == 'scorm1_3') {
1110 $element = 'cmi.score.raw';
1111 }
b1ca5d31 1112 $attempts = $DB->get_records_select('scorm_scoes_track',"element=? AND userid=? AND scormid=?", array($element, $userid, $scorm->id),'attempt','DISTINCT attempt AS attemptnumber');
e24445c7
PH
1113 if ($attempts_only) {
1114 return $attempts;
1115 }
6381fa56 1116 if(!empty($attempts)) {
1117 $attemptcount = count($attempts);
1118 }
1119 return $attemptcount;
1120}
1881df27 1121
1122/**
1123* Figure out with this is a debug situation
1124*
1125* @param object $scorm a moodle scrom object - mdl_scorm
1126* @return boolean - debugging true/false
7554f671 1127*/
1881df27 1128function scorm_debugging($scorm) {
1129 global $CFG, $USER;
30fc6e2d 1130 $cfg_scorm = get_config('scorm');
7554f671 1131
30fc6e2d 1132 if (!$cfg_scorm->allowapidebug) {
1881df27 1133 return false;
1134 }
1135 $identifier = $USER->username.':'.$scorm->name;
30fc6e2d 1136 $test = $cfg_scorm->apidebugmask;
1881df27 1137 // check the regex is only a short list of safe characters
1138 if (!preg_match('/^[\w\s\*\.\?\+\:\_\\\]+$/', $test)) {
1139 return false;
1140 }
1141 $res = false;
1142 eval('$res = preg_match(\'/^'.$test.'/\', $identifier) ? true : false;');
1143 return $res;
1144}
7554f671 1145
1146/**
1147* Delete Scorm tracks for selected users
1148*
1149* @param array $attemptids list of attempts that need to be deleted
8716cc35 1150* @param int $scorm instance
7554f671 1151*
1152* return bool true deleted all responses, false failed deleting an attempt - stopped here
1153*/
8716cc35 1154function scorm_delete_responses($attemptids, $scorm) {
7554f671 1155 if(!is_array($attemptids) || empty($attemptids)) {
1156 return false;
1157 }
1158
1159 foreach($attemptids as $num => $attemptid) {
1160 if(empty($attemptid)) {
1161 unset($attemptids[$num]);
1162 }
1163 }
1164
1165 foreach($attemptids as $attempt) {
1166 $keys = explode(':', $attempt);
1167 if (count($keys) == 2) {
1168 $userid = clean_param($keys[0], PARAM_INT);
1169 $attemptid = clean_param($keys[1], PARAM_INT);
8716cc35 1170 if (!$userid || !$attemptid || !scorm_delete_attempt($userid, $scorm, $attemptid)) {
7554f671 1171 return false;
1172 }
1173 } else {
1174 return false;
1175 }
1176 }
1177 return true;
1178}
1179
1180/**
1181* Delete Scorm tracks for selected users
1182*
1183* @param int $userid ID of User
1184* @param int $scormid ID of Scorm
1185* @param int $attemptid user attempt that need to be deleted
1186*
1187* return bool true suceeded
1188*/
8716cc35 1189function scorm_delete_attempt($userid, $scorm, $attemptid) {
7554f671 1190 global $DB;
1191
8716cc35
DM
1192 $DB->delete_records('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attemptid));
1193 include_once('lib.php');
1194 scorm_update_grades($scorm, $userid, true);
7554f671 1195 return true;
1196}
00c1677a
DM
1197
1198/**
2700b283 1199 * Converts SCORM duration notation to human-readable format
00c1677a 1200 * The function works with both SCORM 1.2 and SCORM 2004 time formats
2700b283 1201 * @param $duration string SCORM duration
00c1677a
DM
1202 * @return string human-readable date/time
1203 */
2700b283 1204function scorm_format_duration($duration) {
00c1677a 1205 // fetch date/time strings
119d64ee 1206 $stryears = get_string('years');
00c1677a 1207 $strmonths = get_string('nummonths');
119d64ee
DM
1208 $strdays = get_string('days');
1209 $strhours = get_string('hours');
1210 $strminutes = get_string('minutes');
1211 $strseconds = get_string('seconds');
64f93798 1212
2700b283 1213 if ($duration[0] == 'P') {
00c1677a
DM
1214 // if timestamp starts with 'P' - it's a SCORM 2004 format
1215 // this regexp discards empty sections, takes Month/Minute ambiguity into consideration,
1216 // and outputs filled sections, discarding leading zeroes and any format literals
1217 // also saves the only zero before seconds decimals (if there are any) and discards decimals if they are zero
1218 $pattern = array( '#([A-Z])0+Y#', '#([A-Z])0+M#', '#([A-Z])0+D#', '#P(|\d+Y)0*(\d+)M#', '#0*(\d+)Y#', '#0*(\d+)D#', '#P#',
1219 '#([A-Z])0+H#', '#([A-Z])[0.]+S#', '#\.0+S#', '#T(|\d+H)0*(\d+)M#', '#0*(\d+)H#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
119d64ee
DM
1220 $replace = array( '$1', '$1', '$1', '$1$2 '.$strmonths.' ', '$1 '.$stryears.' ', '$1 '.$strdays.' ', '',
1221 '$1', '$1', 'S', '$1$2 '.$strminutes.' ', '$1 '.$strhours.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '');
00c1677a
DM
1222 } else {
1223 // else we have SCORM 1.2 format there
1224 // first convert the timestamp to some SCORM 2004-like format for conveniency
2700b283 1225 $duration = preg_replace('#^(\d+):(\d+):([\d.]+)$#', 'T$1H$2M$3S', $duration);
00c1677a
DM
1226 // then convert in the same way as SCORM 2004
1227 $pattern = array( '#T0+H#', '#([A-Z])0+M#', '#([A-Z])[0.]+S#', '#\.0+S#', '#0*(\d+)H#', '#0*(\d+)M#', '#0+\.(\d+)S#', '#0*([\d.]+)S#', '#T#' );
119d64ee 1228 $replace = array( 'T', '$1', '$1', 'S', '$1 '.$strhours.' ', '$1 '.$strminutes.' ', '0.$1 '.$strseconds, '$1 '.$strseconds, '' );
00c1677a 1229 }
64f93798 1230
2700b283 1231 $result = preg_replace($pattern, $replace, $duration);
00c1677a
DM
1232
1233 return $result;
1234}