on-demand release 3.7dev+
[moodle.git] / mod / scorm / datamodels / scorm_13lib.php
blob0048cd49fc5a30d9071500b197690f14b75b3913
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.
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/>.
17 defined('MOODLE_INTERNAL') || die();
19 require_once($CFG->dirroot.'/mod/scorm/datamodels/scormlib.php');
20 require_once($CFG->dirroot.'/mod/scorm/datamodels/sequencinglib.php');
22 function scorm_seq_overall ($scoid, $userid, $request, $attempt) {
23 $seq = scorm_seq_navigation($scoid, $userid, $request, $attempt);
24 if ($seq->navigation) {
25 if ($seq->termination != null) {
26 $seq = scorm_seq_termination($scoid, $userid, $seq);
28 if ($seq->sequencing != null) {
29 $seq = scorm_seq_sequencing($scoid, $userid, $seq);
30 if ($seq->sequencing == 'exit') { // Return the control to the LTS.
31 return 'true';
34 if ($seq->delivery != null) {
35 $seq = scorm_sequencing_delivery($scoid, $userid, $seq);
36 $seq = scorm_content_delivery_environment ($seq, $userid);
39 if ($seq->exception != null) {
40 $seq = scorm_sequencing_exception($seq);
42 return 'true';
45 function scorm_seq_navigation ($scoid, $userid, $request, $attempt=0) {
46 global $DB;
48 // Sequencing structure.
49 $seq = new stdClass();
50 $seq->currentactivity = scorm_get_sco($scoid);
51 $seq->traversaldir = null;
52 $seq->nextactivity = null;
53 $seq->deliveryvalid = null;
54 $seq->attempt = $attempt;
56 $seq->identifiedactivity = null;
57 $seq->delivery = null;
58 $seq->deliverable = false;
59 $seq->active = scorm_seq_is('active', $scoid, $userid);
60 $seq->suspended = scorm_seq_is('suspended', $scoid, $userid);
61 $seq->navigation = null;
62 $seq->termination = null;
63 $seq->sequencing = null;
64 $seq->target = null;
65 $seq->endsession = null;
66 $seq->exception = null;
67 $seq->reachable = true;
68 $seq->prevact = true;
70 $sco = scorm_get_sco($scoid);
72 switch ($request) {
73 case 'start_':
74 if (empty($seq->currentactivity)) {
75 $seq->navigation = true;
76 $seq->sequencing = 'start';
77 } else {
78 $seq->exception = 'NB.2.1-1'; // Sequencing session already begun.
80 break;
81 case 'resumeall_':
82 if (empty($seq->currentactivity)) {
83 // TODO: I think it's suspend instead of suspendedactivity.
84 if ($track = $DB->get_record('scorm_scoes_track',
85 array('scoid' => $scoid, 'userid' => $userid, 'element' => 'suspendedactivity'))) {
87 $seq->navigation = true;
88 $seq->sequencing = 'resumeall';
89 } else {
90 $seq->exception = 'NB.2.1-3'; // No suspended activity found.
92 } else {
93 $seq->exception = 'NB.2.1-1'; // Sequencing session already begun.
95 break;
96 case 'continue_':
97 case 'previous_':
98 if (!empty($seq->currentactivity)) {
99 $sco = $seq->currentactivity;
100 if ($sco->parent != '/') {
101 if ($parentsco = scorm_get_parent($sco)) {
103 if (isset($parentsco->flow) && ($parentsco->flow == true)) { // I think it's parentsco.
104 // Current activity is active!
105 if (scorm_seq_is('active', $sco->id, $userid)) {
106 if ($request == 'continue_') {
107 $seq->navigation = true;
108 $seq->termination = 'exit';
109 $seq->sequencing = 'continue';
110 } else {
111 if (!isset($parentsco->forwardonly) || ($parentsco->forwardonly == false)) {
112 $seq->navigation = true;
113 $seq->termination = 'exit';
114 $seq->sequencing = 'previous';
115 } else {
116 $seq->exception = 'NB.2.1-5'; // Violates control mode.
124 } else {
125 $seq->exception = 'NB.2.1-2'; // Current activity not defined.
127 break;
128 case 'forward_':
129 case 'backward_':
130 $seq->exception = 'NB.2.1-7'; // None to be done, behavior not defined.
131 break;
132 case 'exit_':
133 case 'abandon_':
134 if (!empty($seq->currentactivity)) {
135 // Current activity is active !
136 $seq->navigation = true;
137 $seq->termination = substr($request, 0, -1);
138 $seq->sequencing = 'exit';
139 } else {
140 $seq->exception = 'NB.2.1-2'; // Current activity not defined.
142 case 'exitall_':
143 case 'abandonall_':
144 case 'suspendall_':
145 if (!empty($seq->currentactivity)) {
146 $seq->navigation = true;
147 $seq->termination = substr($request, 0, -1);
148 $seq->sequencing = 'exit';
149 } else {
150 $seq->exception = 'NB.2.1-2'; // Current activity not defined.
152 break;
153 default: // Example {target=<STRING>}choice.
154 if ($targetsco = $DB->get_record('scorm_scoes', array('scorm' => $sco->scorm, 'identifier' => $request))) {
155 if ($targetsco->parent != '/') {
156 $seq->target = $request;
157 } else {
158 if ($parentsco = scorm_get_parent($targetsco)) {
159 if (!isset($parentsco->choice) || ($parentsco->choice == true)) {
160 $seq->target = $request;
164 if ($seq->target != null) {
165 if (empty($seq->currentactivity)) {
166 $seq->navigation = true;
167 $seq->sequencing = 'choice';
168 } else {
169 if (!$sco = scorm_get_sco($scoid)) {
170 return $seq;
172 if ($sco->parent != $targetsco->parent) {
173 $ancestors = scorm_get_ancestors($sco);
174 $commonpos = scorm_find_common_ancestor($ancestors, $targetsco);
175 if ($commonpos !== false) {
176 if ($activitypath = array_slice($ancestors, 0, $commonpos)) {
177 foreach ($activitypath as $activity) {
178 if (scorm_seq_is('active', $activity->id, $userid) &&
179 (isset($activity->choiceexit) && ($activity->choiceexit == false))) {
180 $seq->navigation = false;
181 $seq->termination = null;
182 $seq->sequencing = null;
183 $seq->target = null;
184 $seq->exception = 'NB.2.1-8'; // Violates control mode.
185 return $seq;
188 } else {
189 $seq->navigation = false;
190 $seq->termination = null;
191 $seq->sequencing = null;
192 $seq->target = null;
193 $seq->exception = 'NB.2.1-9';
197 // Current activity is active !
198 $seq->navigation = true;
199 $seq->sequencing = 'choice';
201 } else {
202 $seq->exception = 'NB.2.1-10'; // Violates control mode.
204 } else {
205 $seq->exception = 'NB.2.1-11'; // Target activity does not exists.
207 break;
209 return $seq;
212 function scorm_seq_termination ($seq, $userid) {
213 if (empty($seq->currentactivity)) {
214 $seq->termination = false;
215 $seq->exception = 'TB.2.3-1';
216 return $seq;
219 $sco = $seq->currentactivity;
221 if ((($seq->termination == 'exit') || ($seq->termination == 'abandon')) && !$seq->active) {
222 $seq->termination = false;
223 $seq->exception = 'TB.2.3-2';
224 return $seq;
226 switch ($seq->termination) {
227 case 'exit':
228 scorm_seq_end_attempt($sco, $userid, $seq);
229 $seq = scorm_seq_exit_action_rules($seq, $userid);
230 do {
231 $exit = false;// I think this is false. Originally this was true.
232 $seq = scorm_seq_post_cond_rules($seq, $userid);
233 if ($seq->termination == 'exitparent') {
234 if ($sco->parent != '/') {
235 $sco = scorm_get_parent($sco);
236 $seq->currentactivity = $sco;
237 $seq->active = scorm_seq_is('active', $sco->id, $userid);
238 scorm_seq_end_attempt($sco, $userid, $seq);
239 $exit = true; // I think it's true. Originally this was false.
240 } else {
241 $seq->termination = false;
242 $seq->exception = 'TB.2.3-4';
243 return $seq;
246 } while (($exit == false) && ($seq->termination == 'exit'));
247 if ($seq->termination == 'exit') {
248 $seq->termination = true;
249 return $seq;
251 case 'exitall':
252 if ($seq->active) {
253 scorm_seq_end_attempt($sco, $userid, $seq);
255 // Terminate Descendent Attempts Process.
257 if ($ancestors = scorm_get_ancestors($sco)) {
258 foreach ($ancestors as $ancestor) {
259 scorm_seq_end_attempt($ancestor, $userid, $seq);
260 $seq->currentactivity = $ancestor;
264 $seq->active = scorm_seq_is('active', $seq->currentactivity->id, $userid);
265 $seq->termination = true;
266 $seq->sequencing = 'exit';
267 break;
268 case 'suspendall':
269 if (($seq->active) || ($seq->suspended)) {
270 scorm_seq_set('suspended', $sco->id, $userid, $attempt);
271 } else {
272 if ($sco->parent != '/') {
273 $parentsco = scorm_get_parent($sco);
274 scorm_seq_set('suspended', $parentsco->id, $userid, $attempt);
275 } else {
276 $seq->termination = false;
277 $seq->exception = 'TB.2.3-3';
280 if ($ancestors = scorm_get_ancestors($sco)) {
281 foreach ($ancestors as $ancestor) {
282 scorm_seq_set('active', $ancestor->id, $userid, $attempt, false);
283 scorm_seq_set('suspended', $ancestor->id, $userid, $attempt);
284 $seq->currentactivity = $ancestor;
286 $seq->termination = true;
287 $seq->sequencing = 'exit';
288 } else {
289 $seq->termination = false;
290 $seq->exception = 'TB.2.3-5';
292 break;
293 case 'abandon':
294 scorm_seq_set('active', $sco->id, $userid, $attempt, false);
295 $seq->active = null;
296 $seq->termination = true;
297 break;
298 case 'abandonall':
299 if ($ancestors = scorm_get_ancestors($sco)) {
300 foreach ($ancestors as $ancestor) {
301 scorm_seq_set('active', $ancestor->id, $userid, $attempt, false);
302 $seq->currentactivity = $ancestor;
304 $seq->termination = true;
305 $seq->sequencing = 'exit';
306 } else {
307 $seq->termination = false;
308 $seq->exception = 'TB.2.3-6';
310 break;
311 default:
312 $seq->termination = false;
313 $seq->exception = 'TB.2.3-7';
314 break;
316 return $seq;
319 function scorm_seq_end_attempt($sco, $userid, $seq) {
320 global $DB;
321 if (scorm_is_leaf($sco)) {
322 if (!isset($sco->tracked) || ($sco->tracked == 1)) {
323 if (!scorm_seq_is('suspended', $sco->id, $userid)) {
324 if (!isset($sco->completionsetbycontent) || ($sco->completionsetbycontent == 0)) {
325 if (!scorm_seq_is('attemptprogressstatus', $sco->id, $userid, $seq->attempt)) {
326 $incomplete = $DB->get_field('scorm_scoes_track', 'value',
327 array('scoid' => $sco->id,
328 'userid' => $userid,
329 'element' => 'cmi.completion_status'));
330 if ($incomplete != 'incomplete') {
331 scorm_seq_set('attemptprogressstatus', $sco->id, $userid, $seq->attempt);
332 scorm_seq_set('attemptcompletionstatus', $sco->id, $userid, $seq->attempt);
336 if (!isset($sco->objectivesetbycontent) || ($sco->objectivesetbycontent == 0)) {
337 if ($objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $sco->id))) {
338 foreach ($objectives as $objective) {
339 if ($objective->primaryobj) {
340 if (!scorm_seq_is('objectiveprogressstatus', $sco->id, $userid, $seq->attempt)) {
341 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $seq->attempt);
342 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, $seq->attempt);
350 } else if ($children = scorm_get_children($sco)) {
351 $suspended = false;
352 foreach ($children as $child) {
353 if (scorm_seq_is('suspended', $child, $userid, $seq->attempt)) {
354 $suspended = true;
355 break;
358 if ($suspended) {
359 scorm_seq_set('suspended', $sco, $userid, $seq->attempt);
360 } else {
361 scorm_seq_set('suspended', $sco, $userid, $seq->attempt, false);
364 scorm_seq_set('active', $sco->id, $userid, $seq->attempt, false);
365 scorm_seq_overall_rollup($sco, $userid, $seq);
368 function scorm_seq_is($what, $scoid, $userid, $attempt=0) {
369 global $DB;
371 // Check if passed activity $what is active.
372 $active = false;
373 if ($track = $DB->get_record('scorm_scoes_track',
374 array('scoid' => $scoid, 'userid' => $userid, 'attempt' => $attempt, 'element' => $what))) {
375 $active = true;
377 return $active;
380 function scorm_seq_set($what, $scoid, $userid, $attempt=0, $value='true') {
381 global $DB;
383 $sco = scorm_get_sco($scoid);
385 // Set passed activity to active or not.
386 if ($value == false) {
387 $DB->delete_records('scorm_scoes_track', array('scoid' => $scoid, 'userid' => $userid,
388 'attempt' => $attempt, 'element' => $what));
389 } else {
390 scorm_insert_track($userid, $sco->scorm, $sco->id, $attempt, $what, $value);
393 // Update grades in gradebook.
394 $scorm = $DB->get_record('scorm', array('id' => $sco->scorm));
395 scorm_update_grades($scorm, $userid, true);
398 function scorm_evaluate_condition ($rollupruleconds, $sco, $userid) {
399 global $DB;
401 $res = false;
403 if (strpos($rollupruleconds, 'and ')) {
404 $rollupruleconds = array_filter(explode(' and ', $rollupruleconds));
405 $conditioncombination = 'all';
406 } else {
407 $rollupruleconds = array_filter(explode(' or ', $rollupruleconds));
408 $conditioncombination = 'or';
410 foreach ($rollupruleconds as $rolluprulecond) {
411 $notflag = false;
412 if (strpos($rolluprulecond, 'not') !== false) {
413 $rolluprulecond = str_replace('not', '', $rolluprulecond);
414 $notflag = true;
416 $conditionarray['condition'] = $rolluprulecond;
417 $conditionarray['notflag'] = $notflag;
418 $conditions[] = $conditionarray;
420 foreach ($conditions as $condition) {
421 $checknot = true;
422 $res = false;
423 if ($condition['notflag']) {
424 $checknot = false;
426 switch ($condition['condition']) {
427 case 'satisfied':
428 $r = $DB->get_record('scorm_scoes_track',
429 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivesatisfiedstatus'));
430 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
431 $r = $DB->get_record('scorm_scoes_track',
432 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus'));
433 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
434 $res = true;
437 break;
439 case 'objectiveStatusKnown':
440 $r = $DB->get_record('scorm_scoes_track',
441 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus'));
442 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
443 $res = true;
445 break;
447 case 'notobjectiveStatusKnown':
448 $r = $DB->get_record('scorm_scoes_track',
449 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectiveprogressstatus'));
450 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
451 $res = true;
453 break;
455 case 'objectiveMeasureKnown':
456 $r = $DB->get_record('scorm_scoes_track',
457 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivemeasurestatus'));
458 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
459 $res = true;
461 break;
463 case 'notobjectiveMeasureKnown':
464 $r = $DB->get_record('scorm_scoes_track',
465 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivemeasurestatus'));
466 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
467 $res = true;
469 break;
471 case 'completed':
472 $r = $DB->get_record('scorm_scoes_track',
473 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptcompletionstatus'));
474 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
475 $r = $DB->get_record('scorm_scoes_track',
476 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptprogressstatus'));
477 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
478 $res = true;
481 break;
483 case 'attempted':
484 $attempt = $DB->get_field('scorm_scoes_track', 'attempt',
485 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'x.start.time'));
486 if ($checknot && $attempt > 0) {
487 $res = true;
488 } else if (!$checknot && $attempt <= 0) {
489 $res = true;
491 break;
493 case 'attemptLimitExceeded':
494 $r = $DB->get_record('scorm_scoes_track',
495 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'activityprogressstatus'));
496 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
497 $r = $DB->get_record('scorm_scoes_track',
498 array('scoid' => $sco->id, 'userid' => $userid,
499 'element' => 'limitconditionattemptlimitcontrol'));
500 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
501 if ($r = $DB->get_field('scorm_scoes_track', 'attempt', array('scoid' => $sco->id, 'userid' => $userid)) &&
502 $r2 = $DB->get_record('scorm_scoes_track', array('scoid' => $sco->id, 'userid' => $userid,
503 'element' => 'limitconditionattemptlimit')) ) {
505 if ($checknot && ($r->value >= $r2->value)) {
506 $res = true;
507 } else if (!$checknot && ($r->value < $r2->value)) {
508 $res = true;
513 break;
515 case 'activityProgressKnown':
516 $r = $DB->get_record('scorm_scoes_track',
517 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'activityprogressstatus'));
518 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
519 $r = $DB->get_record('scorm_scoes_track',
520 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'attemptprogressstatus'));
521 if ((!isset($r->value) && !$checknot) || (isset($r->value) && ($r->value == $checknot))) {
522 $res = true;
525 break;
528 if ($conditioncombination == 'all' && !$res) {
529 break;
530 } else if ($conditioncombination == 'or' && $res) {
531 break;
535 return $res;
538 function scorm_check_activity ($activity, $userid) {
539 $act = scorm_seq_rules_check($activity, 'disabled');
540 if ($act != null) {
541 return true;
543 if (scorm_limit_cond_check ($activity, $userid)) {
544 return true;
546 return false;
549 function scorm_limit_cond_check ($activity, $userid) {
550 global $DB;
552 if (isset($activity->tracked) && ($activity->tracked == 0)) {
553 return false;
556 if (scorm_seq_is('active', $activity->id, $userid) || scorm_seq_is('suspended', $activity->id, $userid)) {
557 return false;
560 if (!isset($activity->limitcontrol) || ($activity->limitcontrol == 1)) {
561 $r = $DB->get_record('scorm_scoes_track',
562 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityattemptcount'));
563 if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattempt)) {
564 return true;
568 if (!isset($activity->limitabsdurcontrol) || ($activity->limitabsdurcontrol == 1)) {
569 $r = $DB->get_record('scorm_scoes_track',
570 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityabsoluteduration'));
571 if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitabsduration)) {
572 return true;
576 if (!isset($activity->limitexpdurcontrol) || ($activity->limitexpdurcontrol == 1)) {
577 $r = $DB->get_record('scorm_scoes_track',
578 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'activityexperiencedduration'));
579 if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitexpduration)) {
580 return true;
584 if (!isset($activity->limitattabsdurcontrol) || ($activity->limitattabsdurcontrol == 1)) {
585 $r = $DB->get_record('scorm_scoes_track',
586 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'attemptabsoluteduration'));
587 if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattabsduration)) {
588 return true;
592 if (!isset($activity->limitattexpdurcontrol) || ($activity->limitattexpdurcontrol == 1)) {
593 $r = $DB->get_record('scorm_scoes_track',
594 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'attemptexperiencedduration'));
595 if (scorm_seq_is('activityprogressstatus', $activity->id, $userid) && ($r->value >= $activity->limitattexpduration)) {
596 return true;
600 if (!isset($activity->limitbegincontrol) || ($activity->limitbegincontrol == 1)) {
601 $r = $DB->get_record('scorm_scoes_track',
602 array('scoid' => $activity->id, 'userid' => $userid, 'element' => 'begintime'));
603 if (isset($activity->limitbegintime) && time() >= $activity->limitbegintime) {
604 return true;
608 if (!isset($activity->limitbegincontrol) || ($activity->limitbegincontrol == 1)) {
609 if (isset($activity->limitbegintime) && time() < $activity->limitbegintime) {
610 return true;
614 if (!isset($activity->limitendcontrol) || ($activity->limitendcontrol == 1)) {
615 if (isset($activity->limitendtime) && time() > $activity->limitendtime) {
616 return true;
619 return false;
622 function scorm_seq_rules_check ($sco, $action) {
623 global $DB;
624 $act = null;
626 if ($rules = $DB->get_records('scorm_seq_ruleconds', array('scoid' => $sco->id, 'action' => $action))) {
627 foreach ($rules as $rule) {
628 if ($act = scorm_seq_rule_check($sco, $rule)) {
629 return $act;
633 return $act;
637 function scorm_seq_rule_check ($sco, $rule) {
638 global $DB;
640 $bag = Array();
641 $cond = '';
642 $ruleconds = $DB->get_records('scorm_seq_rulecond', array('scoid' => $sco->id, 'ruleconditionsid' => $rule->id));
643 foreach ($ruleconds as $rulecond) {
644 if ($rulecond->operator == 'not') {
645 if ($rulecond->cond != 'unknown' ) {
646 $rulecond->cond = 'not'.$rulecond->cond;
649 $bag[] = $rulecond->cond;
651 if (empty($bag)) {
652 $cond = 'unknown';
653 return $cond;
656 if ($rule->conditioncombination == 'all') {
657 foreach ($bag as $con) {
658 $cond = $cond.' and '.$con;
660 } else {
661 foreach ($bag as $con) {
662 $cond = $cond.' or '.$con;
665 return $cond;
668 function scorm_seq_overall_rollup($sco, $userid, $seq) {
669 if ($ancestors = scorm_get_ancestors($sco)) {
670 foreach ($ancestors as $ancestor) {
671 if (!scorm_is_leaf($ancestor)) {
672 scorm_seq_measure_rollup($sco, $userid, $seq->attempt);
674 scorm_seq_objective_rollup($sco, $userid, $seq->attempt);
675 scorm_seq_activity_progress_rollup($sco, $userid, $seq);
680 function scorm_seq_measure_rollup($sco, $userid, $attempt = 0) {
681 global $DB;
683 $totalmeasure = 0; // Check if there is something similar in the database.
684 $valid = false; // Same as in the last line.
685 $countedmeasures = 0; // Same too.
686 $targetobjective = null;
687 $objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $sco->id));
689 foreach ($objectives as $objective) {
690 if ($objective->primaryobj == true) { // Objective contributes to rollup.
691 $targetobjective = $objective;
692 break;
696 if ($targetobjective != null) {
697 $children = scorm_get_children($sco);
698 if (!empty ($children)) {
699 foreach ($children as $child) {
700 $child = scorm_get_sco ($child);
701 if (!isset($child->tracked) || ($child->tracked == 1)) {
702 $rolledupobjective = null;// We set the rolled up activity to undefined.
703 $objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $child->id));
704 foreach ($objectives as $objective) {
705 if ($objective->primaryobj == true) {// Objective contributes to rollup I'm using primaryobj field, but not.
706 $rolledupobjective = $objective;
707 break;
710 if ($rolledupobjective != null) {
711 $child = scorm_get_sco($child->id);
712 $countedmeasures = $countedmeasures + ($child->measureweight);
713 if (!scorm_seq_is('objectivemeasurestatus', $sco->id, $userid, $attempt)) {
714 $normalizedmeasure = $DB->get_record('scorm_scoes_track',
715 array('scoid' => $child->id, 'userid' => $userid, 'element' => 'objectivenormalizedmeasure'));
716 $totalmeasure = $totalmeasure + (($normalizedmeasure->value) * ($child->measureweight));
717 $valid = true;
724 if (!$valid) {
725 scorm_seq_set('objectivemeasurestatus', $sco->id, $userid, $attempt, false);
726 } else {
727 if ($countedmeasures > 0) {
728 scorm_seq_set('objectivemeasurestatus', $sco->id, $userid, $attempt);
729 $val = $totalmeasure / $countedmeasures;
730 scorm_seq_set('objectivenormalizedmeasure', $sco->id, $userid, $attempt, $val);
731 } else {
732 scorm_seq_set('objectivemeasurestatus', $sco->id, $userid, $attempt, false);
738 function scorm_seq_objective_rollup($sco, $userid, $attempt = 0) {
739 global $DB;
741 scorm_seq_objective_rollup_measure($sco, $userid, $attempt);
742 scorm_seq_objective_rollup_rules($sco, $userid, $attempt);
743 scorm_seq_objective_rollup_default($sco, $userid, $attempt);
746 if ($targetobjective->satisfiedbymeasure) {
747 scorm_seq_objective_rollup_measure($sco, $userid);
749 else{
750 if ((scorm_seq_rollup_rule_check($sco, $userid, 'incomplete'))
751 || (scorm_seq_rollup_rule_check($sco, $userid, 'completed'))) {
752 scorm_seq_objective_rollup_rules($sco, $userid);
754 else{
756 $rolluprules = $DB->get_record('scorm_seq_rolluprule', array('scoid'=>$sco->id, 'userid'=>$userid));
757 foreach ($rolluprules as $rolluprule) {
758 $rollupruleconds = $DB->get_records('scorm_seq_rolluprulecond', array('rollupruleid'=>$rolluprule->id));
759 foreach ($rollupruleconds as $rolluprulecond) {
761 switch ($rolluprulecond->cond!='satisfied'
762 && $rolluprulecond->cond!='completed' && $rolluprulecond->cond!='attempted') {
764 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, false);
766 break;
776 function scorm_seq_objective_rollup_measure($sco, $userid, $attempt = 0) {
777 global $DB;
779 $targetobjective = null;
781 $objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $sco->id));
782 foreach ($objectives as $objective) {
783 if ($objective->primaryobj == true) {
784 $targetobjective = $objective;
785 break;
788 if ($targetobjective != null) {
789 if ($targetobjective->satisfiedbymeasure) {
790 if (!scorm_seq_is('objectiveprogressstatus', $sco->id, $userid, $attempt)) {
791 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt, false);
792 } else {
793 if (scorm_seq_is('active', $sco->id, $userid, $attempt)) {
794 $isactive = true;
795 } else {
796 $isactive = false;
799 $normalizedmeasure = $DB->get_record('scorm_scoes_track',
800 array('scoid' => $sco->id, 'userid' => $userid, 'element' => 'objectivenormalizedmeasure'));
802 $sco = scorm_get_sco ($sco->id);
804 if (!$isactive || ($isactive &&
805 (!isset($sco->measuresatisfactionifactive) || $sco->measuresatisfactionifactive == true))) {
806 if (isset($normalizedmeasure->value) && ($normalizedmeasure->value >= $targetobjective->minnormalizedmeasure)) {
807 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt);
808 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, $attempt);
809 } else {
810 // TODO: handle the case where cmi.success_status is passed and objectivenormalizedmeasure undefined.
811 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt);
813 } else {
814 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt, false);
821 function scorm_seq_objective_rollup_default($sco, $userid, $attempt = 0) {
822 global $DB;
824 if (!(scorm_seq_rollup_rule_check($sco, $userid, 'incomplete')) && !(scorm_seq_rollup_rule_check($sco, $userid, 'completed'))) {
825 if ($rolluprules = $DB->get_record('scorm_seq_rolluprule', array('scoid' => $sco->id))) {
826 foreach ($rolluprules as $rolluprule) {
827 $rollupruleconds = $DB->get_records('scorm_seq_rolluprulecond', array('rollupruleid' => $rolluprule->id));
828 foreach ($rollupruleconds as $rolluprulecond) {
829 if ($rolluprulecond->cond != 'satisfied' && $rolluprulecond->cond != 'completed' &&
830 $rolluprulecond->cond != 'attempted') {
831 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, $attempt, false);
832 break;
841 function scorm_seq_objective_rollup_rules($sco, $userid, $attempt = 0) {
842 global $DB;
844 $targetobjective = null;
846 $objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $sco->id));
847 foreach ($objectives as $objective) {
848 if ($objective->primaryobj == true) {// Objective contributes to rollup I'm using primaryobj field, but not.
849 $targetobjective = $objective;
850 break;
853 if ($targetobjective != null) {
855 if (scorm_seq_rollup_rule_check($sco, $userid, 'notsatisfied')) {// With not satisfied rollup for the activity.
856 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt);
857 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, $attempt, false);
859 if (scorm_seq_rollup_rule_check($sco, $userid, 'satisfied')) {// With satisfied rollup for the activity.
860 scorm_seq_set('objectiveprogressstatus', $sco->id, $userid, $attempt);
861 scorm_seq_set('objectivesatisfiedstatus', $sco->id, $userid, $attempt);
868 function scorm_seq_activity_progress_rollup ($sco, $userid, $seq) {
870 if (scorm_seq_rollup_rule_check($sco, $userid, 'incomplete')) {
871 // Incomplete rollup action.
872 scorm_seq_set('attemptcompletionstatus', $sco->id, $userid, $seq->attempt, false);
873 scorm_seq_set('attemptprogressstatus', $sco->id, $userid, $seq->attempt, true);
876 if (scorm_seq_rollup_rule_check($sco, $userid, 'completed')) {
877 // Incomplete rollup action.
878 scorm_seq_set('attemptcompletionstatus', $sco->id, $userid, $seq->attempt, true);
879 scorm_seq_set('attemptprogressstatus', $sco->id, $userid, $seq->attempt, true);
884 function scorm_seq_rollup_rule_check ($sco, $userid, $action) {
885 global $DB;
887 if ($rolluprules = $DB->get_record('scorm_seq_rolluprule', array('scoid' => $sco->id, 'action' => $action))) {
888 $childrenbag = Array ();
889 $children = scorm_get_children ($sco);
891 foreach ($rolluprules as $rolluprule) {
892 foreach ($children as $child) {
894 /*$tracked = $DB->get_records('scorm_scoes_track', array('scoid'=>$child->id, 'userid'=>$userid));
895 if ($tracked && $tracked->attemp != 0) {*/
896 $child = scorm_get_sco ($child);
897 if (!isset($child->tracked) || ($child->tracked == 1)) {
898 if (scorm_seq_check_child ($child, $action, $userid)) {
899 $rollupruleconds = $DB->get_records('scorm_seq_rolluprulecond', array('rollupruleid' => $rolluprule->id));
900 $evaluate = scorm_seq_evaluate_rollupcond($child, $rolluprule->conditioncombination,
901 $rollupruleconds, $userid);
902 if ($evaluate == 'unknown') {
903 array_push($childrenbag, 'unknown');
904 } else {
905 if ($evaluate == true) {
906 array_push($childrenbag, true);
907 } else {
908 array_push($childrenbag, false);
915 $change = false;
917 switch ($rolluprule->childactivityset) {
919 case 'all':
920 // I think I can use this condition instead equivalent to OR.
921 if ((array_search(false, $childrenbag) === false) && (array_search('unknown', $childrenbag) === false)) {
922 $change = true;
924 break;
926 case 'any':
927 // I think I can use this condition instead equivalent to OR.
928 if (array_search(true, $childrenbag) !== false) {
929 $change = true;
931 break;
933 case 'none':
934 // I think I can use this condition instead equivalent to OR.
935 if ((array_search(true, $childrenbag) === false) && (array_search('unknown', $childrenbag) === false)) {
936 $change = true;
938 break;
940 case 'atleastcount':
941 // I think I can use this condition instead equivalent to OR.
942 foreach ($childrenbag as $itm) {
943 $cont = 0;
944 if ($itm === true) {
945 $cont++;
947 if ($cont >= $rolluprule->minimumcount) {
948 $change = true;
951 break;
953 case 'atleastcount':
954 foreach ($childrenbag as $itm) {// I think I can use this condition instead equivalent to OR.
955 $cont = 0;
956 if ($itm === true) {
957 $cont++;
959 if ($cont >= $rolluprule->minimumcount) {
960 $change = true;
963 break;
965 case 'atleastpercent':
966 foreach ($childrenbag as $itm) {// I think I can use this condition instead equivalent to OR.
967 $cont = 0;
968 if ($itm === true) {
969 $cont++;
971 if (($cont / count($childrenbag)) >= $rolluprule->minimumcount) {
972 $change = true;
975 break;
977 if ($change == true) {
978 return true;
982 return false;
985 function scorm_seq_flow_tree_traversal($activity, $direction, $childrenflag, $prevdirection, $seq, $userid, $skip = false) {
986 $revdirection = false;
987 $parent = scorm_get_parent($activity);
988 if (!empty($parent)) {
989 $children = scorm_get_available_children($parent);
990 } else {
991 $children = array();
993 $childrensize = count($children);
995 if (($prevdirection != null && $prevdirection == 'backward') && ($children[$childrensize - 1]->id == $activity->id)) {
996 $direction = 'backward';
997 $activity = $children[0];
998 $revdirection = true;
1001 if ($direction == 'forward') {
1002 $ancestors = scorm_get_ancestors($activity);
1003 $ancestorsroot = array_reverse($ancestors);
1004 $preorder = array();
1005 $preorder = scorm_get_preorder($preorder, $ancestorsroot[0]);
1006 $preordersize = count($preorder);
1007 if (($activity->id == $preorder[$preordersize - 1]->id) || (($activity->parent == '/') && !($childrenflag))) {
1008 $seq->endsession = true;
1009 $seq->nextactivity = null;
1010 return $seq;
1012 if (scorm_is_leaf ($activity) || !$childrenflag) {
1013 if ($children[$childrensize - 1]->id == $activity->id) {
1014 $seq = scorm_seq_flow_tree_traversal ($parent, $direction, false, null, $seq, $userid);
1015 if ($seq->nextactivity->launch == null) {
1016 $seq = scorm_seq_flow_tree_traversal ($seq->nextactivity, $direction, true, null, $seq, $userid);
1018 return $seq;
1019 } else {
1020 $position = 0;
1021 foreach ($children as $sco) {
1022 if ($sco->id == $activity->id) {
1023 break;
1025 $position++;
1027 if ($position != ($childrensize - 1)) {
1028 $seq->nextactivity = $children[$position + 1];
1029 $seq->traversaldir = $direction;
1030 return $seq;
1031 } else {
1032 $siblings = scorm_get_siblings($activity);
1033 $children = scorm_get_children($siblings[0]);
1034 $seq->nextactivity = $children[0];
1035 return $seq;
1038 } else {
1039 $children = scorm_get_available_children($activity);
1040 if (!empty($children)) {
1041 $seq->traversaldir = $direction;
1042 $seq->nextactivity = $children[0];
1043 return $seq;
1044 } else {
1045 $seq->traversaldir = null;
1046 $seq->nextactivity = null;
1047 $seq->exception = 'SB.2.1-2';
1048 return $seq;
1051 } else if ($direction == 'backward') {
1052 if ($activity->parent == '/') {
1053 $seq->traversaldir = null;
1054 $seq->nextactivity = null;
1055 $seq->exception = 'SB.2.1-3';
1056 return $seq;
1058 if (scorm_is_leaf ($activity) || !$childrenflag) {
1059 if (!$revdirection) {
1060 if (isset($parent->forwardonly) && ($parent->forwardonly == true && !$skip)) {
1061 $seq->traversaldir = null;
1062 $seq->nextactivity = null;
1063 $seq->exception = 'SB.2.1-4';
1064 return $seq;
1067 if ($children[0]->id == $activity->id) {
1068 $seq = scorm_seq_flow_tree_traversal($parent, 'backward', false, null, $seq, $userid);
1069 return $seq;
1070 } else {
1071 $ancestors = scorm_get_ancestors($activity);
1072 $ancestorsroot = array_reverse($ancestors);
1073 $preorder = array();
1074 $preorder = scorm_get_preorder($preorder, $ancestorsroot[0]);
1075 $position = 0;
1076 foreach ($preorder as $sco) {
1077 if ($sco->id == $activity->id) {
1078 break;
1080 $position++;
1082 if (isset($preorder[$position])) {
1083 $seq->nextactivity = $preorder[$position - 1];
1084 $seq->traversaldir = $direction;
1086 return $seq;
1088 } else {
1089 $children = scorm_get_available_children($activity);
1090 if (!empty($children)) {
1091 if (isset($parent->flow) && ($parent->flow == true)) {
1092 $seq->traversaldir = 'forward';
1093 $seq->nextactivity = $children[0];
1094 return $seq;
1095 } else {
1096 $seq->traversaldir = 'backward';
1097 $seq->nextactivity = $children[count($children) - 1];
1098 return $seq;
1100 } else {
1101 $seq->traversaldir = null;
1102 $seq->nextactivity = null;
1103 $seq->exception = 'SB.2.1-2';
1104 return $seq;
1110 // Returns the next activity on the tree, traversal direction, control returned to the LTS, (may) exception.
1111 function scorm_seq_flow_activity_traversal ($activity, $userid, $direction, $childrenflag, $prevdirection, $seq) {
1112 $parent = scorm_get_parent ($activity);
1113 if (!isset($parent->flow) || ($parent->flow == false)) {
1114 $seq->deliverable = false;
1115 $seq->exception = 'SB.2.2-1';
1116 $seq->nextactivity = $activity;
1117 return $seq;
1120 $rulecheck = scorm_seq_rules_check($activity, 'skip');
1121 if ($rulecheck != null) {
1122 $skip = scorm_evaluate_condition ($rulecheck, $activity, $userid);
1123 if ($skip) {
1124 $seq = scorm_seq_flow_tree_traversal($activity, $direction, false, $prevdirection, $seq, $userid, $skip);
1125 $seq = scorm_seq_flow_activity_traversal($seq->nextactivity, $userid, $direction,
1126 $childrenflag, $prevdirection, $seq);
1127 } else if (!empty($seq->identifiedactivity)) {
1128 $seq->nextactivity = $activity;
1130 return $seq;
1133 $ch = scorm_check_activity ($activity, $userid);
1134 if ($ch) {
1135 $seq->deliverable = false;
1136 $seq->exception = 'SB.2.2-2';
1137 $seq->nextactivity = $activity;
1138 return $seq;
1141 if (!scorm_is_leaf($activity)) {
1142 $seq = scorm_seq_flow_tree_traversal ($activity, $direction, true, null, $seq, $userid);
1143 if ($seq->identifiedactivity == null) {
1144 $seq->deliverable = false;
1145 $seq->nextactivity = $activity;
1146 return $seq;
1147 } else {
1148 if ($direction == 'backward' && $seq->traversaldir == 'forward') {
1149 $seq = scorm_seq_flow_activity_traversal($seq->identifiedactivity, $userid,
1150 'forward', $childrenflag, 'backward', $seq);
1151 } else {
1152 $seq = scorm_seq_flow_activity_traversal($seq->identifiedactivity, $userid,
1153 $direction, $childrenflag, null, $seq);
1155 return $seq;
1160 $seq->deliverable = true;
1161 $seq->nextactivity = $activity;
1162 $seq->exception = null;
1163 return $seq;
1167 function scorm_seq_flow ($activity, $direction, $seq, $childrenflag, $userid) {
1168 // TODO: $PREVDIRECTION NOT DEFINED YET.
1169 $prevdirection = null;
1170 $seq = scorm_seq_flow_tree_traversal ($activity, $direction, $childrenflag, $prevdirection, $seq, $userid);
1171 if ($seq->nextactivity == null) {
1172 $seq->nextactivity = $activity;
1173 $seq->deliverable = false;
1174 return $seq;
1175 } else {
1176 $activity = $seq->nextactivity;
1177 $seq = scorm_seq_flow_activity_traversal($activity, $userid, $direction, $childrenflag, null, $seq);
1178 return $seq;
1183 * Sets up $userdata array and default values for SCORM 1.3 .
1185 * @param stdClass $userdata an empty stdClass variable that should be set up with user values
1186 * @param object $scorm package record
1187 * @param string $scoid SCO Id
1188 * @param string $attempt attempt number for the user
1189 * @param string $mode scorm display mode type
1190 * @return array The default values that should be used for SCORM 1.3 package
1192 function get_scorm_default (&$userdata, $scorm, $scoid, $attempt, $mode) {
1193 global $DB, $USER;
1195 $userdata->student_id = $USER->username;
1196 if (empty(get_config('scorm', 'scormstandard'))) {
1197 $userdata->student_name = fullname($USER);
1198 } else {
1199 $userdata->student_name = $USER->lastname .', '. $USER->firstname;
1202 if ($usertrack = scorm_get_tracks($scoid, $USER->id, $attempt)) {
1203 // According to SCORM 2004(RTE V1, 4.2.8), only cmi.exit==suspend should allow previous datamodel elements on re-launch.
1204 if (isset($usertrack->{'cmi.exit'}) && ($usertrack->{'cmi.exit'} == 'suspend')) {
1205 foreach ($usertrack as $key => $value) {
1206 $userdata->$key = $value;
1208 } else {
1209 $userdata->status = '';
1210 $userdata->score_raw = '';
1212 } else {
1213 $userdata->status = '';
1214 $userdata->score_raw = '';
1217 if ($scodatas = scorm_get_sco($scoid, SCO_DATA)) {
1218 foreach ($scodatas as $key => $value) {
1219 $userdata->$key = $value;
1221 } else {
1222 print_error('cannotfindsco', 'scorm');
1224 if (!$sco = scorm_get_sco($scoid)) {
1225 print_error('cannotfindsco', 'scorm');
1228 if (isset($userdata->status)) {
1229 if (!isset($userdata->{'cmi.exit'}) || $userdata->{'cmi.exit'} == 'time-out' || $userdata->{'cmi.exit'} == 'normal') {
1230 $userdata->entry = 'ab-initio';
1231 } else {
1232 if (isset($userdata->{'cmi.exit'}) && ($userdata->{'cmi.exit'} == 'suspend' || $userdata->{'cmi.exit'} == 'logout')) {
1233 $userdata->entry = 'resume';
1234 } else {
1235 $userdata->entry = '';
1240 $userdata->mode = 'normal';
1241 if (!empty($mode)) {
1242 $userdata->mode = $mode;
1244 if ($userdata->mode == 'normal') {
1245 $userdata->credit = 'credit';
1246 } else {
1247 $userdata->credit = 'no-credit';
1250 $objectives = $DB->get_records('scorm_seq_objective', array('scoid' => $scoid));
1251 $index = 0;
1252 foreach ($objectives as $objective) {
1253 if (!empty($objective->minnormalizedmeasure)) {
1254 $userdata->{'cmi.scaled_passing_score'} = $objective->minnormalizedmeasure;
1256 if (!empty($objective->objectiveid)) {
1257 $userdata->{'cmi.objectives.N'.$index.'.id'} = $objective->objectiveid;
1258 $index++;
1262 $def = array();
1263 $def['cmi.learner_id'] = $userdata->student_id;
1264 $def['cmi.learner_name'] = $userdata->student_name;
1265 $def['cmi.mode'] = $userdata->mode;
1266 $def['cmi.entry'] = $userdata->entry;
1267 $def['cmi.exit'] = scorm_isset($userdata, 'cmi.exit');
1268 $def['cmi.credit'] = scorm_isset($userdata, 'credit');
1269 $def['cmi.completion_status'] = scorm_isset($userdata, 'cmi.completion_status', 'unknown');
1270 $def['cmi.completion_threshold'] = scorm_isset($userdata, 'threshold');
1271 $def['cmi.learner_preference.audio_level'] = scorm_isset($userdata, 'cmi.learner_preference.audio_level', 1);
1272 $def['cmi.learner_preference.language'] = scorm_isset($userdata, 'cmi.learner_preference.language');
1273 $def['cmi.learner_preference.delivery_speed'] = scorm_isset($userdata, 'cmi.learner_preference.delivery_speed');
1274 $def['cmi.learner_preference.audio_captioning'] = scorm_isset($userdata, 'cmi.learner_preference.audio_captioning', 0);
1275 $def['cmi.location'] = scorm_isset($userdata, 'cmi.location');
1276 $def['cmi.max_time_allowed'] = scorm_isset($userdata, 'attemptAbsoluteDurationLimit');
1277 $def['cmi.progress_measure'] = scorm_isset($userdata, 'cmi.progress_measure');
1278 $def['cmi.scaled_passing_score'] = scorm_isset($userdata, 'cmi.scaled_passing_score');
1279 $def['cmi.score.scaled'] = scorm_isset($userdata, 'cmi.score.scaled');
1280 $def['cmi.score.raw'] = scorm_isset($userdata, 'cmi.score.raw');
1281 $def['cmi.score.min'] = scorm_isset($userdata, 'cmi.score.min');
1282 $def['cmi.score.max'] = scorm_isset($userdata, 'cmi.score.max');
1283 $def['cmi.success_status'] = scorm_isset($userdata, 'cmi.success_status', 'unknown');
1284 $def['cmi.suspend_data'] = scorm_isset($userdata, 'cmi.suspend_data');
1285 $def['cmi.time_limit_action'] = scorm_isset($userdata, 'timelimitaction');
1286 $def['cmi.total_time'] = scorm_isset($userdata, 'cmi.total_time', 'PT0H0M0S');
1287 $def['cmi.launch_data'] = scorm_isset($userdata, 'datafromlms');
1289 return $def;