Merge branch 'wip-MDL-46503-master' of git://github.com/marinaglancy/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 28 Jul 2014 11:49:13 +0000 (12:49 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 28 Jul 2014 11:49:13 +0000 (12:49 +0100)
48 files changed:
blocks/login/block_login.php
blog/lib.php
enrol/cohort/yui/quickenrolment/quickenrolment.js
lib/classes/grades_external.php
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/csvlib.class.php
lib/filestorage/file_storage.php
lib/outputrenderers.php
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js
lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js
lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js
lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js
lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js
lib/yui/src/notification/js/ajaxexception.js
lib/yui/src/notification/js/alert.js
lib/yui/src/notification/js/confirm.js
lib/yui/src/notification/js/exception.js
mod/assign/gradingtable.php
mod/assign/locallib.php
mod/assign/tests/locallib_test.php
mod/forum/discuss.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/renderer.php
mod/forum/styles.css
mod/forum/tests/behat/discussion_navigation.feature [new file with mode: 0644]
mod/forum/tests/lib_test.php
mod/scorm/datamodels/aicc.js
mod/scorm/datamodels/aicc.php
mod/scorm/datamodels/debug.js.php
mod/scorm/datamodels/scorm_12.js
mod/scorm/datamodels/scorm_12.php
mod/scorm/datamodels/scorm_13.js
mod/scorm/datamodels/scorm_13.php
mod/scorm/locallib.php
mod/scorm/module.js
rss/file.php
rss/index.html [deleted file]
rss/renderer.php
user/filters/lib.php

index be63064..2c4eda4 100644 (file)
@@ -70,10 +70,15 @@ class block_login extends block_base {
         $this->content->text = '';
 
         if (!isloggedin() or isguestuser()) {   // Show the block
+            if (empty($CFG->authloginviaemail)) {
+                $strusername = get_string('username');
+            } else {
+                $strusername = get_string('usernameemail');
+            }
 
             $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'" '.$autocomplete.'>';
 
-            $this->content->text .= '<div class="c1 fld username"><label for="login_username">'.get_string('username').'</label>';
+            $this->content->text .= '<div class="c1 fld username"><label for="login_username">'.$strusername.'</label>';
             $this->content->text .= '<input type="text" name="username" id="login_username" value="'.s($username).'" /></div>';
 
             $this->content->text .= '<div class="c1 fld password"><label for="login_password">'.get_string('password').'</label>';
index 5b26916..b9cbae5 100644 (file)
@@ -180,9 +180,11 @@ function blog_sync_external_entries($externalblog) {
             $filtertags = array_map('trim', $filtertags);
             $filtertags = array_map('strtolower', $filtertags);
 
-            foreach ($categories as $category) {
-                if (in_array(trim(strtolower($category->term)), $filtertags)) {
-                    $containsfiltertag = true;
+            if (!empty($categories)) {
+                foreach ($categories as $category) {
+                    if (in_array(trim(strtolower($category->term)), $filtertags)) {
+                        $containsfiltertag = true;
+                    }
                 }
             }
 
index 7dc8ffd..661af04 100644 (file)
@@ -292,7 +292,7 @@ YUI.add('moodle-enrol_cohort-quickenrolment', function(Y) {
                             } else {
                                 if (result.response && result.response.message) {
                                     var alertpanel = new M.core.alert(result.response);
-                                    Y.Node.one('#id_yuialertconfirm-' + alertpanel.COUNT).focus();
+                                    Y.Node.one('#id_yuialertconfirm-' + alertpanel.get('COUNT')).focus();
                                 }
                                 var enrolled = Y.Node.create('<div class="'+CSS.COHORTBUTTON+' alreadyenrolled">'+M.str.enrol.synced+'</div>');
                                 node.one('.'+CSS.COHORT+' #cohortid_'+cohort.get(COHORTID)).replace(enrolled);
index 96488db..55eed55 100644 (file)
@@ -308,7 +308,7 @@ class core_grades_external extends external_api {
                                         'str_long_grade' => new external_value(
                                             PARAM_RAW, 'A nicely formatted string representation of the grade'),
                                         'str_feedback' => new external_value(
-                                            PARAM_TEXT, 'A string representation of the feedback from the grader'),
+                                            PARAM_RAW, 'A formatted string representation of the feedback from the grader'),
                                     )
                                 )
                             ),
@@ -345,7 +345,7 @@ class core_grades_external extends external_api {
                                         'str_grade' => new external_value(
                                             PARAM_RAW, 'A string representation of the grade'),
                                         'str_feedback' => new external_value(
-                                            PARAM_TEXT, 'A string representation of the feedback from the grader'),
+                                            PARAM_RAW, 'A formatted string representation of the feedback from the grader'),
                                     )
                                 )
                             ),
index e2ec497..d4e09f9 100644 (file)
@@ -456,6 +456,8 @@ class manager {
         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
         $records = $DB->get_records_select('task_scheduled', $where, $params);
 
+        $pluginmanager = \core_plugin_manager::instance();
+
         foreach ($records as $record) {
 
             if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
@@ -463,6 +465,17 @@ class manager {
                 $task = self::scheduled_task_from_record($record);
 
                 $task->set_lock($lock);
+
+                // See if the component is disabled.
+                $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
+
+                if ($plugininfo) {
+                    if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
+                        $lock->release();
+                        continue;
+                    }
+                }
+
                 if (!$task->is_blocking()) {
                     $cronlock->release();
                 } else {
index 47e93c7..ed2fc03 100644 (file)
@@ -183,6 +183,15 @@ abstract class scheduled_task extends task_base {
         return $this->disabled;
     }
 
+    /**
+     * Override this function if you want this scheduled task to run, even if the component is disabled.
+     *
+     * @return bool
+     */
+    public function get_run_if_component_disabled() {
+        return false;
+    }
+
     /**
      * Take a cron field definition and return an array of valid numbers with the range min-max.
      *
index 1df60c4..b03f48a 100644 (file)
@@ -104,7 +104,7 @@ class csv_import_reader {
         // Create a temporary file and store the csv file there,
         // do not try using fgetcsv() because there is nothing
         // to split rows properly - fgetcsv() itself can not do it.
-        $tempfile = tempnam(make_temp_directory('/cvsimport'), 'tmp');
+        $tempfile = tempnam(make_temp_directory('/csvimport'), 'tmp');
         if (!$fp = fopen($tempfile, 'w+b')) {
             $this->_error = get_string('cannotsavedata', 'error');
             @unlink($tempfile);
index 32fec85..0e92edf 100644 (file)
@@ -1721,6 +1721,8 @@ class file_storage {
      * @return array (contenthash, filesize, newfile)
      */
     public function add_string_to_pool($content) {
+        global $CFG;
+
         $contenthash = sha1($content);
         $filesize = strlen($content); // binary length
 
@@ -1755,7 +1757,13 @@ class file_storage {
         // Hopefully this works around most potential race conditions.
 
         $prev = ignore_user_abort(true);
-        $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
+
+        if (!empty($CFG->preventfilelocking)) {
+            $newsize = file_put_contents($hashfile.'.tmp', $content);
+        } else {
+            $newsize = file_put_contents($hashfile.'.tmp', $content, LOCK_EX);
+        }
+
         if ($newsize === false) {
             // Borked permissions most likely.
             ignore_user_abort($prev);
index 82938bc..bbc616f 100644 (file)
@@ -827,6 +827,10 @@ class core_renderer extends renderer_base {
             $this->page->add_body_class('userloggedinas');
         }
 
+        if (is_role_switched($this->page->course->id)) {
+            $this->page->add_body_class('userswitchedrole');
+        }
+
         // Give themes a chance to init/alter the page object.
         $this->page->theme->init_page($this->page);
 
index 9c79b77..774540e 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js and b/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-debug.js differ
index 22a7bcc..176b086 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js and b/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception-min.js differ
index 9c79b77..774540e 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js and b/lib/yui/build/moodle-core-notification-ajaxexception/moodle-core-notification-ajaxexception.js differ
index 083319b..47a76b6 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-debug.js differ
index da82280..818facf 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert-min.js differ
index 083319b..47a76b6 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js and b/lib/yui/build/moodle-core-notification-alert/moodle-core-notification-alert.js differ
index 6348d4d..51f1198 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-debug.js differ
index 3d45e82..3365c89 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm-min.js differ
index 6348d4d..51f1198 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js and b/lib/yui/build/moodle-core-notification-confirm/moodle-core-notification-confirm.js differ
index 3f813af..96a8aa7 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-debug.js differ
index 4fb70e3..e1a038f 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception-min.js differ
index 3f813af..96a8aa7 100644 (file)
Binary files a/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js and b/lib/yui/build/moodle-core-notification-exception/moodle-core-notification-exception.js differ
index a28774a..65eeb34 100644 (file)
@@ -30,7 +30,7 @@ Y.extend(AJAXEXCEPTION, M.core.notification.info, {
             delay = this.get('hideTimeoutDelay');
         this.get(BASE).addClass('moodle-dialogue-exception');
         this.setStdModContent(Y.WidgetStdMod.HEADER,
-                '<h1 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + Y.Escape.html(config.name) + '</h1>',
+                '<h3 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + Y.Escape.html(config.name) + '</h3>',
                 Y.WidgetStdMod.REPLACE);
         content = Y.Node.create('<div class="moodle-ajaxexception"></div>')
                 .append(Y.Node.create('<div class="moodle-exception-message">'+Y.Escape.html(this.get('error'))+'</div>'))
index 29c1aaa..4347232 100644 (file)
@@ -40,7 +40,7 @@ Y.extend(ALERT, M.core.notification.info, {
         this.get(BASE).addClass('moodle-dialogue-confirm');
         this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
         this.setStdModContent(Y.WidgetStdMod.HEADER,
-                '<h1 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h1>', Y.WidgetStdMod.REPLACE);
+                '<h3 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h3>', Y.WidgetStdMod.REPLACE);
 
         this._closeEvents.push(
             Y.on('key', this.submit, window, 'down:13', this),
index d2e640b..6880ed3 100644 (file)
@@ -43,7 +43,7 @@ Y.extend(CONFIRM, M.core.notification.info, {
         this.get(BASE).addClass('moodle-dialogue-confirm');
         this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
         this.setStdModContent(Y.WidgetStdMod.HEADER,
-                '<h1 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h1>', Y.WidgetStdMod.REPLACE);
+                '<h3 id="moodle-dialogue-'+this.get('COUNT')+'-header-text">' + this.get(TITLE) + '</h3>', Y.WidgetStdMod.REPLACE);
 
         this._closeEvents.push(
             Y.on('key', this.submit, window, 'down:27', this, false),
index 72f71e2..c6096f3 100644 (file)
@@ -46,7 +46,7 @@ Y.extend(EXCEPTION, M.core.notification.info, {
             delay = this.get('hideTimeoutDelay');
         this.get(BASE).addClass('moodle-dialogue-exception');
         this.setStdModContent(Y.WidgetStdMod.HEADER,
-                '<h1 id="moodle-dialogue-'+config.COUNT+'-header-text">' + Y.Escape.html(config.name) + '</h1>',
+                '<h3 id="moodle-dialogue-'+config.COUNT+'-header-text">' + Y.Escape.html(config.name) + '</h3>',
                 Y.WidgetStdMod.REPLACE);
         content = Y.Node.create('<div class="moodle-exception"></div>')
                 .append(Y.Node.create('<div class="moodle-exception-message">'+Y.Escape.html(this.get('message'))+'</div>'))
index 115a04e..44a1f70 100644 (file)
@@ -202,7 +202,8 @@ class assign_grading_table extends table_sql implements renderable {
             }
         }
 
-        if ($this->assignment->get_instance()->markingallocation) {
+        if ($this->assignment->get_instance()->markingworkflow &&
+            $this->assignment->get_instance()->markingallocation) {
             if (has_capability('mod/assign:manageallocations', $this->assignment->get_context())) {
                 // Check to see if marker filter is set.
                 $markerfilter = (int)get_user_preferences('assign_markerfilter', '');
@@ -296,7 +297,8 @@ class assign_grading_table extends table_sql implements renderable {
             $headers[] = get_string('submissionteam', 'assign');
         }
         // Allocated marker.
-        if ($this->assignment->get_instance()->markingallocation &&
+        if ($this->assignment->get_instance()->markingworkflow &&
+            $this->assignment->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->assignment->get_context())) {
             // Add a column for the allocated marker.
             $columns[] = 'allocatedmarker';
@@ -504,7 +506,8 @@ class assign_grading_table extends table_sql implements renderable {
             $name = 'quickgrade_' . $row->id . '_workflowstate';
             $o .= html_writer::select($workflowstates, $name, $workflowstate, array('' => $notmarked));
             // Check if this user is a marker that can't manage allocations and doesn't have the marker column added.
-            if ($this->assignment->get_instance()->markingallocation &&
+            if ($this->assignment->get_instance()->markingworkflow &&
+                $this->assignment->get_instance()->markingallocation &&
                 !has_capability('mod/assign:manageallocations', $this->assignment->get_context())) {
 
                 $name = 'quickgrade_' . $row->id . '_allocatedmarker';
index 24a1959..27cb50e 100644 (file)
@@ -595,6 +595,9 @@ class assign {
         }
         $update->markingworkflow = $formdata->markingworkflow;
         $update->markingallocation = $formdata->markingallocation;
+        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
+            $update->markingallocation = 0;
+        }
 
         $returnid = $DB->insert_record('assign', $update);
         $this->instance = $DB->get_record('assign', array('id'=>$returnid), '*', MUST_EXIST);
@@ -945,6 +948,9 @@ class assign {
         }
         $update->markingworkflow = $formdata->markingworkflow;
         $update->markingallocation = $formdata->markingallocation;
+        if (empty($update->markingworkflow)) { // If marking workflow is disabled, make sure allocation is disabled.
+            $update->markingallocation = 0;
+        }
 
         $result = $DB->update_record('assign', $update);
         $this->instance = $DB->get_record('assign', array('id'=>$update->id), '*', MUST_EXIST);
@@ -1888,12 +1894,12 @@ class assign {
         if (!$batchusers) {
             $userid = required_param('userid', PARAM_INT);
 
-            $grade = $this->get_user_grade($userid, false);
+            $flags = $this->get_user_flags($userid, false);
 
             $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
 
-            if ($grade) {
-                $data->extensionduedate = $grade->extensionduedate;
+            if ($flags) {
+                $data->extensionduedate = $flags->extensionduedate;
             }
             $data->userid = $userid;
         } else {
@@ -3067,7 +3073,8 @@ class assign {
         $quickgrading = get_user_preferences('assign_quickgrading', false);
         $showonlyactiveenrolopt = has_capability('moodle/course:viewsuspendedusers', $this->context);
 
-        $markingallocation = $this->get_instance()->markingallocation &&
+        $markingallocation = $this->get_instance()->markingworkflow &&
+            $this->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->context);
         // Get markers to use in drop lists.
         $markingallocationoptions = array();
@@ -3437,7 +3444,8 @@ class assign {
         require_once($CFG->dirroot . '/mod/assign/gradingbatchoperationsform.php');
         require_sesskey();
 
-        $markingallocation = $this->get_instance()->markingallocation &&
+        $markingallocation = $this->get_instance()->markingworkflow &&
+            $this->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->context);
 
         $batchformparams = array('cm'=>$this->get_course_module()->id,
@@ -5136,7 +5144,8 @@ class assign {
                 $current->grade = floatval($current->grade);
             }
             $gradechanged = $gradecolpresent && $current->grade !== $modified->grade;
-            $markingallocationchanged = $this->get_instance()->markingallocation &&
+            $markingallocationchanged = $this->get_instance()->markingworkflow &&
+                                        $this->get_instance()->markingallocation &&
                                             ($modified->allocatedmarker !== false) &&
                                             ($current->allocatedmarker != $modified->allocatedmarker);
             $workflowstatechanged = $this->get_instance()->markingworkflow &&
@@ -5319,7 +5328,8 @@ class assign {
             $showonlyactiveenrolopt = false;
         }
 
-        $markingallocation = $this->get_instance()->markingallocation &&
+        $markingallocation = $this->get_instance()->markingworkflow &&
+            $this->get_instance()->markingallocation &&
             has_capability('mod/assign:manageallocations', $this->context);
         // Get markers to use in drop lists.
         $markingallocationoptions = array();
@@ -5417,8 +5427,12 @@ class assign {
             $user = $DB->get_record('user', array('id' => $submission->userid), '*', MUST_EXIST);
             $name = fullname($user);
         } else {
-            $group = $DB->get_record('groups', array('id' => $submission->groupid), '*', MUST_EXIST);
-            $name = $group->name;
+            $group = $this->get_submission_group($submission->userid);
+            if ($group) {
+                $name = $group->name;
+            } else {
+                $name = get_string('defaultteam', 'assign');
+            }
         }
         $status = get_string('submissionstatus_' . $submission->status, 'assign');
         $params = array('id'=>$submission->userid, 'fullname'=>$name, 'status'=>$status);
@@ -5895,7 +5909,10 @@ class assign {
             $mform->addHelpButton('workflowstate', 'markingworkflowstate', 'assign');
         }
 
-        if ($this->get_instance()->markingallocation && has_capability('mod/assign:manageallocations', $this->context)) {
+        if ($this->get_instance()->markingworkflow &&
+            $this->get_instance()->markingallocation &&
+            has_capability('mod/assign:manageallocations', $this->context)) {
+
             $markers = get_users_by_capability($this->context, 'mod/assign:grade');
             $markerlist = array('' =>  get_string('choosemarker', 'assign'));
             foreach ($markers as $marker) {
index d168afb..1a203d2 100644 (file)
@@ -606,6 +606,38 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $this->assertEquals(self::GROUP_COUNT + 1, $assign->count_teams());
     }
 
+    public function test_submit_to_default_group() {
+        global $DB;
+
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        $this->setUser($this->editingteachers[0]);
+        $params = array('teamsubmission' => 1,
+                        'assignsubmission_onlinetext_enabled' => 1,
+                        'submissiondrafts'=>0);
+        $assign = $this->create_instance($params);
+
+        $newstudent = $this->getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname'=>'student'));
+        $this->getDataGenerator()->enrol_user($newstudent->id,
+                                              $this->course->id,
+                                              $studentrole->id);
+        $this->setUser($newstudent);
+        $data = new stdClass();
+        $data->onlinetext_editor = array('itemid'=>file_get_unused_draft_itemid(),
+                                         'text'=>'Submission text',
+                                         'format'=>FORMAT_MOODLE);
+        $notices = array();
+
+        $group = $assign->get_submission_group($newstudent->id);
+        $this->assertFalse($group, 'New student is in default group');
+        $assign->save_submission($data, $notices);
+        $this->assertEmpty($notices, 'No errors on save submission');
+
+        $sink->close();
+    }
+
     public function test_count_submissions() {
         $this->create_extra_users();
         $this->setUser($this->editingteachers[0]);
index 88e36cb..dcded7a 100644 (file)
     $PAGE->set_title("$course->shortname: ".format_string($discussion->name));
     $PAGE->set_heading($course->fullname);
     $PAGE->set_button($searchform);
+    $renderer = $PAGE->get_renderer('mod_forum');
+
     echo $OUTPUT->header();
 
     $headingvalue = format_string($forum->name);
         }
     }
 
+    // Output the links to neighbour discussions.
+    $neighbours = forum_get_discussion_neighbours($cm, $discussion);
+    echo $renderer->neighbouring_discussion_navigation($neighbours['prev'], $neighbours['next']);
+
 /// Print the controls across the top
     echo '<div class="discussioncontrols clearfix">';
 
index c1e8fd2..1c6804f 100644 (file)
@@ -317,6 +317,7 @@ $string['namenews'] = 'News forum';
 $string['namenews_help'] = 'The news forum is a special forum for announcements that is automatically created when a course is created. A course can have only one news forum. Only teachers and administrators can post in the news forum. The "Latest news" block will display recent discussions from the news forum.';
 $string['namesocial'] = 'Social forum';
 $string['nameteacher'] = 'Teacher forum';
+$string['nextdiscussiona'] = 'Next discussion: {$a}';
 $string['newforumposts'] = 'New forum posts';
 $string['noattachments'] = 'There are no attachments to this post';
 $string['nodiscussions'] = 'There are no discussion topics yet in this forum';
@@ -359,6 +360,7 @@ $string['page-mod-forum-view'] = 'Forum module main page';
 $string['page-mod-forum-discuss'] = 'Forum module discussion thread page';
 $string['parent'] = 'Show parent';
 $string['parentofthispost'] = 'Parent of this post';
+$string['prevdiscussiona'] = 'Previous discussion: {$a}';
 $string['pluginadministration'] = 'Forum administration';
 $string['pluginname'] = 'Forum';
 $string['postadded'] = '<p>Your post was successfully added.</p> <p>You have {$a} to edit it if you want to make any changes.</p>';
index 024db75..232c3ed 100644 (file)
@@ -2638,6 +2638,95 @@ function forum_get_discussions($cm, $forumsort="d.timemodified DESC", $fullpost=
     return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
 }
 
+/**
+ * Gets the neighbours (previous and next) of a discussion.
+ *
+ * The calculation is based on the timemodified of the discussion and does not handle
+ * the neighbours having an identical timemodified. The reason is that we do not have any
+ * other mean to sort the records, e.g. we cannot use IDs as a greater ID can have a lower
+ * timemodified.
+ *
+ * Please note that this does not check whether or not the discussion passed is accessible
+ * by the user, it simply uses it as a reference to find the neighbours. On the other hand,
+ * the returned neighbours are checked and are accessible to the current user.
+ *
+ * @param object $cm The CM record.
+ * @param object $discussion The discussion record.
+ * @return array That always contains the keys 'prev' and 'next'. When there is a result
+ *               they contain the record with minimal information such as 'id' and 'name'.
+ *               When the neighbour is not found the value is false.
+ */
+function forum_get_discussion_neighbours($cm, $discussion) {
+    global $CFG, $DB, $USER;
+
+    if ($cm->instance != $discussion->forum) {
+        throw new coding_exception('Discussion is not part of the same forum.');
+    }
+
+    $neighbours = array('prev' => false, 'next' => false);
+    $now = round(time(), -2);
+    $params = array();
+
+    $modcontext = context_module::instance($cm->id);
+    $groupmode    = groups_get_activity_groupmode($cm);
+    $currentgroup = groups_get_activity_group($cm);
+
+    // Users must fulfill timed posts.
+    $timelimit = '';
+    if (!empty($CFG->forum_enabletimedposts)) {
+        if (!has_capability('mod/forum:viewhiddentimedposts', $modcontext)) {
+            $timelimit = ' AND ((d.timestart <= :tltimestart AND (d.timeend = 0 OR d.timeend > :tltimeend))';
+            $params['tltimestart'] = $now;
+            $params['tltimeend'] = $now;
+            if (isloggedin()) {
+                $timelimit .= ' OR d.userid = :tluserid';
+                $params['tluserid'] = $USER->id;
+            }
+            $timelimit .= ')';
+        }
+    }
+
+    // Limiting to posts accessible according to groups.
+    $groupselect = '';
+    if ($groupmode) {
+        if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $modcontext)) {
+            if ($currentgroup) {
+                $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
+                $params['groupid'] = $currentgroup;
+            }
+        } else {
+            if ($currentgroup) {
+                $groupselect = 'AND (d.groupid = :groupid OR d.groupid = -1)';
+                $params['groupid'] = $currentgroup;
+            } else {
+                $groupselect = 'AND d.groupid = -1';
+            }
+        }
+    }
+
+    $params['forumid'] = $cm->instance;
+    $params['discid'] = $discussion->id;
+    $params['disctimemodified'] = $discussion->timemodified;
+
+    $sql = "SELECT d.id, d.name, d.timemodified, d.groupid, d.timestart, d.timeend
+              FROM {forum_discussions} d
+             WHERE d.forum = :forumid
+               AND d.id <> :discid
+                   $timelimit
+                   $groupselect";
+
+    $prevsql = $sql . " AND d.timemodified < :disctimemodified
+                   ORDER BY d.timemodified DESC";
+
+    $nextsql = $sql . " AND d.timemodified > :disctimemodified
+                   ORDER BY d.timemodified ASC";
+
+    $neighbours['prev'] = $DB->get_record_sql($prevsql, $params, IGNORE_MULTIPLE);
+    $neighbours['next'] = $DB->get_record_sql($nextsql, $params, IGNORE_MULTIPLE);
+
+    return $neighbours;
+}
+
 /**
  *
  * @global object
index f479e77..237ee77 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  **/
 class mod_forum_renderer extends plugin_renderer_base {
+
+    /**
+     * Returns the navigation to the previous and next discussion.
+     *
+     * @param mixed $prev Previous discussion record, or false.
+     * @param mixed $next Next discussion record, or false.
+     * @return string The output.
+     */
+    public function neighbouring_discussion_navigation($prev, $next) {
+        $html = '';
+        if ($prev || $next) {
+            $html .= html_writer::start_tag('div', array('class' => 'discussion-nav clearfix'));
+            $html .= html_writer::start_tag('ul');
+            if ($prev) {
+                $url = new moodle_url('/mod/forum/discuss.php', array('d' => $prev->id));
+                $html .= html_writer::start_tag('li', array('class' => 'prev-discussion'));
+                $html .= html_writer::link($url, $prev->name,
+                    array('aria-label' => get_string('prevdiscussiona', 'mod_forum', $prev->name)));
+                $html .= html_writer::end_tag('li');
+            }
+            if ($next) {
+                $url = new moodle_url('/mod/forum/discuss.php', array('d' => $next->id));
+                $html .= html_writer::start_tag('li', array('class' => 'next-discussion'));
+                $html .= html_writer::link($url, $next->name,
+                    array('aria-label' => get_string('nextdiscussiona', 'mod_forum', $next->name)));
+                $html .= html_writer::end_tag('li');
+            }
+            $html .= html_writer::end_tag('ul');
+            $html .= html_writer::end_tag('div');
+        }
+        return $html;
+    }
+
     /**
      * This method is used to generate HTML for a subscriber selection form that
      * uses two user_selector controls
index aefeb13..3c239bb 100644 (file)
@@ -112,3 +112,28 @@ span.unread {
 .forumpost.unread .row.header {
     border-bottom: 1px solid #DDD;
 }
+
+/* Discussion navigation */
+.path-mod-forum .discussion-nav {
+    margin: .5em 0;
+}
+.path-mod-forum .discussion-nav ul {
+    margin: 0;
+    list-style: none;
+}
+.dir-rtl.path-mod-forum .discussion-nav .next-discussion:after,
+.path-mod-forum .discussion-nav .prev-discussion:before {
+    content: ' ◄ ';
+}
+.dir-rtl.path-mod-forum .discussion-nav .prev-discussion:before,
+.path-mod-forum .discussion-nav .next-discussion:after {
+    content: ' ► ';
+}
+.dir-rtl.path-mod-forum .discussion-nav .prev-discussion,
+.path-mod-forum .discussion-nav .next-discussion {
+    float: right;
+}
+.dir-rtl.path-mod-forum .discussion-nav .next-discussion,
+.path-mod-forum .discussion-nav .prev-discussion {
+    float: left;
+}
diff --git a/mod/forum/tests/behat/discussion_navigation.feature b/mod/forum/tests/behat/discussion_navigation.feature
new file mode 100644 (file)
index 0000000..4491b6c
--- /dev/null
@@ -0,0 +1,165 @@
+@mod @mod_forum
+Feature: A user can navigate to previous and next discussions
+  In order to get go the previous discussion
+  As a user
+  I need to click on the previous discussion link
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@asd.com |
+      | student2 | Student | 2 | student2@asd.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+    And I log in as "admin"
+    And I follow "Course 1"
+    And I navigate to "Groups" node in "Users"
+    And I press "Create group"
+    And I set the following fields to these values:
+      | Group name | Group 1 |
+    And I press "Save changes"
+    And I press "Create group"
+    And I set the following fields to these values:
+      | Group name | Group 2 |
+    And I press "Save changes"
+    And I add "Student 1" user to "Group 1" group members
+    And I add "Student 2" user to "Group 2" group members
+    And I am on homepage
+    And I follow "Course 1"
+    And I turn editing mode on
+
+  @javascript
+  Scenario: A user can navigate between discussions
+    Given I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 3 |
+      | Message | Test post message |
+    When I follow "Discussion 3"
+    Then I should not see "Discussion 1"
+    And I should see "Discussion 2"
+    And I follow "Discussion 2"
+    And I should see "Discussion 1"
+    And I should see "Discussion 3"
+    And I follow "Discussion 1"
+    And I should see "Discussion 2"
+    And I should not see "Discussion 3"
+    And I follow "Reply"
+    And I set the following fields to these values:
+      | Message | Answer to discussion |
+    And I press "Post to forum"
+    And I should not see "Discussion 2"
+    And I should see "Discussion 3"
+    And I follow "Discussion 3"
+    And I should see "Discussion 1"
+    And I should see "Discussion 2"
+    And I follow "Discussion 2"
+    And I should not see "Discussion 1"
+    And I should see "Discussion 3"
+
+  @javascript
+  Scenario: A user can navigate between discussions with visible groups
+    Given I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+      | Group mode | Visible groups |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 0 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 0 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 1 |
+      | Message | Test post message |
+      | Group   | Group 1 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 1 |
+      | Message | Test post message |
+      | Group   | Group 1 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 2 |
+      | Message | Test post message |
+      | Group   | Group 2 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 2 |
+      | Message | Test post message |
+      | Group   | Group 2 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test forum name"
+    And I set the field "Visible groups" to "All participants"
+    And I follow "Discussion 1 Group 0"
+    Then I should see "Discussion 2 Group 0"
+    And I should not see "Group 1"
+    And I should not see "Group 2"
+    And I follow "Discussion 2 Group 0"
+    And I should see "Discussion 1 Group 0"
+    And I should see "Discussion 1 Group 1"
+    And I follow "Discussion 1 Group 1"
+    And I should see "Discussion 2 Group 0"
+    And I should see "Discussion 2 Group 1"
+    And I follow "Test forum name"
+    And I follow "Discussion 1 Group 2"
+    And I should see "Discussion 2 Group 1"
+    And I should see "Discussion 2 Group 2"
+    And I follow "Test forum name"
+    And I set the field "Visible groups" to "Group 1"
+    And I follow "Discussion 1 Group 1"
+    Then I should see "Discussion 2 Group 0"
+    And I should see "Discussion 2 Group 1"
+    And I follow "Discussion 2 Group 1"
+    And I should see "Discussion 1 Group 1"
+    And I should not see "Group 2"
+
+  @javascript
+  Scenario: A user can navigate between discussions with separate groups
+    Given I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+      | Group mode | Separate groups |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 0 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 0 |
+      | Message | Test post message |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 1 |
+      | Message | Test post message |
+      | Group   | Group 1 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 1 |
+      | Message | Test post message |
+      | Group   | Group 1 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 1 Group 2 |
+      | Message | Test post message |
+      | Group   | Group 2 |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Discussion 2 Group 2 |
+      | Message | Test post message |
+      | Group   | Group 2 |
+    And I log out
+    When I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Test forum name"
+    And I follow "Discussion 1 Group 1"
+    Then I should see "Discussion 2 Group 0"
+    And I should see "Discussion 2 Group 1"
+    And I follow "Discussion 2 Group 1"
+    And I should see "Discussion 1 Group 1"
+    And I should not see "Group 2"
index b955a2c..e7417b5 100644 (file)
@@ -788,4 +788,389 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $this->assertEquals($forumcontext, $result);
         $this->assertEquals(1, $aftercount - $startcount);
     }
+
+    /**
+     * Test getting the neighbour threads of a discussion.
+     */
+    public function test_forum_get_neighbours() {
+        global $CFG, $DB;
+        $this->resetAfterTest();
+
+        // Setup test data.
+        $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+
+        $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
+        $cm = get_coursemodule_from_instance('forum', $forum->id);
+        $context = context_module::instance($cm->id);
+
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $user->id;
+        $record->forum = $forum->id;
+        $disc1 = $forumgen->create_discussion($record);
+        sleep(1);
+        $disc2 = $forumgen->create_discussion($record);
+        sleep(1);
+        $disc3 = $forumgen->create_discussion($record);
+        sleep(1);
+        $disc4 = $forumgen->create_discussion($record);
+        sleep(1);
+        $disc5 = $forumgen->create_discussion($record);
+
+        // Getting the neighbours.
+        $neighbours = forum_get_discussion_neighbours($cm, $disc1);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc2->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc2);
+        $this->assertEquals($disc1->id, $neighbours['prev']->id);
+        $this->assertEquals($disc3->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc3);
+        $this->assertEquals($disc2->id, $neighbours['prev']->id);
+        $this->assertEquals($disc4->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc4);
+        $this->assertEquals($disc3->id, $neighbours['prev']->id);
+        $this->assertEquals($disc5->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc5);
+        $this->assertEquals($disc4->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Post in some discussions. We manually update the discussion record because
+        // the data generator plays with timemodified in a way that would break this test.
+        sleep(1);
+        $disc1->timemodified = time();
+        $DB->update_record('forum_discussions', $disc1);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc5);
+        $this->assertEquals($disc4->id, $neighbours['prev']->id);
+        $this->assertEquals($disc1->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc2);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc3->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc1);
+        $this->assertEquals($disc5->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // After some discussions were created.
+        sleep(1);
+        $disc6 = $forumgen->create_discussion($record);
+        $neighbours = forum_get_discussion_neighbours($cm, $disc6);
+        $this->assertEquals($disc1->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        sleep(1);
+        $disc7 = $forumgen->create_discussion($record);
+        $neighbours = forum_get_discussion_neighbours($cm, $disc7);
+        $this->assertEquals($disc6->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Adding timed discussions.
+        $CFG->forum_enabletimedposts = true;
+        $now = time();
+        $past = $now - 60;
+        $future = $now + 60;
+
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $user->id;
+        $record->forum = $forum->id;
+        $record->timestart = $past;
+        $record->timeend = $future;
+        sleep(1);
+        $disc8 = $forumgen->create_discussion($record);
+        sleep(1);
+        $record->timestart = $future;
+        $record->timeend = 0;
+        $disc9 = $forumgen->create_discussion($record);
+        sleep(1);
+        $record->timestart = 0;
+        $record->timeend = 0;
+        $disc10 = $forumgen->create_discussion($record);
+        sleep(1);
+        $record->timestart = 0;
+        $record->timeend = $past;
+        $disc11 = $forumgen->create_discussion($record);
+        sleep(1);
+        $record->timestart = $past;
+        $record->timeend = $future;
+        $disc12 = $forumgen->create_discussion($record);
+
+        // Admin user ignores the timed settings of discussions.
+        $this->setAdminUser();
+        $neighbours = forum_get_discussion_neighbours($cm, $disc8);
+        $this->assertEquals($disc7->id, $neighbours['prev']->id);
+        $this->assertEquals($disc9->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc9);
+        $this->assertEquals($disc8->id, $neighbours['prev']->id);
+        $this->assertEquals($disc10->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc10);
+        $this->assertEquals($disc9->id, $neighbours['prev']->id);
+        $this->assertEquals($disc11->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc11);
+        $this->assertEquals($disc10->id, $neighbours['prev']->id);
+        $this->assertEquals($disc12->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc12);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Normal user can see their own timed discussions.
+        $this->setUser($user);
+        $neighbours = forum_get_discussion_neighbours($cm, $disc8);
+        $this->assertEquals($disc7->id, $neighbours['prev']->id);
+        $this->assertEquals($disc9->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc9);
+        $this->assertEquals($disc8->id, $neighbours['prev']->id);
+        $this->assertEquals($disc10->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc10);
+        $this->assertEquals($disc9->id, $neighbours['prev']->id);
+        $this->assertEquals($disc11->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc11);
+        $this->assertEquals($disc10->id, $neighbours['prev']->id);
+        $this->assertEquals($disc12->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc12);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Normal user does not ignore timed settings.
+        $this->setUser($user2);
+        $neighbours = forum_get_discussion_neighbours($cm, $disc8);
+        $this->assertEquals($disc7->id, $neighbours['prev']->id);
+        $this->assertEquals($disc10->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc10);
+        $this->assertEquals($disc8->id, $neighbours['prev']->id);
+        $this->assertEquals($disc12->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc12);
+        $this->assertEquals($disc10->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Reset to normal mode.
+        $CFG->forum_enabletimedposts = false;
+        $this->setAdminUser();
+
+        // Two discussions with identical timemodified ignore each other.
+        sleep(1);
+        $now = time();
+        $DB->update_record('forum_discussions', (object) array('id' => $disc3->id, 'timemodified' => $now));
+        $DB->update_record('forum_discussions', (object) array('id' => $disc2->id, 'timemodified' => $now));
+        $disc2 = $DB->get_record('forum_discussions', array('id' => $disc2->id));
+        $disc3 = $DB->get_record('forum_discussions', array('id' => $disc3->id));
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc2);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        $neighbours = forum_get_discussion_neighbours($cm, $disc3);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+    }
+
+    /**
+     * Test getting the neighbour threads of a discussion.
+     */
+    public function test_forum_get_neighbours_with_groups() {
+        $this->resetAfterTest();
+
+        // Setup test data.
+        $forumgen = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+        $course = $this->getDataGenerator()->create_course();
+        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $group2 = $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+        $user1 = $this->getDataGenerator()->create_user();
+        $user2 = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->create_group_member(array('userid' => $user1->id, 'groupid' => $group1->id));
+
+        $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'groupmode' => VISIBLEGROUPS));
+        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id, 'groupmode' => SEPARATEGROUPS));
+        $cm1 = get_coursemodule_from_instance('forum', $forum1->id);
+        $cm2 = get_coursemodule_from_instance('forum', $forum2->id);
+        $context1 = context_module::instance($cm1->id);
+        $context2 = context_module::instance($cm2->id);
+
+        // Creating discussions in both forums.
+        $record = new stdClass();
+        $record->course = $course->id;
+        $record->userid = $user1->id;
+        $record->forum = $forum1->id;
+        $record->groupid = $group1->id;
+        $disc11 = $forumgen->create_discussion($record);
+        $record->forum = $forum2->id;
+        $disc21 = $forumgen->create_discussion($record);
+
+        sleep(1);
+        $record->userid = $user2->id;
+        $record->forum = $forum1->id;
+        $record->groupid = $group2->id;
+        $disc12 = $forumgen->create_discussion($record);
+        $record->forum = $forum2->id;
+        $disc22 = $forumgen->create_discussion($record);
+
+        sleep(1);
+        $record->userid = $user1->id;
+        $record->forum = $forum1->id;
+        $record->groupid = null;
+        $disc13 = $forumgen->create_discussion($record);
+        $record->forum = $forum2->id;
+        $disc23 = $forumgen->create_discussion($record);
+
+        sleep(1);
+        $record->userid = $user2->id;
+        $record->forum = $forum1->id;
+        $record->groupid = $group2->id;
+        $disc14 = $forumgen->create_discussion($record);
+        $record->forum = $forum2->id;
+        $disc24 = $forumgen->create_discussion($record);
+
+        sleep(1);
+        $record->userid = $user1->id;
+        $record->forum = $forum1->id;
+        $record->groupid = $group1->id;
+        $disc15 = $forumgen->create_discussion($record);
+        $record->forum = $forum2->id;
+        $disc25 = $forumgen->create_discussion($record);
+
+        // Admin user can see all groups.
+        $this->setAdminUser();
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc11);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc12->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc21);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc22->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc12);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc22);
+        $this->assertEquals($disc21->id, $neighbours['prev']->id);
+        $this->assertEquals($disc23->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc13);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc14->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc23);
+        $this->assertEquals($disc22->id, $neighbours['prev']->id);
+        $this->assertEquals($disc24->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc14);
+        $this->assertEquals($disc13->id, $neighbours['prev']->id);
+        $this->assertEquals($disc15->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc24);
+        $this->assertEquals($disc23->id, $neighbours['prev']->id);
+        $this->assertEquals($disc25->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc15);
+        $this->assertEquals($disc14->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc25);
+        $this->assertEquals($disc24->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Admin user is only viewing group 1.
+        $_POST['group'] = $group1->id;
+        $this->assertEquals($group1->id, groups_get_activity_group($cm1, true));
+        $this->assertEquals($group1->id, groups_get_activity_group($cm2, true));
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc11);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc21);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc23->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc13);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc15->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc23);
+        $this->assertEquals($disc21->id, $neighbours['prev']->id);
+        $this->assertEquals($disc25->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc15);
+        $this->assertEquals($disc13->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc25);
+        $this->assertEquals($disc23->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Normal user viewing non-grouped posts (this is only possible in visible groups).
+        $this->setUser($user1);
+        $_POST['group'] = 0;
+        $this->assertEquals(0, groups_get_activity_group($cm1, true));
+
+        // They can see anything in visible groups.
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc12);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc13);
+        $this->assertEquals($disc12->id, $neighbours['prev']->id);
+        $this->assertEquals($disc14->id, $neighbours['next']->id);
+
+        // Normal user, orphan of groups, can only see non-grouped posts in separate groups.
+        $this->setUser($user2);
+        $_POST['group'] = 0;
+        $this->assertEquals(0, groups_get_activity_group($cm2, true));
+
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc23);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEmpty($neighbours['next']);
+
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc22);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc23->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc24);
+        $this->assertEquals($disc23->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Switching to viewing group 1.
+        $this->setUser($user1);
+        $_POST['group'] = $group1->id;
+        $this->assertEquals($group1->id, groups_get_activity_group($cm1, true));
+        $this->assertEquals($group1->id, groups_get_activity_group($cm2, true));
+
+        // They can see non-grouped or same group.
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc11);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc13->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc21);
+        $this->assertEmpty($neighbours['prev']);
+        $this->assertEquals($disc23->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc13);
+        $this->assertEquals($disc11->id, $neighbours['prev']->id);
+        $this->assertEquals($disc15->id, $neighbours['next']->id);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc23);
+        $this->assertEquals($disc21->id, $neighbours['prev']->id);
+        $this->assertEquals($disc25->id, $neighbours['next']->id);
+
+        $neighbours = forum_get_discussion_neighbours($cm1, $disc15);
+        $this->assertEquals($disc13->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+        $neighbours = forum_get_discussion_neighbours($cm2, $disc25);
+        $this->assertEquals($disc23->id, $neighbours['prev']->id);
+        $this->assertEmpty($neighbours['next']);
+
+        // Querying the neighbours of a discussion passing the wrong CM.
+        $this->setExpectedException('coding_exception');
+        forum_get_discussion_neighbours($cm2, $disc11);
+    }
 }
index 90240f3..27362c3 100644 (file)
@@ -60,105 +60,114 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
     text_range = '-1#1';
 
     // The AICC data model
-    var datamodel =  {
-        'cmi._children':{'defaultvalue':cmi_children, 'mod':'r', 'writeerror':'402'},
-        'cmi._version':{'defaultvalue':'3.4', 'mod':'r', 'writeerror':'402'},
-        'cmi.core._children':{'defaultvalue':core_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.core.student_id':{'defaultvalue':def['cmi.core.student_id'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.student_name':{'defaultvalue':def['cmi.core.student_name'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_location':{'defaultvalue':def['cmi.core.lesson_location'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.credit':{'defaultvalue':def['cmi.core.credit'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_status':{'defaultvalue':def['cmi.core.lesson_status'], 'format':CMIStatus, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.exit':{'defaultvalue':def['cmi.core.exit'], 'format':CMIExit, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.core.entry':{'defaultvalue':def['cmi.core.entry'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.score._children':{'defaultvalue':score_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.core.score.raw':{'defaultvalue':def['cmi.core.score.raw'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.score.max':{'defaultvalue':def['cmi.core.score.max'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.score.min':{'defaultvalue':def['cmi.core.score.min'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'00:00:00', 'readerror':'404', 'writeerror':'405'},
-        'cmi.core.total_time':{'defaultvalue':def['cmi.core.total_time'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_mode':{'defaultvalue':def['cmi.core.lesson_mode'], 'mod':'r', 'writeerror':'403'},
-        'cmi.suspend_data':{'defaultvalue':def['cmi.suspend_data'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
-        'cmi.launch_data':{'defaultvalue':def['cmi.launch_data'], 'mod':'r', 'writeerror':'403'},
-        'cmi.comments':{'defaultvalue':def['cmi.comments'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
-        // deprecated evaluation attributes
-        'cmi.evaluation.comments._count':{'defaultvalue':'0', 'mod':'r', 'writeerror':'402'},
-        'cmi.evaluation.comments._children':{'defaultvalue':comments_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.evaluation.comments.n.content':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.evaluation.comments.n.location':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.evaluation.comments.n.time':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
-        'cmi.comments_from_lms':{'mod':'r', 'writeerror':'403'},
-        'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score._children':{'pattern':CMIIndex, 'mod':'r', 'writeerror':'402'},
-        'cmi.objectives.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data._children':{'defaultvalue':student_data_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.student_data.attempt_number':{'defaultvalue':def['cmi.student_data.attempt_number'], 'mod':'r', 'writeerror':'402'},
-        'cmi.student_data.tries.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data.tries.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data.tries.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data.tries.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data.tries.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data.mastery_score':{'defaultvalue':def['cmi.student_data.mastery_score'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_data.max_time_allowed':{'defaultvalue':def['cmi.student_data.max_time_allowed'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_data.time_limit_action':{'defaultvalue':def['cmi.student_data.time_limit_action'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_data.tries_during_lesson':{'defaultvalue':def['cmi.student_data.tries_during_lesson'], 'mod':'r', 'writeerror':'402'},
-        'cmi.student_preference._children':{'defaultvalue':student_preference_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.student_preference.audio':{'defaultvalue':'0', 'format':CMISInteger, 'range':audio_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.language':{'defaultvalue':'', 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.speed':{'defaultvalue':'0', 'format':CMISInteger, 'range':speed_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.text':{'defaultvalue':'0', 'format':CMISInteger, 'range':text_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.correct_responses._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'range':weighting_range, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.student_response':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'nav.event':{'defaultvalue':'', 'format':NAVEvent, 'mod':'w', 'readerror':'404', 'writeerror':'405'}
-    };
+    var datamodel = {};
+    for(scoid in def){
+        datamodel[scoid] = {
+            'cmi._children':{'defaultvalue':cmi_children, 'mod':'r', 'writeerror':'402'},
+            'cmi._version':{'defaultvalue':'3.4', 'mod':'r', 'writeerror':'402'},
+            'cmi.core._children':{'defaultvalue':core_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.core.student_id':{'defaultvalue':def[scoid]['cmi.core.student_id'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.student_name':{'defaultvalue':def[scoid]['cmi.core.student_name'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_location':{'defaultvalue':def[scoid]['cmi.core.lesson_location'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.credit':{'defaultvalue':def[scoid]['cmi.core.credit'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_status':{'defaultvalue':def[scoid]['cmi.core.lesson_status'], 'format':CMIStatus, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.exit':{'defaultvalue':def[scoid]['cmi.core.exit'], 'format':CMIExit, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.core.entry':{'defaultvalue':def[scoid]['cmi.core.entry'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.score._children':{'defaultvalue':score_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.core.score.raw':{'defaultvalue':def[scoid]['cmi.core.score.raw'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.score.max':{'defaultvalue':def[scoid]['cmi.core.score.max'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.score.min':{'defaultvalue':def[scoid]['cmi.core.score.min'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'00:00:00', 'readerror':'404', 'writeerror':'405'},
+            'cmi.core.total_time':{'defaultvalue':def[scoid]['cmi.core.total_time'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_mode':{'defaultvalue':def[scoid]['cmi.core.lesson_mode'], 'mod':'r', 'writeerror':'403'},
+            'cmi.suspend_data':{'defaultvalue':def[scoid]['cmi.suspend_data'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
+            'cmi.launch_data':{'defaultvalue':def[scoid]['cmi.launch_data'], 'mod':'r', 'writeerror':'403'},
+            'cmi.comments':{'defaultvalue':def[scoid]['cmi.comments'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
+            // deprecated evaluation attributes
+            'cmi.evaluation.comments._count':{'defaultvalue':'0', 'mod':'r', 'writeerror':'402'},
+            'cmi.evaluation.comments._children':{'defaultvalue':comments_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.evaluation.comments.n.content':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.evaluation.comments.n.location':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.evaluation.comments.n.time':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
+            'cmi.comments_from_lms':{'mod':'r', 'writeerror':'403'},
+            'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score._children':{'pattern':CMIIndex, 'mod':'r', 'writeerror':'402'},
+            'cmi.objectives.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data._children':{'defaultvalue':student_data_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.student_data.attempt_number':{'defaultvalue':def[scoid]['cmi.student_data.attempt_number'], 'mod':'r', 'writeerror':'402'},
+            'cmi.student_data.tries.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data.tries.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data.tries.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data.tries.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data.tries.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data.mastery_score':{'defaultvalue':def[scoid]['cmi.student_data.mastery_score'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_data.max_time_allowed':{'defaultvalue':def[scoid]['cmi.student_data.max_time_allowed'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_data.time_limit_action':{'defaultvalue':def[scoid]['cmi.student_data.time_limit_action'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_data.tries_during_lesson':{'defaultvalue':def[scoid]['cmi.student_data.tries_during_lesson'], 'mod':'r', 'writeerror':'402'},
+            'cmi.student_preference._children':{'defaultvalue':student_preference_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.student_preference.audio':{'defaultvalue':'0', 'format':CMISInteger, 'range':audio_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.language':{'defaultvalue':'', 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.speed':{'defaultvalue':'0', 'format':CMISInteger, 'range':speed_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.text':{'defaultvalue':'0', 'format':CMISInteger, 'range':text_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.correct_responses._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'range':weighting_range, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.student_response':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'nav.event':{'defaultvalue':'', 'format':NAVEvent, 'mod':'w', 'readerror':'404', 'writeerror':'405'}
+        };
+    }
 
-    //
-    // Datamodel inizialization
-    //
-    var cmi = new Object();
-        cmi.core = new Object();
-        cmi.core.score = new Object();
-        cmi.objectives = new Object();
-        cmi.student_data = new Object();
-        cmi.student_preference = new Object();
-        cmi.interactions = new Object();
-        // deprecated evaluation attributes
-        cmi.evaluation = new Object();
-        cmi.evaluation.comments = new Object();
-
-    // Navigation Object
-    var nav = new Object();
-
-    for (element in datamodel) {
-        if (element.match(/\.n\./) == null) {
-            if ((typeof eval('datamodel["'+element+'"].defaultvalue')) != 'undefined') {
-                eval(element+' = datamodel["'+element+'"].defaultvalue;');
-            } else {
-                eval(element+' = "";');
+    var cmi, nav;
+    function initdatamodel(scoid){
+        prerequrl = cfgwwwroot + "/mod/scorm/prereqs.php?a="+scormid+"&scoid="+scoid+"&attempt="+attempt+"&mode="+viewmode+"&currentorg="+currentorg+"&sesskey="+sesskey;
+        datamodelurlparams = "id="+cmid+"&a="+scormid+"&sesskey="+sesskey+"&attempt="+attempt+"&scoid="+scoid;
+
+        //
+        // Datamodel inizialization
+        //
+        cmi = new Object();
+            cmi.core = new Object();
+            cmi.core.score = new Object();
+            cmi.objectives = new Object();
+            cmi.student_data = new Object();
+            cmi.student_preference = new Object();
+            cmi.interactions = new Object();
+            // deprecated evaluation attributes
+            cmi.evaluation = new Object();
+            cmi.evaluation.comments = new Object();
+
+        // Navigation Object
+        nav = new Object();
+
+        for (element in datamodel[scoid]) {
+            if (element.match(/\.n\./) == null) {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue')) != 'undefined') {
+                    eval(element+' = datamodel["'+scoid+'"]["'+element+'"].defaultvalue;');
+                } else {
+                    eval(element+' = "";');
+                }
             }
         }
-    }
 
-    eval(cmiobj);
+        eval(cmiobj[scoid]);
 
-    if (cmi.core.lesson_status == '') {
-        cmi.core.lesson_status = 'not attempted';
+        if (cmi.core.lesson_status == '') {
+            cmi.core.lesson_status = 'not attempted';
+        }
     }
 
     //
@@ -167,6 +176,9 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
     var Initialized = false;
 
     function LMSInitialize (param) {
+        scoid = scorm_current_node ? scorm_current_node.scoid : scoid ;
+        initdatamodel(scoid);
+
         errorCode = "0";
         if (param == "") {
             if (!Initialized) {
@@ -221,8 +233,8 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
             if (element !="") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'w') {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'w') {
                             element = String(element).replace(expression, "_$1.");
                             elementIndexes = element.split('.');
                         subelement = 'cmi';
@@ -237,21 +249,21 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                             errorCode = "0"; // Need to check if it is the right errorCode
                         }
                     } else {
-                        errorCode = eval('datamodel["'+elementmodel+'"].readerror');
+                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].readerror');
                     }
                 } else {
                     childrenstr = '._children';
                     countstr = '._count';
                     if (elementmodel.substr(elementmodel.length-childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-childrenstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "202";
                         } else {
                             errorCode = "201";
                         }
                     } else if (elementmodel.substr(elementmodel.length-countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-countstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "203";
                         } else {
                             errorCode = "201";
@@ -275,9 +287,9 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'r') {
-                        expression = new RegExp(eval('datamodel["'+elementmodel+'"].format'));
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'r') {
+                        expression = new RegExp(eval('datamodel["'+scoid+'"]["'+elementmodel+'"].format'));
                         value = value+'';
                         matches = value.match(expression);
                         if (matches != null) {
@@ -324,8 +336,8 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                             }
                             //Store data
                             if (errorCode == "0") {
-                                if ((typeof eval('datamodel["'+elementmodel+'"].range')) != "undefined") {
-                                    range = eval('datamodel["'+elementmodel+'"].range');
+                                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range')) != "undefined") {
+                                    range = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range');
                                     ranges = range.split('#');
                                     value = value*1.0;
                                     if ((value >= ranges[0]) && (value <= ranges[1])) {
@@ -333,7 +345,7 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                                         errorCode = "0";
                                         return "true";
                                     } else {
-                                        errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                                     }
                                 } else {
                                     if (element == 'cmi.comments') {
@@ -346,10 +358,10 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                                 }
                             }
                         } else {
-                            errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                            errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                         }
                     } else {
-                        errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                     }
                 } else {
                     errorCode = "201"
@@ -471,11 +483,11 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                 element = parent+'.'+property;
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'r') {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'r') {
                         elementstring = '&'+underscore(element)+'='+escape(data[property]);
-                        if ((typeof eval('datamodel["'+elementmodel+'"].defaultvalue')) != "undefined") {
-                            if (eval('datamodel["'+elementmodel+'"].defaultvalue') != data[property]) {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"].defaultvalue')) != "undefined") {
+                            if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].defaultvalue') != data[property]) {
                                 datastring += elementstring;
                             }
                         } else {
@@ -502,7 +514,7 @@ function AICCapi(def, cmiobj, scormauto, cfgwwwroot, scormid, scoid, attempt, vi
                 }
             }
             if (cmi.core.lesson_mode == 'browse') {
-                if (datamodel['cmi.core.lesson_status'].defaultvalue == '' && cmi.core.lesson_status == 'not attempted') {
+                if (datamodel[scoid]['cmi.core.lesson_status'].defaultvalue == '' && cmi.core.lesson_status == 'not attempted') {
                     cmi.core.lesson_status = 'browsed';
                 }
             }
index 17839ac..9e89734 100644 (file)
 require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 
 $userdata = new stdClass();
-$def = get_scorm_default($userdata, $scorm, $scoid, $attempt, $mode);
+$def = new stdClass();
+$cmiobj = new stdClass();
 
 if (!isset($currentorg)) {
     $currentorg = '';
 }
 
-$cmiobj = '';
-$currentobj = '';
-$count = 0;
-foreach ($userdata as $element => $value) {
-    if (substr($element, 0, 14) == 'cmi.objectives') {
-        $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
-        preg_match('/\_(\d+)\./', $element, $matches);
-        if (count($matches) > 0 && $currentobj != $matches[1]) {
-            $currentobj = $matches[1];
-            $count++;
-            $end = strpos($element, $matches[1])+strlen($matches[1]);
-            $subelement = substr($element, 0, $end);
-            $cmiobj .= '    '.$subelement." = new Object();\n";
-            $cmiobj .= '    '.$subelement.".score = new Object();\n";
-            $cmiobj .= '    '.$subelement.".score._children = score_children;\n";
-            $cmiobj .= '    '.$subelement.".score.raw = '';\n";
-            $cmiobj .= '    '.$subelement.".score.min = '';\n";
-            $cmiobj .= '    '.$subelement.".score.max = '';\n";
+if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
+    // Drop keys so that it is a simple array.
+    $scoes = array_values($scoes);
+    foreach ($scoes as $sco) {
+        $def->{($sco->id)} = new stdClass();
+        $userdata->{($sco->id)} = new stdClass();
+        $def->{($sco->id)} = get_scorm_default($userdata->{($sco->id)}, $scorm, $sco->id, $attempt, $mode);
+
+        // Reconstitute objectives, comments_from_learner and comments_from_lms.
+        $cmiobj->{($sco->id)} = '';
+        $currentobj = '';
+        $count = 0;
+        foreach ($userdata as $element => $value) {
+            if (substr($element, 0, 14) == 'cmi.objectives') {
+                $element = preg_replace('/\.(\d+)\./', "_\$1.", $element);
+                preg_match('/\_(\d+)\./', $element, $matches);
+                if (count($matches) > 0 && $currentobj != $matches[1]) {
+                    $currentobj = $matches[1];
+                    $count++;
+                    $end = strpos($element, $matches[1]) + strlen($matches[1]);
+                    $subelement = substr($element, 0, $end);
+                    $cmiobj->{($sco->id)} .= '    '.$subelement." = new Object();\n";
+                    $cmiobj->{($sco->id)} .= '    '.$subelement.".score = new Object();\n";
+                    $cmiobj->{($sco->id)} .= '    '.$subelement.".score._children = score_children;\n";
+                    $cmiobj->{($sco->id)} .= '    '.$subelement.".score.raw = '';\n";
+                    $cmiobj->{($sco->id)} .= '    '.$subelement.".score.min = '';\n";
+                    $cmiobj->{($sco->id)} .= '    '.$subelement.".score.max = '';\n";
+                }
+                $cmiobj->{($sco->id)} .= '    '.$element.' = \''.$value."';\n";
+            }
+        }
+        if ($count > 0) {
+            $cmiobj->{($sco->id)} .= '    cmi.objectives._count = '.$count.";\n";
         }
-        $cmiobj .= '    '.$element.' = \''.$value."';\n";
     }
 }
-if ($count > 0) {
-    $cmiobj .= '    cmi.objectives._count = '.$count.";\n";
-}
+
 
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $scorm->auto, $CFG->wwwroot, $scorm->id, $scoid, $attempt,
                                                          $mode, $currentorg, sesskey(), $id));
index 771ef76..28b6fbb 100644 (file)
@@ -736,7 +736,7 @@ function LogAPICall(func, nam, val, rc) {
     if (func.match(/GetValue/)) {
         s += ' - ' + val;
     }
-    s += ' => ' + String(rc);
+    s += ' => ' + String(rc) + "   scoid = " + scorm_current_node.scoid;
     AppendToLog(s, rc);
 <?php
 if (scorm_debugging($scorm) && ($sco->scormtype == 'asset')) {
index 44ac498..a7a37b4 100644 (file)
@@ -56,99 +56,111 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
     speed_range = '-100#100';
     weighting_range = '-100#100';
     text_range = '-1#1';
+
     // The SCORM 1.2 data model
-    var datamodel =  {
-        'cmi._children':{'defaultvalue':cmi_children, 'mod':'r', 'writeerror':'402'},
-        'cmi._version':{'defaultvalue':'3.4', 'mod':'r', 'writeerror':'402'},
-        'cmi.core._children':{'defaultvalue':core_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.core.student_id':{'defaultvalue':def['cmi.core.student_id'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.student_name':{'defaultvalue':def['cmi.core.student_name'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_location':{'defaultvalue':def['cmi.core.lesson_location'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.credit':{'defaultvalue':def['cmi.core.credit'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_status':{'defaultvalue':def['cmi.core.lesson_status'], 'format':CMIStatus, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.entry':{'defaultvalue':def['cmi.core.entry'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.score._children':{'defaultvalue':score_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.core.score.raw':{'defaultvalue':def['cmi.core.score.raw'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.score.max':{'defaultvalue':def['cmi.core.score.max'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.score.min':{'defaultvalue':def['cmi.core.score.min'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.core.total_time':{'defaultvalue':def['cmi.core.total_time'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.lesson_mode':{'defaultvalue':def['cmi.core.lesson_mode'], 'mod':'r', 'writeerror':'403'},
-        'cmi.core.exit':{'defaultvalue':def['cmi.core.exit'], 'format':CMIExit, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.core.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'00:00:00', 'readerror':'404', 'writeerror':'405'},
-        'cmi.suspend_data':{'defaultvalue':def['cmi.suspend_data'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
-        'cmi.launch_data':{'defaultvalue':def['cmi.launch_data'], 'mod':'r', 'writeerror':'403'},
-        'cmi.comments':{'defaultvalue':def['cmi.comments'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
-        // deprecated evaluation attributes
-        'cmi.evaluation.comments._count':{'defaultvalue':'0', 'mod':'r', 'writeerror':'402'},
-        'cmi.evaluation.comments._children':{'defaultvalue':comments_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.evaluation.comments.n.content':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.evaluation.comments.n.location':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.evaluation.comments.n.time':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
-        'cmi.comments_from_lms':{'mod':'r', 'writeerror':'403'},
-        'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score._children':{'pattern':CMIIndex, 'mod':'r', 'writeerror':'402'},
-        'cmi.objectives.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.objectives.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_data._children':{'defaultvalue':student_data_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.student_data.mastery_score':{'defaultvalue':def['cmi.student_data.mastery_score'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_data.max_time_allowed':{'defaultvalue':def['cmi.student_data.max_time_allowed'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_data.time_limit_action':{'defaultvalue':def['cmi.student_data.time_limit_action'], 'mod':'r', 'writeerror':'403'},
-        'cmi.student_preference._children':{'defaultvalue':student_preference_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.student_preference.audio':{'defaultvalue':'0', 'format':CMISInteger, 'range':audio_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.language':{'defaultvalue':'', 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.speed':{'defaultvalue':'0', 'format':CMISInteger, 'range':speed_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.student_preference.text':{'defaultvalue':'0', 'format':CMISInteger, 'range':text_range, 'mod':'rw', 'writeerror':'405'},
-        'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r', 'writeerror':'402'},
-        'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.correct_responses._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
-        'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'range':weighting_range, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.student_response':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
-        'nav.event':{'defaultvalue':'', 'format':NAVEvent, 'mod':'w', 'readerror':'404', 'writeerror':'405'}
-    };
-    //
-    // Datamodel inizialization
-    //
-    var cmi = new Object();
-        cmi.core = new Object();
-        cmi.core.score = new Object();
-        cmi.objectives = new Object();
-        cmi.student_data = new Object();
-        cmi.student_preference = new Object();
-        cmi.interactions = new Object();
-        // deprecated evaluation attributes
-        cmi.evaluation = new Object();
-        cmi.evaluation.comments = new Object();
-
-    // Navigation Object
-    var nav = new Object();
-
-    for (element in datamodel) {
-        if (element.match(/\.n\./) == null) {
-            if ((typeof eval('datamodel["'+element+'"].defaultvalue')) != 'undefined') {
-                eval(element+' = datamodel["'+element+'"].defaultvalue;');
-            } else {
-                eval(element+' = "";');
+    // Set up data model for each sco
+    var datamodel = {};
+    for(scoid in def){
+        datamodel[scoid] = {
+            'cmi._children':{'defaultvalue':cmi_children, 'mod':'r', 'writeerror':'402'},
+            'cmi._version':{'defaultvalue':'3.4', 'mod':'r', 'writeerror':'402'},
+            'cmi.core._children':{'defaultvalue':core_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.core.student_id':{'defaultvalue':def[scoid]['cmi.core.student_id'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.student_name':{'defaultvalue':def[scoid]['cmi.core.student_name'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_location':{'defaultvalue':def[scoid]['cmi.core.lesson_location'], 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.credit':{'defaultvalue':def[scoid]['cmi.core.credit'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_status':{'defaultvalue':def[scoid]['cmi.core.lesson_status'], 'format':CMIStatus, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.entry':{'defaultvalue':def[scoid]['cmi.core.entry'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.score._children':{'defaultvalue':score_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.core.score.raw':{'defaultvalue':def[scoid]['cmi.core.score.raw'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.score.max':{'defaultvalue':def[scoid]['cmi.core.score.max'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.score.min':{'defaultvalue':def[scoid]['cmi.core.score.min'], 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.core.total_time':{'defaultvalue':def[scoid]['cmi.core.total_time'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.lesson_mode':{'defaultvalue':def[scoid]['cmi.core.lesson_mode'], 'mod':'r', 'writeerror':'403'},
+            'cmi.core.exit':{'defaultvalue':def[scoid]['cmi.core.exit'], 'format':CMIExit, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.core.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'00:00:00', 'readerror':'404', 'writeerror':'405'},
+            'cmi.suspend_data':{'defaultvalue':def[scoid]['cmi.suspend_data'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
+            'cmi.launch_data':{'defaultvalue':def[scoid]['cmi.launch_data'], 'mod':'r', 'writeerror':'403'},
+            'cmi.comments':{'defaultvalue':def[scoid]['cmi.comments'], 'format':CMIString4096, 'mod':'rw', 'writeerror':'405'},
+            // deprecated evaluation attributes
+            'cmi.evaluation.comments._count':{'defaultvalue':'0', 'mod':'r', 'writeerror':'402'},
+            'cmi.evaluation.comments._children':{'defaultvalue':comments_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.evaluation.comments.n.content':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.evaluation.comments.n.location':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.evaluation.comments.n.time':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMITime, 'mod':'rw', 'writeerror':'405'},
+            'cmi.comments_from_lms':{'mod':'r', 'writeerror':'403'},
+            'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score._children':{'pattern':CMIIndex, 'mod':'r', 'writeerror':'402'},
+            'cmi.objectives.n.score.raw':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score.min':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.score.max':{'defaultvalue':'', 'pattern':CMIIndex, 'format':CMIDecimal, 'range':score_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.objectives.n.status':{'pattern':CMIIndex, 'format':CMIStatus2, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_data._children':{'defaultvalue':student_data_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.student_data.mastery_score':{'defaultvalue':def[scoid]['cmi.student_data.mastery_score'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_data.max_time_allowed':{'defaultvalue':def[scoid]['cmi.student_data.max_time_allowed'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_data.time_limit_action':{'defaultvalue':def[scoid]['cmi.student_data.time_limit_action'], 'mod':'r', 'writeerror':'403'},
+            'cmi.student_preference._children':{'defaultvalue':student_preference_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.student_preference.audio':{'defaultvalue':'0', 'format':CMISInteger, 'range':audio_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.language':{'defaultvalue':'', 'format':CMIString256, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.speed':{'defaultvalue':'0', 'format':CMISInteger, 'range':speed_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.student_preference.text':{'defaultvalue':'0', 'format':CMISInteger, 'range':text_range, 'mod':'rw', 'writeerror':'405'},
+            'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r', 'writeerror':'402'},
+            'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMIIdentifier, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.time':{'pattern':CMIIndex, 'format':CMITime, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.correct_responses._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0', 'writeerror':'402'},
+            'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'range':weighting_range, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.student_response':{'pattern':CMIIndex, 'format':CMIFeedback, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'w', 'readerror':'404', 'writeerror':'405'},
+            'nav.event':{'defaultvalue':'', 'format':NAVEvent, 'mod':'w', 'readerror':'404', 'writeerror':'405'}
+        };
+    }
+
+    var cmi, nav;
+    function initdatamodel(scoid){
+        prerequrl = cfgwwwroot + "/mod/scorm/prereqs.php?a="+scormid+"&scoid="+scoid+"&attempt="+attempt+"&mode="+viewmode+"&currentorg="+currentorg+"&sesskey="+sesskey;
+        datamodelurlparams = "id="+cmid+"&a="+scormid+"&sesskey="+sesskey+"&attempt="+attempt+"&scoid="+scoid;
+
+        //
+        // Datamodel inizialization
+        //
+        cmi = new Object();
+            cmi.core = new Object();
+            cmi.core.score = new Object();
+            cmi.objectives = new Object();
+            cmi.student_data = new Object();
+            cmi.student_preference = new Object();
+            cmi.interactions = new Object();
+            // deprecated evaluation attributes
+            cmi.evaluation = new Object();
+            cmi.evaluation.comments = new Object();
+
+        // Navigation Object
+        nav = new Object();
+
+        for (element in datamodel[scoid]) {
+            if (element.match(/\.n\./) == null) {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue')) != 'undefined') {
+                    eval(element+' = datamodel["'+scoid+'"]["'+element+'"].defaultvalue;');
+                } else {
+                    eval(element+' = "";');
+                }
             }
         }
-    }
 
-    eval(cmiobj);
-    eval(cmiint);
+        eval(cmiobj[scoid]);
+        eval(cmiint[scoid]);
 
-    if (cmi.core.lesson_status == '') {
-        cmi.core.lesson_status = 'not attempted';
+        if (cmi.core.lesson_status == '') {
+            cmi.core.lesson_status = 'not attempted';
+        }
     }
 
     //
@@ -157,6 +169,9 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
     var Initialized = false;
 
     function LMSInitialize (param) {
+        scoid = scorm_current_node ? scorm_current_node.scoid : scoid ;
+        initdatamodel(scoid);
+
         errorCode = "0";
         if (param == "") {
             if (!Initialized) {
@@ -229,8 +244,8 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
             if (element !="") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'w') {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'w') {
                         element = String(element).replace(expression, "_$1.");
                         elementIndexes = element.split('.');
                         subelement = 'cmi';
@@ -248,21 +263,21 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                             errorCode = "0"; // Need to check if it is the right errorCode
                         }
                     } else {
-                        errorCode = eval('datamodel["'+elementmodel+'"].readerror');
+                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].readerror');
                     }
                 } else {
                     childrenstr = '._children';
                     countstr = '._count';
                     if (elementmodel.substr(elementmodel.length-childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-childrenstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "202";
                         } else {
                             errorCode = "201";
                         }
                     } else if (elementmodel.substr(elementmodel.length-countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-countstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "203";
                         } else {
                             errorCode = "201";
@@ -289,9 +304,9 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
             if (element != "") {
                 expression = new RegExp(CMIIndex,'g');
                 elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'r') {
-                        expression = new RegExp(eval('datamodel["'+elementmodel+'"].format'));
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'r') {
+                        expression = new RegExp(eval('datamodel["'+scoid+'"]["'+elementmodel+'"].format'));
                         value = value+'';
                         matches = value.match(expression);
                         if (matches != null) {
@@ -338,8 +353,8 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                             }
                             //Store data
                             if (errorCode == "0") {
-                                if ((typeof eval('datamodel["'+elementmodel+'"].range')) != "undefined") {
-                                    range = eval('datamodel["'+elementmodel+'"].range');
+                                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range')) != "undefined") {
+                                    range = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range');
                                     ranges = range.split('#');
                                     value = value*1.0;
                                     if ((value >= ranges[0]) && (value <= ranges[1])) {
@@ -350,7 +365,7 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                                         }
                                         return "true";
                                     } else {
-                                        errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                                     }
                                 } else {
                                     if (element == 'cmi.comments') {
@@ -366,10 +381,10 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                                 }
                             }
                         } else {
-                            errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                            errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                         }
                     } else {
-                        errorCode = eval('datamodel["'+elementmodel+'"].writeerror');
+                        errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].writeerror');
                     }
                 } else {
                     errorCode = "201"
@@ -535,40 +550,40 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
 
                     // check if this specific element is not defined in the datamodel,
                     // but the generic element name is
-                    if ((eval('typeof datamodel["'+element+'"]')) == "undefined"
-                        && (eval('typeof datamodel["'+elementmodel+'"]')) != "undefined") {
+                    if ((eval('typeof datamodel["'+scoid+'"]["'+element+'"]')) == "undefined"
+                        && (eval('typeof datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
 
                         // add this specific element to the data model (by cloning
                         // the generic element) so we can track changes to it
-                        eval('datamodel["'+element+'"]=CloneObj(datamodel["'+elementmodel+'"]);');
+                        eval('datamodel["'+scoid+'"]["'+element+'"]=CloneObj(datamodel["'+scoid+'"]["'+elementmodel+'"]);');
                     }
 
                     // check if the current element exists in the datamodel
-                    if ((typeof eval('datamodel["'+element+'"]')) != "undefined") {
+                    if ((typeof eval('datamodel["'+scoid+'"]["'+element+'"]')) != "undefined") {
 
                         // make sure this is not a read only element
-                        if (eval('datamodel["'+element+'"].mod') != 'r') {
+                        if (eval('datamodel["'+scoid+'"]["'+element+'"].mod') != 'r') {
 
                             elementstring = '&'+underscore(element)+'='+encodeURIComponent(data[property]);
 
                             // check if the element has a default value
-                            if ((typeof eval('datamodel["'+element+'"].defaultvalue')) != "undefined") {
+                            if ((typeof eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue')) != "undefined") {
 
                                 // check if the default value is different from the current value
-                                if (eval('datamodel["'+element+'"].defaultvalue') != data[property]
-                                    || eval('typeof(datamodel["'+element+'"].defaultvalue)') != typeof(data[property])) {
+                                if (eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue') != data[property]
+                                    || eval('typeof(datamodel["'+scoid+'"]["'+element+'"].defaultvalue)') != typeof(data[property])) {
 
                                     // append the URI fragment to the string we plan to commit
                                     datastring += elementstring;
 
                                     // update the element default to reflect the current committed value
-                                    eval('datamodel["'+element+'"].defaultvalue=data[property];');
+                                    eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue=data[property];');
                                 }
                             } else {
                                 // append the URI fragment to the string we plan to commit
                                 datastring += elementstring;
                                 // no default value for the element, so set it now
-                                eval('datamodel["'+element+'"].defaultvalue=data[property];');
+                                eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue=data[property];');
                             }
                         }
                     }
@@ -608,7 +623,7 @@ function SCORMapi1_2(def, cmiobj, cmiint, cmistring256, cmistring4096, scormdebu
                 }
             }
             if (cmi.core.lesson_mode == 'browse') {
-                if (datamodel['cmi.core.lesson_status'].defaultvalue == '' && cmi.core.lesson_status == 'not attempted') {
+                if (datamodel[scoid]['cmi.core.lesson_status'].defaultvalue == '' && cmi.core.lesson_status == 'not attempted') {
                     cmi.core.lesson_status = 'browsed';
                 }
             }
index 187fd62..0d129cc 100644 (file)
@@ -18,12 +18,30 @@ require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 
 // Set some vars to use as default values.
 $userdata = new stdClass();
-$def = get_scorm_default($userdata, $scorm, $scoid, $attempt, $mode);
+$def = new stdClass();
+$cmiobj = new stdClass();
+$cmiint = new stdClass();
 
 if (!isset($currentorg)) {
     $currentorg = '';
 }
 
+if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
+    // Drop keys so that it is a simple array.
+    $scoes = array_values($scoes);
+    foreach ($scoes as $sco) {
+        $def->{($sco->id)} = new stdClass();
+        $userdata->{($sco->id)} = new stdClass();
+        $def->{($sco->id)} = get_scorm_default($userdata->{($sco->id)}, $scorm, $sco->id, $attempt, $mode);
+
+        // Reconstitute objectives.
+        $cmiobj->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.objectives', array('score'));
+        $cmiint->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.interactions', array('objectives', 'correct_responses'));
+    }
+}
+
 // If SCORM 1.2 standard mode is disabled allow higher datamodel limits.
 if (intval(get_config("scorm", "scorm12standard"))) {
     $cmistring256 = '^[\\u0000-\\uFFFF]{0,255}$';
@@ -33,10 +51,6 @@ if (intval(get_config("scorm", "scorm12standard"))) {
     $cmistring4096 = $cmistring256;
 }
 
-// reconstitute objectives
-$cmiobj = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.objectives', array('score'));
-$cmiint = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.interactions', array('objectives', 'correct_responses'));
-
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $cmiint, $cmistring256, $cmistring4096,
                                                         scorm_debugging($scorm), $scorm->auto, $scorm->id, $CFG->wwwroot,
                                                         sesskey(), $scoid, $attempt, $mode, $id, $currentorg));
index 9756206..4dfd1cb 100644 (file)
@@ -150,112 +150,123 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
     }
 
     // The SCORM 1.3 data model
-    var datamodel =  {
-        'cmi._children':{'defaultvalue':cmi_children, 'mod':'r'},
-        'cmi._version':{'defaultvalue':'1.0', 'mod':'r'},
-        'cmi.comments_from_learner._children':{'defaultvalue':comments_children, 'mod':'r'},
-        'cmi.comments_from_learner._count':{'mod':'r', 'defaultvalue':'0'},
-        'cmi.comments_from_learner.n.comment':{'format':CMILangString4000, 'mod':'rw'},
-        'cmi.comments_from_learner.n.location':{'format':CMIString250, 'mod':'rw'},
-        'cmi.comments_from_learner.n.timestamp':{'format':CMITime, 'mod':'rw'},
-        'cmi.comments_from_lms._children':{'defaultvalue':comments_children, 'mod':'r'},
-        'cmi.comments_from_lms._count':{'mod':'r', 'defaultvalue':'0'},
-        'cmi.comments_from_lms.n.comment':{'format':CMILangString4000, 'mod':'r'},
-        'cmi.comments_from_lms.n.location':{'format':CMIString250, 'mod':'r'},
-        'cmi.comments_from_lms.n.timestamp':{'format':CMITime, 'mod':'r'},
-        'cmi.completion_status':{'defaultvalue':def['cmi.completion_status'], 'format':CMICStatus, 'mod':'rw'},
-        'cmi.completion_threshold':{'defaultvalue':def['cmi.completion_threshold'], 'mod':'r'},
-        'cmi.credit':{'defaultvalue':def['cmi.credit'], 'mod':'r'},
-        'cmi.entry':{'defaultvalue':def['cmi.entry'], 'mod':'r'},
-        'cmi.exit':{'defaultvalue':def['cmi.exit'], 'format':CMIExit, 'mod':'w'},
-        'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r'},
-        'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0'},
-        'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
-        'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'rw'},
-        'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0'},
-        'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
-        'cmi.interactions.n.timestamp':{'pattern':CMIIndex, 'format':CMITime, 'mod':'rw'},
-        'cmi.interactions.n.correct_responses._count':{'defaultvalue':'0', 'pattern':CMIIndex, 'mod':'r'},
-        'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':'CMIFeedback', 'mod':'rw'},
-        'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.interactions.n.learner_response':{'pattern':CMIIndex, 'format':'CMIFeedback', 'mod':'rw'},
-        'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'rw'},
-        'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'rw'},
-        'cmi.interactions.n.description':{'pattern':CMIIndex, 'format':CMILangString250, 'mod':'rw'},
-        'cmi.launch_data':{'defaultvalue':def['cmi.exit'], 'mod':'r'},
-        'cmi.learner_id':{'defaultvalue':def['cmi.learner_id'], 'mod':'r'},
-        'cmi.learner_name':{'defaultvalue':def['cmi.learner_name'], 'mod':'r'},
-        'cmi.learner_preference._children':{'defaultvalue':student_preference_children, 'mod':'r'},
-        'cmi.learner_preference.audio_level':{'defaultvalue':def['cmi.learner_preference.audio_level'], 'format':CMIDecimal, 'range':audio_range, 'mod':'rw'},
-        'cmi.learner_preference.language':{'defaultvalue':def['cmi.learner_preference.language'], 'format':CMILang, 'mod':'rw'},
-        'cmi.learner_preference.delivery_speed':{'defaultvalue':def['cmi.learner_preference.delivery_speed'], 'format':CMIDecimal, 'range':speed_range, 'mod':'rw'},
-        'cmi.learner_preference.audio_captioning':{'defaultvalue':def['cmi.learner_preference.audio_captioning'], 'format':CMISInteger, 'range':text_range, 'mod':'rw'},
-        'cmi.location':{'defaultvalue':def['cmi.location'], 'format':CMIString1000, 'mod':'rw'},
-        'cmi.max_time_allowed':{'defaultvalue':def['cmi.max_time_allowed'], 'mod':'r'},
-        'cmi.mode':{'defaultvalue':def['cmi.mode'], 'mod':'r'},
-        'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r'},
-        'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0'},
-        'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
-        'cmi.objectives.n.score._children':{'defaultvalue':score_children, 'pattern':CMIIndex, 'mod':'r'},
-        'cmi.objectives.n.score.scaled':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'range':scaled_range, 'mod':'rw'},
-        'cmi.objectives.n.score.raw':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.objectives.n.score.min':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.objectives.n.score.max':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.objectives.n.success_status':{'defaultvalue':'unknown', 'pattern':CMIIndex, 'format':CMISStatus, 'mod':'rw'},
-        'cmi.objectives.n.completion_status':{'defaultvalue':'unknown', 'pattern':CMIIndex, 'format':CMICStatus, 'mod':'rw'},
-        'cmi.objectives.n.progress_measure':{'defaultvalue':null, 'format':CMIDecimal, 'range':progress_range, 'mod':'rw'},
-        'cmi.objectives.n.description':{'pattern':CMIIndex, 'format':CMILangString250, 'mod':'rw'},
-        'cmi.progress_measure':{'defaultvalue':def['cmi.progress_measure'], 'format':CMIDecimal, 'range':progress_range, 'mod':'rw'},
-        'cmi.scaled_passing_score':{'defaultvalue':def['cmi.scaled_passing_score'], 'format':CMIDecimal, 'range':scaled_range, 'mod':'r'},
-        'cmi.score._children':{'defaultvalue':score_children, 'mod':'r'},
-        'cmi.score.scaled':{'defaultvalue':def['cmi.score.scaled'], 'format':CMIDecimal, 'range':scaled_range, 'mod':'rw'},
-        'cmi.score.raw':{'defaultvalue':def['cmi.score.raw'], 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.score.min':{'defaultvalue':def['cmi.score.min'], 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.score.max':{'defaultvalue':def['cmi.score.max'], 'format':CMIDecimal, 'mod':'rw'},
-        'cmi.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'PT0H0M0S'},
-        'cmi.success_status':{'defaultvalue':def['cmi.success_status'], 'format':CMISStatus, 'mod':'rw'},
-        'cmi.suspend_data':{'defaultvalue':def['cmi.suspend_data'], 'format':CMIString64000, 'mod':'rw'},
-        'cmi.time_limit_action':{'defaultvalue':def['cmi.time_limit_action'], 'mod':'r'},
-        'cmi.total_time':{'defaultvalue':def['cmi.total_time'], 'mod':'r'},
-        'adl.nav.request':{'defaultvalue':'_none_', 'format':NAVEvent, 'mod':'rw'}
-    };
-    //
-    // Datamodel inizialization
-    //
-        var cmi = new Object();
-        cmi.comments_from_learner = new Object();
-        cmi.comments_from_learner._count = 0;
-        cmi.comments_from_lms = new Object();
-        cmi.comments_from_lms._count = 0;
-        cmi.interactions = new Object();
-        cmi.interactions._count = 0;
-        cmi.learner_preference = new Object();
-        cmi.objectives = new Object();
-        cmi.objectives._count = 0;
-        cmi.score = new Object();
+    // Set up data model for each sco
+    var datamodel = {};
+    for(scoid in def){
+        datamodel[scoid] = {
+            'cmi._children':{'defaultvalue':cmi_children, 'mod':'r'},
+            'cmi._version':{'defaultvalue':'1.0', 'mod':'r'},
+            'cmi.comments_from_learner._children':{'defaultvalue':comments_children, 'mod':'r'},
+            'cmi.comments_from_learner._count':{'mod':'r', 'defaultvalue':'0'},
+            'cmi.comments_from_learner.n.comment':{'format':CMILangString4000, 'mod':'rw'},
+            'cmi.comments_from_learner.n.location':{'format':CMIString250, 'mod':'rw'},
+            'cmi.comments_from_learner.n.timestamp':{'format':CMITime, 'mod':'rw'},
+            'cmi.comments_from_lms._children':{'defaultvalue':comments_children, 'mod':'r'},
+            'cmi.comments_from_lms._count':{'mod':'r', 'defaultvalue':'0'},
+            'cmi.comments_from_lms.n.comment':{'format':CMILangString4000, 'mod':'r'},
+            'cmi.comments_from_lms.n.location':{'format':CMIString250, 'mod':'r'},
+            'cmi.comments_from_lms.n.timestamp':{'format':CMITime, 'mod':'r'},
+            'cmi.completion_status':{'defaultvalue':def[scoid]['cmi.completion_status'], 'format':CMICStatus, 'mod':'rw'},
+            'cmi.completion_threshold':{'defaultvalue':def[scoid]['cmi.completion_threshold'], 'mod':'r'},
+            'cmi.credit':{'defaultvalue':def[scoid]['cmi.credit'], 'mod':'r'},
+            'cmi.entry':{'defaultvalue':def[scoid]['cmi.entry'], 'mod':'r'},
+            'cmi.exit':{'defaultvalue':def[scoid]['cmi.exit'], 'format':CMIExit, 'mod':'w'},
+            'cmi.interactions._children':{'defaultvalue':interactions_children, 'mod':'r'},
+            'cmi.interactions._count':{'mod':'r', 'defaultvalue':'0'},
+            'cmi.interactions.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
+            'cmi.interactions.n.type':{'pattern':CMIIndex, 'format':CMIType, 'mod':'rw'},
+            'cmi.interactions.n.objectives._count':{'pattern':CMIIndex, 'mod':'r', 'defaultvalue':'0'},
+            'cmi.interactions.n.objectives.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
+            'cmi.interactions.n.timestamp':{'pattern':CMIIndex, 'format':CMITime, 'mod':'rw'},
+            'cmi.interactions.n.correct_responses._count':{'defaultvalue':'0', 'pattern':CMIIndex, 'mod':'r'},
+            'cmi.interactions.n.correct_responses.n.pattern':{'pattern':CMIIndex, 'format':'CMIFeedback', 'mod':'rw'},
+            'cmi.interactions.n.weighting':{'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.interactions.n.learner_response':{'pattern':CMIIndex, 'format':'CMIFeedback', 'mod':'rw'},
+            'cmi.interactions.n.result':{'pattern':CMIIndex, 'format':CMIResult, 'mod':'rw'},
+            'cmi.interactions.n.latency':{'pattern':CMIIndex, 'format':CMITimespan, 'mod':'rw'},
+            'cmi.interactions.n.description':{'pattern':CMIIndex, 'format':CMILangString250, 'mod':'rw'},
+            'cmi.launch_data':{'defaultvalue':def[scoid]['cmi.exit'], 'mod':'r'},
+            'cmi.learner_id':{'defaultvalue':def[scoid]['cmi.learner_id'], 'mod':'r'},
+            'cmi.learner_name':{'defaultvalue':def[scoid]['cmi.learner_name'], 'mod':'r'},
+            'cmi.learner_preference._children':{'defaultvalue':student_preference_children, 'mod':'r'},
+            'cmi.learner_preference.audio_level':{'defaultvalue':def[scoid]['cmi.learner_preference.audio_level'], 'format':CMIDecimal, 'range':audio_range, 'mod':'rw'},
+            'cmi.learner_preference.language':{'defaultvalue':def[scoid]['cmi.learner_preference.language'], 'format':CMILang, 'mod':'rw'},
+            'cmi.learner_preference.delivery_speed':{'defaultvalue':def[scoid]['cmi.learner_preference.delivery_speed'], 'format':CMIDecimal, 'range':speed_range, 'mod':'rw'},
+            'cmi.learner_preference.audio_captioning':{'defaultvalue':def[scoid]['cmi.learner_preference.audio_captioning'], 'format':CMISInteger, 'range':text_range, 'mod':'rw'},
+            'cmi.location':{'defaultvalue':def[scoid]['cmi.location'], 'format':CMIString1000, 'mod':'rw'},
+            'cmi.max_time_allowed':{'defaultvalue':def[scoid]['cmi.max_time_allowed'], 'mod':'r'},
+            'cmi.mode':{'defaultvalue':def[scoid]['cmi.mode'], 'mod':'r'},
+            'cmi.objectives._children':{'defaultvalue':objectives_children, 'mod':'r'},
+            'cmi.objectives._count':{'mod':'r', 'defaultvalue':'0'},
+            'cmi.objectives.n.id':{'pattern':CMIIndex, 'format':CMILongIdentifier, 'mod':'rw'},
+            'cmi.objectives.n.score._children':{'defaultvalue':score_children, 'pattern':CMIIndex, 'mod':'r'},
+            'cmi.objectives.n.score.scaled':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'range':scaled_range, 'mod':'rw'},
+            'cmi.objectives.n.score.raw':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.objectives.n.score.min':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.objectives.n.score.max':{'defaultvalue':null, 'pattern':CMIIndex, 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.objectives.n.success_status':{'defaultvalue':'unknown', 'pattern':CMIIndex, 'format':CMISStatus, 'mod':'rw'},
+            'cmi.objectives.n.completion_status':{'defaultvalue':'unknown', 'pattern':CMIIndex, 'format':CMICStatus, 'mod':'rw'},
+            'cmi.objectives.n.progress_measure':{'defaultvalue':null, 'format':CMIDecimal, 'range':progress_range, 'mod':'rw'},
+            'cmi.objectives.n.description':{'pattern':CMIIndex, 'format':CMILangString250, 'mod':'rw'},
+            'cmi.progress_measure':{'defaultvalue':def[scoid]['cmi.progress_measure'], 'format':CMIDecimal, 'range':progress_range, 'mod':'rw'},
+            'cmi.scaled_passing_score':{'defaultvalue':def[scoid]['cmi.scaled_passing_score'], 'format':CMIDecimal, 'range':scaled_range, 'mod':'r'},
+            'cmi.score._children':{'defaultvalue':score_children, 'mod':'r'},
+            'cmi.score.scaled':{'defaultvalue':def[scoid]['cmi.score.scaled'], 'format':CMIDecimal, 'range':scaled_range, 'mod':'rw'},
+            'cmi.score.raw':{'defaultvalue':def[scoid]['cmi.score.raw'], 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.score.min':{'defaultvalue':def[scoid]['cmi.score.min'], 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.score.max':{'defaultvalue':def[scoid]['cmi.score.max'], 'format':CMIDecimal, 'mod':'rw'},
+            'cmi.session_time':{'format':CMITimespan, 'mod':'w', 'defaultvalue':'PT0H0M0S'},
+            'cmi.success_status':{'defaultvalue':def[scoid]['cmi.success_status'], 'format':CMISStatus, 'mod':'rw'},
+            'cmi.suspend_data':{'defaultvalue':def[scoid]['cmi.suspend_data'], 'format':CMIString64000, 'mod':'rw'},
+            'cmi.time_limit_action':{'defaultvalue':def[scoid]['cmi.time_limit_action'], 'mod':'r'},
+            'cmi.total_time':{'defaultvalue':def[scoid]['cmi.total_time'], 'mod':'r'},
+            'adl.nav.request':{'defaultvalue':'_none_', 'format':NAVEvent, 'mod':'rw'}
+        };
+    }
 
-    // Navigation Object
-    var adl = new Object();
-        adl.nav = new Object();
-        adl.nav.request_valid = new Array();
+    var cmi, adl;
+    function initdatamodel(scoid){
+        prerequrl = cfgwwwroot + "/mod/scorm/prereqs.php?a="+scormid+"&scoid="+scoid+"&attempt="+attempt+"&mode="+viewmode+"&currentorg="+currentorg+"&sesskey="+sesskey;
+        datamodelurlparams = "id="+cmid+"&a="+scormid+"&sesskey="+sesskey+"&attempt="+attempt+"&scoid="+scoid;
 
-    for (element in datamodel) {
-        if (element.match(/\.n\./) == null) {
-            if ((typeof eval('datamodel["'+element+'"].defaultvalue')) != 'undefined') {
-                eval(element+' = datamodel["'+element+'"].defaultvalue;');
-            } else {
-                eval(element+' = "";');
+        //
+        // Datamodel inizialization
+        //
+        cmi = new Object();
+            cmi.comments_from_learner = new Object();
+            cmi.comments_from_learner._count = 0;
+            cmi.comments_from_lms = new Object();
+            cmi.comments_from_lms._count = 0;
+            cmi.interactions = new Object();
+            cmi.interactions._count = 0;
+            cmi.learner_preference = new Object();
+            cmi.objectives = new Object();
+            cmi.objectives._count = 0;
+            cmi.score = new Object();
+
+        // Navigation Object
+        adl = new Object();
+            adl.nav = new Object();
+            adl.nav.request_valid = new Array();
+
+        for (element in datamodel[scoid]) {
+            if (element.match(/\.n\./) == null) {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+element+'"].defaultvalue')) != 'undefined') {
+                    eval(element+' = datamodel["'+scoid+'"]["'+element+'"].defaultvalue;');
+                } else {
+                    eval(element+' = "";');
+                }
             }
         }
-    }
 
-    eval(cmiobj);
-    eval(cmiint);
-    eval(cmicommentsuser);
-    eval(cmicommentslms);
+        eval(cmiobj[scoid]);
+        eval(cmiint[scoid]);
+        eval(cmicommentsuser[scoid]);
+        eval(cmicommentslms[scoid]);
 
-    if (cmi.completion_status == '') {
-        cmi.completion_status = 'not attempted';
+        if (cmi.completion_status == '') {
+            cmi.completion_status = 'not attempted';
+        }
     }
 
     //
@@ -267,6 +278,9 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
     var errorCode = "0";
 
     function Initialize (param) {
+        scoid = scorm_current_node ? scorm_current_node.scoid : scoid ;
+        initdatamodel(scoid);
+
         errorCode = "0";
         if (param == "") {
             if ((!Initialized) && (!Terminated)) {
@@ -365,8 +379,8 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
             if (element !="") {
                 var expression = new RegExp(CMIIndex,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'w') {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'w') {
 
                         element = String(element).replace(/\.(\d+)\./, ".N$1.");
                         element = element.replace(/\.(\d+)\./, ".N$1.");
@@ -393,7 +407,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                             errorCode = "301";
                         }
                     } else {
-                        //errorCode = eval('datamodel["'+elementmodel+'"].readerror');
+                        //errorCode = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].readerror');
                         errorCode = "405";
                     }
                 } else {
@@ -402,7 +416,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                     var parentmodel = '';
                     if (elementmodel.substr(elementmodel.length-childrenstr.length,elementmodel.length) == childrenstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-childrenstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "301";
                             diagnostic = "Data Model Element Does Not Have Children";
                         } else {
@@ -410,7 +424,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                         }
                     } else if (elementmodel.substr(elementmodel.length-countstr.length,elementmodel.length) == countstr) {
                         parentmodel = elementmodel.substr(0,elementmodel.length-countstr.length);
-                        if ((typeof eval('datamodel["'+parentmodel+'"]')) != "undefined") {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+parentmodel+'"]')) != "undefined") {
                             errorCode = "301";
                             diagnostic = "Data Model Element Cannot Have Count";
                         } else {
@@ -458,10 +472,10 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
             if (element != "") {
                 var expression = new RegExp(CMIIndex,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'r') {
-                        if (eval('datamodel["'+elementmodel+'"].format') != 'CMIFeedback') {
-                            expression = new RegExp(eval('datamodel["'+elementmodel+'"].format'));
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'r') {
+                        if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].format') != 'CMIFeedback') {
+                            expression = new RegExp(eval('datamodel["'+scoid+'"]["'+elementmodel+'"].format'));
                         } else {
                             // cmi.interactions.n.type depending format accept everything at this stage
                             expression = new RegExp(CMIFeedback);
@@ -517,15 +531,15 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                                                         eval(parentelement+'._count++;');
                                                         eval(subelement+' = new Object();');
                                                         var subobject = eval(subelement);
-                                                        subobject.success_status = datamodel["cmi.objectives.n.success_status"].defaultvalue;
-                                                        subobject.completion_status = datamodel["cmi.objectives.n.completion_status"].defaultvalue;
-                                                        subobject.progress_measure = datamodel["cmi.objectives.n.progress_measure"].defaultvalue;
+                                                        subobject.success_status = datamodel[scoid]["cmi.objectives.n.success_status"].defaultvalue;
+                                                        subobject.completion_status = datamodel[scoid]["cmi.objectives.n.completion_status"].defaultvalue;
+                                                        subobject.progress_measure = datamodel[scoid]["cmi.objectives.n.progress_measure"].defaultvalue;
                                                         subobject.score = new Object();
                                                         subobject.score._children = score_children;
-                                                        subobject.score.scaled = datamodel["cmi.objectives.n.score.scaled"].defaultvalue;
-                                                        subobject.score.raw = datamodel["cmi.objectives.n.score.raw"].defaultvalue;
-                                                        subobject.score.min = datamodel["cmi.objectives.n.score.min"].defaultvalue;
-                                                        subobject.score.max = datamodel["cmi.objectives.n.score.max"].defaultvalue;
+                                                        subobject.score.scaled = datamodel[scoid]["cmi.objectives.n.score.scaled"].defaultvalue;
+                                                        subobject.score.raw = datamodel[scoid]["cmi.objectives.n.score.raw"].defaultvalue;
+                                                        subobject.score.min = datamodel[scoid]["cmi.objectives.n.score.min"].defaultvalue;
+                                                        subobject.score.max = datamodel[scoid]["cmi.objectives.n.score.max"].defaultvalue;
                                                     }
                                                 } else {
                                                     errorCode="351";
@@ -731,8 +745,8 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                             //Store data
                             if (errorCode == "0") {
 
-                                if ((typeof eval('datamodel["'+elementmodel+'"].range')) != "undefined") {
-                                    range = eval('datamodel["'+elementmodel+'"].range');
+                                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range')) != "undefined") {
+                                    range = eval('datamodel["'+scoid+'"]["'+elementmodel+'"].range');
                                     ranges = range.split('#');
                                     value = value*1.0;
                                     if (value >= ranges[0]) {
@@ -1068,12 +1082,12 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
     }
 
     function getElementModel(element) {
-        if (typeof datamodel[element] != "undefined") {
+        if (typeof datamodel[scoid][element] != "undefined") {
             return element;
         } else {
             var expression = new RegExp(CMIIndex,'g');
             var elementmodel = String(element).replace(expression,'.n.');
-            if (typeof datamodel[elementmodel] != "undefined") {
+            if (typeof datamodel[scoid][elementmodel] != "undefined") {
                 return elementmodel;
             }
         }
@@ -1161,11 +1175,11 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
                 var element = parent+'.'+property;
                 var expression = new RegExp(CMIIndexStore,'g');
                 var elementmodel = String(element).replace(expression,'.n.');
-                if ((typeof eval('datamodel["'+elementmodel+'"]')) != "undefined") {
-                    if (eval('datamodel["'+elementmodel+'"].mod') != 'r') {
+                if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"]')) != "undefined") {
+                    if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].mod') != 'r') {
                         var elementstring = '&'+underscore(element)+'='+encodeURIComponent(data[property]);
-                        if ((typeof eval('datamodel["'+elementmodel+'"].defaultvalue')) != "undefined") {
-                            if (eval('datamodel["'+elementmodel+'"].defaultvalue') != data[property] || eval('typeof(datamodel["'+elementmodel+'"].defaultvalue)') != typeof(data[property])) {
+                        if ((typeof eval('datamodel["'+scoid+'"]["'+elementmodel+'"].defaultvalue')) != "undefined") {
+                            if (eval('datamodel["'+scoid+'"]["'+elementmodel+'"].defaultvalue') != data[property] || eval('typeof(datamodel["'+scoid+'"]["'+elementmodel+'"].defaultvalue)') != typeof(data[property])) {
                                 datastring += elementstring;
                             }
                         } else {
@@ -1203,7 +1217,7 @@ function SCORMapi1_3(def, cmiobj, cmiint, cmicommentsuser, cmicommentslms, scorm
         }
         datastring += CollectData(data,'cmi');
         var element = 'adl.nav.request';
-        var navrequest = eval(element) != datamodel[element].defaultvalue ? '&'+underscore(element)+'='+encodeURIComponent(eval(element)) : '';
+        var navrequest = eval(element) != datamodel[scoid][element].defaultvalue ? '&'+underscore(element)+'='+encodeURIComponent(eval(element)) : '';
         datastring += navrequest;
 
         var myRequest = NewHttpReq();
index e8a4f6d..8b534dc 100644 (file)
 require_once($CFG->dirroot.'/mod/scorm/locallib.php');
 
 $userdata = new stdClass();
-$def = get_scorm_default($userdata, $scorm, $scoid, $attempt, $mode);
+$def = new stdClass();
+$cmiobj = new stdClass();
+$cmiint = new stdClass();
+$cmicommentsuser = new stdClass();
+$cmicommentslms = new stdClass();
 
 if (!isset($currentorg)) {
     $currentorg = '';
 }
 
-// reconstitute objectives, comments_from_learner and comments_from_lms
-$cmiobj = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.objectives', array('score'));
-$cmiint = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.interactions', array('objectives', 'correct_responses'));
-$cmicommentsuser = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.comments_from_learner', array());
-$cmicommentslms = scorm_reconstitute_array_element($scorm->version, $userdata, 'cmi.comments_from_lms', array());
+if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) {
+    // Drop keys so that it is a simple array.
+    $scoes = array_values($scoes);
+    foreach ($scoes as $sco) {
+        $def->{($sco->id)} = new stdClass();
+        $userdata->{($sco->id)} = new stdClass();
+        $def->{($sco->id)} = get_scorm_default($userdata->{($sco->id)}, $scorm, $sco->id, $attempt, $mode);
+
+        // Reconstitute objectives, comments_from_learner and comments_from_lms.
+        $cmiobj->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.objectives', array('score'));
+        $cmiint->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.interactions', array('objectives', 'correct_responses'));
+        $cmicommentsuser->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.comments_from_learner', array());
+        $cmicommentslms->{($sco->id)} = scorm_reconstitute_array_element($scorm->version, $userdata->{($sco->id)},
+                                                                    'cmi.comments_from_lms', array());
+    }
+}
+
 
 $PAGE->requires->js_init_call('M.scorm_api.init', array($def, $cmiobj, $cmiint, $cmicommentsuser, $cmicommentslms,
                                                         scorm_debugging($scorm), $scorm->auto, $scorm->id, $CFG->wwwroot,
                                                         sesskey(), $scoid, $attempt, $mode, $id, $currentorg));
 
 
-// pull in the debugging utilities
+// Pull in the debugging utilities.
 if (scorm_debugging($scorm)) {
     require_once($CFG->dirroot.'/mod/scorm/datamodels/debug.js.php');
     echo html_writer::script('AppendToLog("Moodle SCORM 1.3 API Loaded, Activity: '.$scorm->name.', SCO: '.$sco->identifier.'", 0);');
index 14a5c01..ca8d359 100644 (file)
@@ -909,7 +909,7 @@ function scorm_view_display ($user, $scorm, $action, $cm) {
             }
         } else if (!empty($attemptcount) && ($incomplete === false) && (($result->attemptleft > 0)||($scorm->maxattempt == 0))) {
                 echo html_writer::empty_tag('br');
-                echo html_writer::checkbox('newattempt', '', false, '', array('id' => 'a'));
+                echo html_writer::checkbox('newattempt', 'on', false, '', array('id' => 'a'));
                 echo html_writer::label(get_string('newattempt', 'scorm'), 'a');
         }
         if (!empty($scorm->popup)) {
index 8f45f42..c7099fc 100644 (file)
@@ -27,6 +27,8 @@ mod_scorm_activate_item = null;
 mod_scorm_parse_toc_tree = null;
 scorm_layout_widget = null;
 
+window.scorm_current_node = null;
+
 function underscore(str) {
     str = String(str).replace(/.N/g,".");
     return str.replace(/\./g,"__");
@@ -48,7 +50,6 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
     }
 
     scoes_nav = Y.JSON.parse(scoes_nav);
-    var scorm_current_node;
     var scorm_buttons = [];
     var scorm_bloody_labelclick = false;
     var scorm_nav_panel;
@@ -124,15 +125,23 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
                 return;
             }
             // Check if the item is already active, avoid recursive calls.
-            if (Y.one('#scorm_object')) {
+            var content = Y.one('#scorm_content');
+            var old = Y.one('#scorm_object');
+            if (old) {
                 var scorm_active_url = Y.one('#scorm_object').getAttribute('src');
                 var node_full_url = M.cfg.wwwroot + '/mod/scorm/loadSCO.php?' + node.title;
                 if (node_full_url === scorm_active_url) {
                     return;
                 }
+                // Start to unload iframe here
+                if(!window_name){
+                    content.removeChild(old);
+                    old = null;
+                }
             }
+            // End of - Avoid recursive calls.
+
             scorm_current_node = node;
-            // Avoid recursive calls.
             if (!scorm_current_node.state.selected) {
                 scorm_current_node.select();
             }
@@ -144,7 +153,6 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
                 el_old_api.parentNode.removeChild(el_old_api);
             }
 
-            var content = Y.one('#scorm_content');
             var obj = document.createElement('iframe');
             obj.setAttribute('id', 'scorm_object');
             obj.setAttribute('type', 'text/html');
@@ -159,7 +167,6 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
                 mine.close();
             }
 
-            var old = Y.one('#scorm_object');
             if (old) {
                 if(window_name) {
                     var cwidth = scormplayerdata.cwidth;
@@ -167,8 +174,6 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
                     var poptions = scormplayerdata.popupoptions;
                     poptions = poptions + ',resizable=yes'; // Added for IE (MDL-32506).
                     scorm_openpopup(M.cfg.wwwroot + "/mod/scorm/loadSCO.php?" + node.title, window_name, poptions, cwidth, cheight);
-                } else {
-                    content.replaceChild(obj, old);
                 }
             } else {
                 content.prepend(obj);
@@ -577,14 +582,18 @@ M.mod_scorm.init = function(Y, nav_display, navposition_left, navposition_top, h
             if (node.title == '' || node.title == null) {
                 return; //this item has no navigation
             }
+
             // If item is already active, return; avoid recursive calls.
-            if (Y.one('#scorm_data')) {
-                var scorm_active_url = Y.one('#scorm_object').getAttribute('src');
+            if (obj = Y.one('#scorm_object')) {
+                var scorm_active_url = obj.getAttribute('src');
                 var node_full_url = M.cfg.wwwroot + '/mod/scorm/loadSCO.php?' + node.title;
                 if (node_full_url === scorm_active_url) {
                     return;
                 }
+            } else if(scorm_current_node == node){
+                return;
             }
+
             // Update launch_sco.
             if (typeof node.scoid !== 'undefined') {
                 launch_sco = node.scoid;
index cdd6f89..65b2e1f 100644 (file)
@@ -27,8 +27,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
-/** NO_DEBUG_DISPLAY - bool, Disable moodle specific debug messages and any errors in output. Set to false to see any error messages during RSS generation */
+/** NO_DEBUG_DISPLAY - bool, Disable moodle debug and error messages. Set to false to see any errors during RSS generation */
 define('NO_DEBUG_DISPLAY', true);
 
 /** NO_MOODLE_COOKIES - bool, Disable the use of sessions/cookies - we recreate $USER for every call. */
@@ -38,21 +37,19 @@ require_once('../config.php');
 require_once($CFG->libdir.'/filelib.php');
 require_once($CFG->libdir.'/rsslib.php');
 
-// RSS feeds must be enabled site-wide
+// RSS feeds must be enabled site-wide.
 if (empty($CFG->enablerssfeeds)) {
     debugging('DISABLED (admin variables)');
     rss_error();
 }
 
-
-// All the arguments are in the path
+// All the arguments are in the path.
 $relativepath = get_file_argument();
 if (!$relativepath) {
     rss_error();
 }
 
-
-// Extract relative path components into variables
+// Extract relative path components into variables.
 $args = explode('/', trim($relativepath, '/'));
 if (count($args) < 5) {
     rss_error();
@@ -62,25 +59,25 @@ $contextid   = (int)$args[0];
 $token  = clean_param($args[1], PARAM_ALPHANUM);
 $componentname = clean_param($args[2], PARAM_FILE);
 
-//check if they have requested a 1.9 RSS feed
-//if token is an int its a user id (1.9 request)
-//if token contains any letters its a token (2.0 request)
+// Check if they have requested a 1.9 RSS feed.
+// If token is an int it is a user id (1.9 request).
+// If token contains any letters it is a token (2.0 request).
 $inttoken = intval($token);
-if ($token==="$inttoken") {
-    //they've requested a feed using a 1.9 url. redirect them to the 2.0 url using the guest account
+if ($token === "$inttoken") {
+    // They have requested a feed using a 1.9 url. redirect them to the 2.0 url using the guest account.
 
     $instanceid  = clean_param($args[3], PARAM_INT);
 
-    //1.9 URL puts course id where the context id is in 2.0 URLs
+    // 1.9 URL puts course id where the context id is in 2.0 URLs.
     $courseid = $contextid;
     unset($contextid);
 
-    //find the context id
+    // Find the context id.
     if ($course = $DB->get_record('course', array('id' => $courseid))) {
         $modinfo = get_fast_modinfo($course);
 
-        foreach ($modinfo->get_instances_of($componentname) as $modinstanceid=>$cm) {
-            if ($modinstanceid==$instanceid) {
+        foreach ($modinfo->get_instances_of($componentname) as $modinstanceid => $cm) {
+            if ($modinstanceid == $instanceid) {
                 $context = context_module::instance($cm->id, IGNORE_MISSING);
                 break;
             }
@@ -88,47 +85,47 @@ if ($token==="$inttoken") {
     }
 
     if (empty($context)) {
-        //this shouldnt happen. something bad is going on.
+        // This shouldnt happen. something bad is going on.
         rss_error('rsserror');
     }
 
-    //make sure that $CFG->siteguest is set
+    // Make sure that $CFG->siteguest is set.
     if (empty($CFG->siteguest)) {
-        if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
-            // guest does not exist yet, weird
+        if (!$guestid = $DB->get_field('user', 'id', array('username' => 'guest', 'mnethostid' => $CFG->mnet_localhost_id))) {
+            // Guest does not exist yet, weird.
             rss_error('rsserror');
         }
         set_config('siteguest', $guestid);
     }
     $guesttoken = rss_get_token($CFG->siteguest);
 
-    //change forum to mod_forum (for example)
+    // Change forum to mod_forum (for example).
     $componentname = 'mod_'.$componentname;
 
     $url = $PAGE->url;
     $url->set_slashargument("/{$context->id}/$guesttoken/$componentname/$instanceid/rss.xml");
 
-    //redirect to the 2.0 rss URL
+    // Redirect to the 2.0 rss URL.
     redirect($url);
 } else {
-    // Authenticate the user from the token
+    // Authenticate the user from the token.
     $userid = rss_get_userid_from_token($token);
     if (!$userid) {
         rss_error('rsserrorauth');
     }
 }
 
-// Check the context actually exists
+// Check the context actually exists.
 list($context, $course, $cm) = get_context_info_array($contextid);
 
 $PAGE->set_context($context);
 
 $user = get_complete_user_data('id', $userid);
 
-// let enrol plugins deal with new enrolments if necessary
+// Let enrol plugins deal with new enrolments if necessary.
 enrol_check_plugins($user);
 
-\core\session\manager::set_user($user); //for login and capability checks
+\core\session\manager::set_user($user); // For login and capability checks.
 
 try {
     $autologinguest = true;
@@ -143,12 +140,11 @@ try {
     }
 }
 
-// Work out which component in Moodle we want (from the frankenstyle name)
+// Work out which component in Moodle we want (from the frankenstyle name).
 $componentdir = core_component::get_component_directory($componentname);
 list($type, $plugin) = core_component::normalize_component($componentname);
 
-
-// Call the component to check/update the feed and tell us the path to the cached file
+// Call the component to check/update the feed and tell us the path to the cached file.
 $pathname = null;
 
 if (file_exists($componentdir)) {
@@ -156,8 +152,8 @@ if (file_exists($componentdir)) {
     $functionname = $plugin.'_rss_get_feed';
 
     if (function_exists($functionname)) {
-        // $pathname will be null if there was a problem (eg user doesn't have the necessary capabilities)
-        // NOTE:the component providing the feed must do its own capability checks and security
+        // The $pathname will be null if there was a problem (eg user doesn't have the necessary capabilities).
+        // NOTE:the component providing the feed must do its own capability checks and security.
         try {
             $pathname = $functionname($context, $args);
         } catch (Exception $e) {
@@ -166,15 +162,13 @@ if (file_exists($componentdir)) {
     }
 }
 
-
-// Check that file exists
+// Check that file exists.
 if (empty($pathname) || !file_exists($pathname)) {
     rss_error();
 }
 
 // Send the RSS file to the user!
-send_file($pathname, 'rss.xml', 3600);   // Cached by browsers for 1 hour
-
+send_file($pathname, 'rss.xml', 3600);   // Cached by browsers for 1 hour.
 
 /**
  * Sends an error formatted as an rss file and then exits
diff --git a/rss/index.html b/rss/index.html
deleted file mode 100644 (file)
index 8b13789..0000000
+++ /dev/null
@@ -1 +0,0 @@
-
index 3d7040c..fdc0def 100644 (file)
@@ -1,23 +1,18 @@
 <?php
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// This file is part of Moodle - http://moodle.org/                      //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//                                                                       //
-// Moodle is free software: you can redistribute it and/or modify        //
-// it under the terms of the GNU General Public License as published by  //
-// the Free Software Foundation, either version 3 of the License, or     //
-// (at your option) any later version.                                   //
-//                                                                       //
-// Moodle is distributed in the hope that it will be useful,             //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of        //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         //
-// GNU General Public License for more details.                          //
-//                                                                       //
-// You should have received a copy of the GNU General Public License     //
-// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.       //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
  * Web service documentation renderer.
@@ -42,8 +37,8 @@ class core_rss_renderer extends plugin_renderer_base {
     public function user_reset_rss_token_confirmation() {
         global $OUTPUT, $CFG;
         $managetokenurl = $CFG->wwwroot."/user/managetoken.php?sesskey=" . sesskey();
-        $optionsyes = array('action'=>'resetrsstoken', 'confirm'=>1, 'sesskey'=>sesskey());
-        $optionsno  = array('section'=>'webservicetokens', 'sesskey'=>sesskey());
+        $optionsyes = array('action' => 'resetrsstoken', 'confirm' => 1, 'sesskey' => sesskey());
+        $optionsno  = array('section' => 'webservicetokens', 'sesskey' => sesskey());
         $formcontinue = new single_button(new moodle_url($managetokenurl, $optionsyes), get_string('reset'));
         $formcancel = new single_button(new moodle_url($managetokenurl, $optionsno), get_string('cancel'), 'get');
         $html = $OUTPUT->confirm(get_string('resettokenconfirmsimple', 'webservice'), $formcontinue, $formcancel);
@@ -58,7 +53,7 @@ class core_rss_renderer extends plugin_renderer_base {
     public function user_rss_token_box($token) {
         global $OUTPUT, $CFG;
 
-        // display strings
+        // Display strings.
         $stroperation = get_string('operation', 'webservice');
         $strtoken = get_string('key', 'webservice');
 
index 342199e..c26dc32 100644 (file)
@@ -63,10 +63,10 @@ class user_filtering {
         }
 
         if (empty($fieldnames)) {
-            $fieldnames = array('realname' => 0, 'lastname' => 1, 'firstname' => 1, 'email' => 1, 'city' => 1, 'country' => 1,
+            $fieldnames = array('realname' => 0, 'lastname' => 1, 'firstname' => 1, 'username' => 1, 'email' => 1, 'city' => 1, 'country' => 1,
                                 'confirmed' => 1, 'suspended' => 1, 'profile' => 1, 'courserole' => 1, 'systemrole' => 1,
                                 'cohort' => 1, 'firstaccess' => 1, 'lastaccess' => 1, 'neveraccessed' => 1, 'timemodified' => 1,
-                                'nevermodified' => 1, 'username' => 1, 'auth' => 1, 'mnethostid' => 1);
+                                'nevermodified' => 1, 'auth' => 1, 'mnethostid' => 1);
         }
 
         $this->_fields  = array();