Merge branch 'MDL-43908-master' of https://github.com/lucisgit/moodle
authorSam Hemelryk <sam@moodle.com>
Tue, 29 Jul 2014 03:50:50 +0000 (15:50 +1200)
committerSam Hemelryk <sam@moodle.com>
Tue, 29 Jul 2014 03:50:50 +0000 (15:50 +1200)
73 files changed:
admin/tool/behat/tests/behat/data_generators.feature
blocks/login/block_login.php
blog/lib.php
enrol/cohort/yui/quickenrolment/quickenrolment.js
filter/tex/latex.php
filter/tex/lib.php
filter/tex/settings.php
filter/tex/texdebug.php
lib/classes/grades_external.php
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/csvlib.class.php
lib/db/install.xml
lib/db/upgrade.php
lib/editor/atto/plugins/accessibilitychecker/lang/en/atto_accessibilitychecker.php
lib/editor/atto/plugins/accessibilitychecker/lib.php
lib/editor/atto/plugins/accessibilitychecker/tests/behat/accessibilitychecker.feature
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js
lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js
lib/editor/atto/plugins/accessibilitychecker/yui/src/button/js/button.js
lib/filestorage/file_storage.php
lib/outputrenderers.php
lib/tests/behat/behat_data_generators.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
message/lib.php
message/tests/messagelib_test.php
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
rating/index.php
rating/lib.php
rating/module.js
rating/rate.php
rating/rate_ajax.php
rating/tests/rating_test.php
rss/file.php
rss/index.html [deleted file]
rss/renderer.php
user/externallib.php
user/filters/lib.php
version.php

index 3561419..2b9dfdf 100644 (file)
@@ -252,3 +252,25 @@ Feature: Set up contextual data for tests
     And the "members" select box should contain "Student 1"
     And I set the field "groups" to "Group 2 (1)"
     And the "members" select box should contain "Student 2"
+
+  Scenario: Add cohorts with data generator
+    Given the following "categories" exist:
+      | name  | category | idnumber |
+      | Cat 1 | 0        | CAT1     |
+    And the following "cohorts" exist:
+      | name            | idnumber |
+      | System cohort 1 | CH01     |
+    And the following "cohorts" exist:
+      | name                 | idnumber | contextlevel | reference |
+      | System cohort 2      | CH02     | System       |           |
+      | Cohort in category 1 | CH1      | Category     | CAT1      |
+    When I log in as "admin"
+    And I navigate to "Cohorts" node in "Site administration > Users > Accounts"
+    Then I should see "System cohort 1"
+    And I should see "System cohort 2"
+    And I should not see "Cohort in category"
+    And I follow "Courses"
+    And I follow "Cat 1"
+    And I follow "Cohorts"
+    And I should see "Cohort in category 1"
+    And I should not see "System cohort"
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 d626a9f..a1966da 100644 (file)
@@ -94,6 +94,7 @@
             if (empty($pathlatex)) {
                 return false;
             }
+            $pathlatex = escapeshellarg(trim($pathlatex, " '\""));
 
             $doc = $this->construct_latex_document( $formula, $fontsize );
 
             fclose( $fh );
 
             // run latex on document
-            $command = "{$pathlatex} --interaction=nonstopmode --halt-on-error $tex";
+            $command = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
             chdir( $this->temp_dir );
             if ($this->execute($command, $log)) { // It allways False on Windows
 //                return false;
             }
 
             // run dvips (.dvi to .ps)
-            $pathdvips = get_config('filter_tex', 'pathdvips');
-            $command = "{$pathdvips} -E $dvi -o $ps";
+            $pathdvips = escapeshellarg(trim(get_config('filter_tex', 'pathdvips'), " '\""));
+            $command = "$pathdvips -E $dvi -o $ps";
             if ($this->execute($command, $log )) {
                 return false;
             }
                 $bg_opt = "";
             }
             if ($convertformat == 'svg') {
-                $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
-                $command = "{$pathdvisvgm} -E $ps -o $img";
+                $pathdvisvgm = escapeshellarg(trim(get_config('filter_tex', 'pathdvisvgm'), " '\""));
+                $command = "$pathdvisvgm -E $ps -o $img";
             } else {
-                $pathconvert = get_config('filter_tex', 'pathconvert');
-                $command = "{$pathconvert} -density $density -trim $bg_opt $ps $img";
+                $pathconvert = escapeshellarg(trim(get_config('filter_tex', 'pathconvert'), " '\""));
+                $command = "$pathconvert -density $density -trim $bg_opt $ps $img";
             }
             if ($this->execute($command, $log )) {
                 return false;
index f48bca8..4aad3ec 100644 (file)
@@ -125,9 +125,10 @@ function filter_tex_updatedcallback($name) {
         return;
     }
 
-    $pathdvips = get_config('filter_tex', 'pathdvips');
-    $pathconvert = get_config('filter_tex', 'pathconvert');
-    $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
+    $pathlatex = trim($pathlatex, " '\"");
+    $pathdvips = trim(get_config('filter_tex', 'pathdvips'), " '\"");
+    $pathconvert = trim(get_config('filter_tex', 'pathconvert'), " '\"");
+    $pathdvisvgm = trim(get_config('filter_tex', 'pathdvisvgm'), " '\"");
 
     $supportedformats = array('gif');
     if ((is_file($pathlatex) && is_executable($pathlatex)) &&
index fb0fcdf..e0d5ddc 100644 (file)
@@ -55,9 +55,22 @@ if ($ADMIN->fulltree) {
     } else if (PHP_OS=='WINNT' or PHP_OS=='WIN32' or PHP_OS=='Windows') {
         // note: you need Ghostscript installed (standard), miktex (standard)
         // and ImageMagick (install at c:\ImageMagick)
-        $default_filter_tex_pathlatex   = "\"c:\\texmf\\miktex\\bin\\latex.exe\" ";
-        $default_filter_tex_pathdvips   = "\"c:\\texmf\\miktex\\bin\\dvips.exe\" ";
-        $default_filter_tex_pathconvert = "\"c:\\imagemagick\\convert.exe\" ";
+        $default_filter_tex_pathlatex   = "c:\\texmf\\miktex\\bin\\latex.exe";
+        $default_filter_tex_pathdvips   = "c:\\texmf\\miktex\\bin\\dvips.exe";
+        $default_filter_tex_pathdvisvgm   = "c:\\texmf\\miktex\\bin\\dvisvgm.exe";
+        $default_filter_tex_pathconvert = "c:\\imagemagick\\convert.exe";
+    }
+
+    $pathlatex = get_config('filter_tex', 'pathlatex');
+    $pathdvips = get_config('filter_tex', 'pathdvips');
+    $pathconvert = get_config('filter_tex', 'pathconvert');
+    $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
+    if (strrpos($pathlatex . $pathdvips . $pathconvert . $pathdvisvgm, '"') or
+            strrpos($pathlatex . $pathdvips . $pathconvert . $pathdvisvgm, "'")) {
+        set_config('pathlatex', trim($pathlatex, " '\""), 'filter_tex');
+        set_config('pathdvips', trim($pathdvips, " '\""), 'filter_tex');
+        set_config('pathconvert', trim($pathconvert, " '\""), 'filter_tex');
+        set_config('pathdvisvgm', trim($pathdvisvgm, " '\""), 'filter_tex');
     }
 
     $items[] = new admin_setting_configexecutable('filter_tex/pathlatex', get_string('pathlatex', 'filter_tex'), '', $default_filter_tex_pathlatex);
index b82e97c..ad28cbc 100644 (file)
         // first check if it is likely to work at all
         $output .= "<h3>Checking executables</h3>\n";
         $executablesexist = true;
-        $pathlatex = get_config('filter_tex', 'pathlatex');
+        $pathlatex = trim(get_config('filter_tex', 'pathlatex'), " '\"");
         if (is_file($pathlatex)) {
             $output .= "latex executable ($pathlatex) is readable<br />\n";
         } else {
             $executablesexist = false;
             $output .= "<b>Error:</b> latex executable ($pathlatex) is not readable<br />\n";
         }
-        $pathdvips = get_config('filter_tex', 'pathdvips');
+        $pathdvips = trim(get_config('filter_tex', 'pathdvips'), " '\"");
         if (is_file($pathdvips)) {
             $output .= "dvips executable ($pathdvips) is readable<br />\n";
         } else {
             $executablesexist = false;
             $output .= "<b>Error:</b> dvips executable ($pathdvips) is not readable<br />\n";
         }
-        $pathconvert = get_config('filter_tex', 'pathconvert');
+        $pathconvert = trim(get_config('filter_tex', 'pathconvert'), " '\"");
         if (is_file($pathconvert)) {
             $output .= "convert executable ($pathconvert) is readable<br />\n";
         } else {
             $executablesexist = false;
             $output .= "<b>Error:</b> convert executable ($pathconvert) is not readable<br />\n";
         }
-        $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
+        $pathdvisvgm = trim(get_config('filter_tex', 'pathdvisvgm'), " '\"");
         if (is_file($pathdvisvgm)) {
             $output .= "dvisvgm executable ($pathdvisvgm) is readable<br />\n";
         } else {
         chdir($latex->temp_dir);
 
         // step 1: latex command
+        $pathlatex = escapeshellarg($pathlatex);
         $cmd = "$pathlatex --interaction=nonstopmode --halt-on-error $tex";
         $output .= execute($cmd);
 
         // step 2: dvips command
+        $pathdvips = escapeshellarg($pathdvips);
         $cmd = "$pathdvips -E $dvi -o $ps";
         $output .= execute($cmd);
 
         // Step 3: Set convert or dvisvgm command.
         if ($convertformat == 'svg') {
+            $pathdvisvgm = escapeshellarg($pathdvisvgm);
             $cmd = "$pathdvisvgm -E $ps -o $img";
         } else {
+            $pathconvert = escapeshellarg($pathconvert);
             $cmd = "$pathconvert -density 240 -trim $ps $img ";
         }
         $output .= execute($cmd);
index 96488db..a7c4fb5 100644 (file)
@@ -169,14 +169,18 @@ class core_grades_external extends external_api {
 
             if (!empty($gradeitem->grades)) {
                 foreach ($gradeitem->grades as $studentid => $studentgrade) {
-                    $gradegradeinstance = grade_grade::fetch(
-                        array(
-                            'userid' => $studentid,
-                            'itemid' => $gradeiteminstance->id
-                        )
-                    );
-                    if (!$canviewhidden && $gradegradeinstance->is_hidden()) {
-                        continue;
+                    if (!$canviewhidden) {
+                        // Need to load the grade_grade object to check visibility.
+                        $gradegradeinstance = grade_grade::fetch(
+                            array(
+                                'userid' => $studentid,
+                                'itemid' => $gradeiteminstance->id
+                            )
+                        );
+                        // The grade grade may be legitimately missing if the student has no grade.
+                        if (!empty($gradegradeinstance) && $gradegradeinstance->is_hidden()) {
+                            continue;
+                        }
                     }
                     $gradeitemarray['grades'][$studentid] = (array)$studentgrade;
                     // Add the student ID as some WS clients can't access the array key.
@@ -308,7 +312,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 +349,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 7fcd96e..ac9693e 100644 (file)
       <KEYS>
         <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
         <KEY NAME="pushid-userid" TYPE="unique" FIELDS="pushid, userid"/>
-        <KEY NAME="pushid-platform" TYPE="unique" FIELDS="pushid, platform"/>
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
index fc405f7..5d88ee4 100644 (file)
@@ -3709,5 +3709,15 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2014070101.00);
     }
 
+    if ($oldversion < 2014072400.01) {
+        $table = new xmldb_table('user_devices');
+        $oldindex = new xmldb_index('pushid-platform', XMLDB_KEY_UNIQUE, array('pushid', 'platform'));
+        if ($dbman->index_exists($table, $oldindex)) {
+            $key = new xmldb_key('pushid-platform', XMLDB_KEY_UNIQUE, array('pushid', 'platform'));
+            $dbman->drop_key($table, $key);
+        }
+        upgrade_main_savepoint(true, 2014072400.01);
+    }
+
     return true;
 }
index 28a90bb..fdd3df1 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['pluginname'] = 'Accessibility checker';
-$string['nowarnings'] = 'Congratulations, no accessibility problems found!';
-$string['report'] = 'Accessibility report:';
+$string['emptytext'] = 'Empty text';
 $string['imagesmissingalt'] = 'Images require alternative text. To fix this warning, add an alt attribute to your img tags. An empty alt attribute may be used, but only when the image is purely decorative and carries no information.';
 $string['needsmorecontrast'] = 'The colours of the foreground and background text do not have enough contrast. To fix this warning, change either foreground or background colour of the text so that it is easier to read.';
+$string['needsmoreheadings'] = 'There is a lot of text with no headings. Headings will allow screen reader users to navigate through the page easily and will make the page more usable for everyone.';
+$string['nowarnings'] = 'Congratulations, no accessibility problems found!';
+$string['pluginname'] = 'Accessibility checker';
+$string['report'] = 'Accessibility report:';
+$string['tablesmissingcaption'] = 'Tables should have captions. While it is not necessary for each table to have a caption, a caption is generally very helpful.';
+$string['tablesmissingheaders'] = 'Tables should use row and/or column headers.';
+$string['tableswithmergedcells'] = 'Tables should not contain merged cells. Despite being standard markup for tables for many years, some screen readers still do not fully support complex tables. When possible, try to "flatten" the table and avoid merged cells.';
index dd829f4..6bc3645 100644 (file)
@@ -34,7 +34,12 @@ function atto_accessibilitychecker_strings_for_js() {
     $PAGE->requires->strings_for_js(array('nowarnings',
                                     'report',
                                     'imagesmissingalt',
-                                    'needsmorecontrast'),
+                                    'needsmorecontrast',
+                                    'needsmoreheadings',
+                                    'tableswithmergedcells',
+                                    'tablesmissingcaption',
+                                    'emptytext',
+                                    'tablesmissingheaders'),
                                     'atto_accessibilitychecker');
 }
 
index a861bd8..9378dfe 100644 (file)
@@ -34,3 +34,39 @@ Feature: Atto accessibility checker
     When I click on "Show more buttons" "button"
     And I click on "Accessibility checker" "button"
     Then I should see "The colours of the foreground and background text do not have enough contrast."
+
+  @javascript
+  Scenario: No headings
+    Given I log in as "admin"
+    And I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<p>Sweet roll oat cake jelly-o macaroon donut oat cake. Caramels macaroon cookie sweet roll croissant cheesecake candy jelly-o. Gummies sugar plum sugar plum gingerbread dessert. Tiramisu bonbon jujubes danish marshmallow cookie chocolate cake cupcake tiramisu. Bear claw oat cake chocolate bar croissant. Lollipop cookie topping liquorice croissant. Brownie cookie cupcake lollipop cupcake cupcake. Fruitcake dessert sweet biscuit dragée caramels marzipan brownie. Chupa chups gingerbread apple pie cookie liquorice caramels carrot cake cookie gingerbread. Croissant candy jelly beans. Tiramisu apple pie dessert apple pie macaroon soufflé. Brownie powder carrot cake chocolate. Tart applicake croissant dragée macaroon chocolate donut.</p><p>Jelly beans gingerbread tootsie roll. Sugar plum tiramisu cotton candy toffee pie cotton candy tiramisu. Carrot cake chocolate bar sesame snaps cupcake cake dessert sweet fruitcake wafer. Marshmallow cupcake gingerbread pie sweet candy canes powder gummi bears. Jujubes cake muffin marshmallow candy jelly beans tootsie roll pie. Gummi bears applicake chocolate cake sweet jelly sesame snaps lollipop lollipop carrot cake. Marshmallow cake jelly beans. Jelly beans sesame snaps muffin halvah cookie ice cream candy canes carrot cake. Halvah donut marshmallow tiramisu. Cookie dessert gummi bears. Sugar plum apple pie jelly beans gummi bears tart chupa chups. Liquorice macaroon gummi bears gummies macaroon marshmallow sweet roll cake topping. Lemon drops caramels pie icing danish. Chocolate cake oat cake dessert halvah danish carrot cake apple pie.</p>"
+    When I click on "Show more buttons" "button"
+    And I click on "Accessibility checker" "button"
+    Then I should see "There is a lot of text with no headings."
+
+  @javascript
+  Scenario: Merged cells
+    Given I log in as "admin"
+    And I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<table><caption>Dogs that look good in pants</caption><tr><th>Breed</th><th>Coolness</th></tr><tr><td>Poodle</td><td rowspan='2'>NOT COOL</td></tr><tr><td>Doberman</td></tr></table>"
+    When I click on "Show more buttons" "button"
+    And I click on "Accessibility checker" "button"
+    Then I should see "Tables should not contain merged cells."
+
+  @javascript
+  Scenario: Table missing row/column headers
+    Given I log in as "admin"
+    And I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<table><caption>Dogs that look good in pants</caption><tr><th>Breed</th><td>Coolness</td></tr><tr><td>Poodle</td><td>NOT COOL</td></tr><tr><td>Doberman</td><td>COOL</td></tr></table>"
+    When I click on "Show more buttons" "button"
+    And I click on "Accessibility checker" "button"
+    Then I should see "Tables should use row and/or column headers."
+
+  @javascript
+  Scenario: Table missing caption
+    Given I log in as "admin"
+    And I navigate to "Edit profile" node in "My profile settings"
+    And I set the field "Description" to "<table><tr><th>Breed</th><th>Coolness</th></tr><tr><td>Poodle</td><td>NOT COOL</td></tr><tr><td>Doberman</td><td>COOL</td></tr></table>"
+    When I click on "Show more buttons" "button"
+    And I click on "Accessibility checker" "button"
+    Then I should see "Tables should have captions."
index cbdb87b..6547435 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-debug.js differ
index 026e26b..2418607 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button-min.js differ
index 9a4c177..7fd2b0b 100644 (file)
Binary files a/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js and b/lib/editor/atto/plugins/accessibilitychecker/yui/build/moodle-atto_accessibilitychecker-button/moodle-atto_accessibilitychecker-button.js differ
index 5797448..3aa5e5b 100644 (file)
@@ -168,10 +168,57 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
         }, this);
         this._addWarnings(list, M.util.get_string('needsmorecontrast', COMPONENT), problemNodes, false);
 
+        // Check for lots of text with no headings.
+        if (this.editor.get('text').length > 1000 && !this.editor.one('h3, h4, h5')) {
+            this._addWarnings(list, M.util.get_string('needsmoreheadings', COMPONENT), [this.editor], false);
+        }
+
+        // Check for tables with no captions.
+        problemNodes = [];
+        this.editor.all('table').each(function (table) {
+            caption = table.one('caption');
+            if (caption === null) {
+                problemNodes.push(table);
+            }
+        }, this);
+        this._addWarnings(list, M.util.get_string('tablesmissingcaption', COMPONENT), problemNodes, false);
+
+        // Check for tables with merged cells.
+        problemNodes = [];
+        this.editor.all('table').each(function (table) {
+            caption = table.one('[colspan],[rowspan]');
+            if (caption !== null) {
+                problemNodes.push(table);
+            }
+        }, this);
+        this._addWarnings(list, M.util.get_string('tableswithmergedcells', COMPONENT), problemNodes, false);
+
+        // Check for tables with no row/col headers
+        problemNodes = [];
+        this.editor.all('table').each(function (table) {
+            if (table.one('tr').one('td')) {
+                // First row has a non-header cell, so all rows must have at least one header.
+                table.all('tr').some(function (row) {
+                    if (!row.one('th')) {
+                        problemNodes.push(table);
+                        return true;
+                    }
+                    return false;
+                }, this);
+            } else {
+                // First row must have at least one header then.
+                if (!table.one('tr').one('th')) {
+                    problemNodes.push(table);
+                }
+            }
+        }, this);
+        this._addWarnings(list, M.util.get_string('tablesmissingheaders', COMPONENT), problemNodes, false);
+
         if (!list.hasChildNodes()) {
             list.append('<p>' + M.util.get_string('nowarnings', COMPONENT) + '</p>');
         }
-        // Append the list of current styles.
+
+        // Return the list of current warnings.
         return list;
     },
 
@@ -185,7 +232,7 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
      * @param {boolean} imagewarnings true if the warnings are related to images, false if text.
      */
     _addWarnings: function(list, description, nodes, imagewarnings) {
-        var warning, fails, i, src, textfield, li, link;
+        var warning, fails, i, src, textfield, li, link, text;
 
         if (nodes.length > 0) {
             warning = Y.Node.create('<p>' + description + '</p>');
@@ -198,7 +245,11 @@ Y.namespace('M.atto_accessibilitychecker').Button = Y.Base.create('button', Y.M.
                     link = Y.Node.create('<a href="#"><img src="' + src + '" /> ' + src + '</a>');
                 } else {
                     textfield = ('innerText' in nodes[i]) ? 'innerText' : 'textContent';
-                    link = Y.Node.create('<a href="#">' + nodes[i].get(textfield) + '</a>');
+                    text = nodes[i].get(textfield).trim();
+                    if (text === '') {
+                        text = M.util.get_string('emptytext', COMPONENT);
+                    }
+                    link = Y.Node.create('<a href="#">' + text + '</a>');
                 }
                 link.setData('sourceNode', nodes[i]);
                 li.append(link);
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..91afb0c 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);
 
@@ -1952,7 +1956,7 @@ class core_renderer extends renderer_base {
     public function doc_link($path, $text = '', $forcepopup = false) {
         global $CFG;
 
-        $icon = $this->pix_icon('docs', $text, 'moodle', array('class'=>'iconhelp icon-pre'));
+        $icon = $this->pix_icon('docs', '', 'moodle', array('class'=>'iconhelp icon-pre', 'role'=>'presentation'));
 
         $url = new moodle_url(get_docs_url($path));
 
index 0a18e4b..aba7b66 100644 (file)
@@ -217,6 +217,25 @@ class behat_data_generators extends behat_base {
         return $data;
     }
 
+    /**
+     * If contextlevel and reference are specified for cohort, transform them to the contextid.
+     *
+     * @param array $data
+     * @return array
+     */
+    protected function preprocess_cohort($data) {
+        if (isset($data['contextlevel'])) {
+            if (!isset($data['reference'])) {
+                throw new Exception('If field contextlevel is specified, field reference must also be present');
+            }
+            $context = $this->get_context($data['contextlevel'], $data['reference']);
+            unset($data['contextlevel']);
+            unset($data['reference']);
+            $data['contextid'] = $context->id;
+        }
+        return $data;
+    }
+
     /**
      * Adapter to modules generator
      * @throws Exception Custom exception for test writers
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 85e4a9e..9408c07 100644 (file)
@@ -704,70 +704,56 @@ function message_print_search($advancedsearch = false, $user1=null) {
 function message_get_recent_conversations($user, $limitfrom=0, $limitto=100) {
     global $DB;
 
-    $userfields = user_picture::fields('u', array('lastaccess'));
-    //This query retrieves the last message received from and sent to each user
-    //It unions that data then, within that set, it finds the most recent message you've exchanged with each user over all
-    //It then joins with some other tables to get some additional data we need
-
-    //message ID is used instead of timecreated as it should sort the same and will be much faster
-
-    //There is a separate query for read and unread queries as they are stored in different tables
-    //They were originally retrieved in one query but it was so large that it was difficult to be confident in its correctness
-    $sql = "SELECT $userfields, mr.id as mid, mr.notification, mr.smallmessage, mr.fullmessage, mr.fullmessagehtml, mr.fullmessageformat, mr.timecreated, mc.id as contactlistid, mc.blocked
-              FROM {message_read} mr
-              JOIN (
-                    SELECT messages.userid AS userid, MAX(messages.mid) AS mid
-                      FROM (
-                           SELECT mr1.useridto AS userid, MAX(mr1.id) AS mid
-                             FROM {message_read} mr1
-                            WHERE mr1.useridfrom = :userid1
-                                  AND mr1.notification = 0
-                         GROUP BY mr1.useridto
-                                  UNION
-                           SELECT mr2.useridfrom AS userid, MAX(mr2.id) AS mid
-                             FROM {message_read} mr2
-                            WHERE mr2.useridto = :userid2
-                                  AND mr2.notification = 0
-                         GROUP BY mr2.useridfrom
-                           ) messages
-                  GROUP BY messages.userid
-                   ) messages2 ON mr.id = messages2.mid AND (mr.useridto = messages2.userid OR mr.useridfrom = messages2.userid)
-              JOIN {user} u ON u.id = messages2.userid
-         LEFT JOIN {message_contacts} mc ON mc.userid = :userid3 AND mc.contactid = u.id
-             WHERE u.deleted = '0'
-          ORDER BY mr.id DESC";
-    $params = array('userid1' => $user->id, 'userid2' => $user->id, 'userid3' => $user->id);
-    $read =  $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
-
-    $sql = "SELECT $userfields, m.id as mid, m.notification, m.smallmessage, m.fullmessage, m.fullmessagehtml, m.fullmessageformat, m.timecreated, mc.id as contactlistid, mc.blocked
-              FROM {message} m
-              JOIN (
-                    SELECT messages.userid AS userid, MAX(messages.mid) AS mid
-                      FROM (
-                           SELECT m1.useridto AS userid, MAX(m1.id) AS mid
-                             FROM {message} m1
-                            WHERE m1.useridfrom = :userid1
-                                  AND m1.notification = 0
-                         GROUP BY m1.useridto
-                                  UNION
-                           SELECT m2.useridfrom AS userid, MAX(m2.id) AS mid
-                             FROM {message} m2
-                            WHERE m2.useridto = :userid2
-                                  AND m2.notification = 0
-                         GROUP BY m2.useridfrom
-                           ) messages
-                  GROUP BY messages.userid
-                   ) messages2 ON m.id = messages2.mid AND (m.useridto = messages2.userid OR m.useridfrom = messages2.userid)
-              JOIN {user} u ON u.id = messages2.userid
-         LEFT JOIN {message_contacts} mc ON mc.userid = :userid3 AND mc.contactid = u.id
-             WHERE u.deleted = '0'
-             ORDER BY m.id DESC";
-    $unread =  $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
+    $userfields = user_picture::fields('otheruser', array('lastaccess'));
+
+    // This query retrieves the most recent message received from or sent to
+    // seach other user.
+    //
+    // If two messages have the same timecreated, we take the one with the
+    // larger id.
+    //
+    // There is a separate query for read and unread messages as they are stored
+    // in different tables. They were originally retrieved in one query but it
+    // was so large that it was difficult to be confident in its correctness.
+    $sql = "SELECT $userfields,
+                   message.id as mid, message.notification, message.smallmessage, message.fullmessage,
+                   message.fullmessagehtml, message.fullmessageformat, message.timecreated,
+                   contact.id as contactlistid, contact.blocked
+
+              FROM {message_read} message
+              JOIN {user} otheruser ON otheruser.id = CASE
+                                WHEN message.useridto = :userid1 THEN message.useridfrom
+                                                                 ELSE message.useridto END
+         LEFT JOIN {message_contacts} contact ON contact.userid = :userid2 AND contact.contactid = otheruser.id
+
+             WHERE otheruser.deleted = 0
+               AND (message.useridto = :userid3 OR message.useridfrom = :userid4)
+               AND message.notification = 0
+               AND NOT EXISTS (
+                        SELECT 1
+                          FROM {message_read} othermessage
+                         WHERE ((othermessage.useridto = :userid5 AND othermessage.useridfrom = otheruser.id) OR
+                                (othermessage.useridfrom = :userid6 AND othermessage.useridto = otheruser.id))
+                           AND (othermessage.timecreated > message.timecreated OR (
+                                othermessage.timecreated = message.timecreated AND othermessage.id > message.id))
+                   )
+
+          ORDER BY message.timecreated DESC";
+    $params = array('userid1' => $user->id, 'userid2' => $user->id, 'userid3' => $user->id,
+            'userid4' => $user->id, 'userid5' => $user->id, 'userid6' => $user->id);
+    $read = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
+
+    // We want to get the messages that have not been read. These are stored in the 'message' table. It is the
+    // exact same query as the one above, except for the table we are querying. So, simply replace references to
+    // the 'message_read' table with the 'message' table.
+    $sql = str_replace('{message_read}', '{message}', $sql);
+    $unread = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
 
     $conversations = array();
 
-    //Union the 2 result sets together looking for the message with the most recent timecreated for each other user
-    //$conversation->id (the array key) is the other user's ID
+    // Union the 2 result sets together looking for the message with the most
+    // recent timecreated for each other user.
+    // $conversation->id (the array key) is the other user's ID.
     $conversation_arrays = array($unread, $read);
     foreach ($conversation_arrays as $conversation_array) {
         foreach ($conversation_array as $conversation) {
@@ -802,7 +788,7 @@ function message_get_recent_notifications($user, $limitfrom=0, $limitto=100) {
               FROM {message_read} mr
                    JOIN {user} u ON u.id=mr.useridfrom
              WHERE mr.useridto = :userid1 AND u.deleted = '0' AND mr.notification = :notification
-             ORDER BY mr.id DESC";//ordering by id should give the same result as ordering by timecreated but will be faster
+             ORDER BY mr.timecreated DESC";
     $params = array('userid1' => $user->id, 'notification' => 1);
 
     $notifications =  $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
index 126e6af..57006d0 100644 (file)
@@ -62,6 +62,7 @@ class core_message_messagelib_testcase extends advanced_testcase {
      * @param stdClass $userfrom user object of the one sending the message.
      * @param stdClass $userto user object of the one receiving the message.
      * @param string $message message to send.
+     * @return int the id of the message
      */
     protected function send_fake_message($userfrom, $userto, $message = 'Hello world!') {
         global $DB;
@@ -72,7 +73,8 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $record->subject = 'No subject';
         $record->fullmessage = $message;
         $record->timecreated = time();
-        $insert = $DB->insert_record('message', $record);
+
+        return $DB->insert_record('message', $record);
     }
 
     /**
@@ -358,4 +360,108 @@ class core_message_messagelib_testcase extends advanced_testcase {
         $this->assertEquals(false, message_search(array('Message'), true, true, 2));
         $this->assertCount(5, message_search(array('Message'), true, true, SITEID));
     }
+
+    /**
+     * Test message_get_recent_conversations.
+     */
+    public function test_message_get_recent_conversations() {
+        global $DB, $USER;
+
+        // Set this user as the admin.
+        $this->setAdminUser();
+
+        // Create user's to send messages to/from.
+        $user1 = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'user1'));
+        $user2 = $this->getDataGenerator()->create_user(array('firstname' => 'Test2', 'lastname' => 'user2'));
+
+        // Add a few messages that have been read and some that are unread.
+        $m1 = $this->send_fake_message($USER, $user1, 'Message 1'); // An unread message.
+        $m2 = $this->send_fake_message($user1, $USER, 'Message 2'); // An unread message.
+        $m3 = $this->send_fake_message($USER, $user1, 'Message 3'); // An unread message.
+        $m4 = message_post_message($USER, $user2, 'Message 4', FORMAT_PLAIN);
+        $m5 = message_post_message($user2, $USER, 'Message 5', FORMAT_PLAIN);
+        $m6 = message_post_message($USER, $user2, 'Message 6', FORMAT_PLAIN);
+
+        // We want to alter the timecreated values so we can ensure message_get_recent_conversations orders
+        // by timecreated, not the max id, to begin with. However, we also want more than one message to have
+        // the same timecreated value to ensure that when this happens we retrieve the one with the maximum id.
+
+        // Store the current time.
+        $time = time();
+
+        // Set the first and second unread messages to have the same timecreated value.
+        $updatemessage = new stdClass();
+        $updatemessage->id = $m1;
+        $updatemessage->timecreated = $time;
+        $DB->update_record('message', $updatemessage);
+
+        $updatemessage->id = $m2;
+        $updatemessage->timecreated = $time;
+        $DB->update_record('message', $updatemessage);
+
+        // Set the third unread message to have a timecreated value of 0.
+        $updatemessage->id = $m3;
+        $updatemessage->timecreated = 0;
+        $DB->update_record('message', $updatemessage);
+
+        // Set the first and second read messages to have the same timecreated value.
+        $updatemessage->id = $m4;
+        $updatemessage->timecreated = $time + 1;
+        $DB->update_record('message', $updatemessage);
+
+        $updatemessage->id = $m5;
+        $updatemessage->timecreated = $time + 1;
+        $DB->update_record('message', $updatemessage);
+
+        // Set the third read message to have a timecreated value of 0.
+        $updatemessage->id = $m6;
+        $updatemessage->timecreated = 0;
+        $DB->update_record('message_read', $updatemessage);
+
+        // Get the recent conversations for the current user.
+        $conversations = message_get_recent_conversations($USER);
+
+        // Confirm that we have received the messages with the maximum timecreated, rather than the max id.
+        $this->assertEquals('Message 2', $conversations[0]->fullmessage);
+        $this->assertEquals('Message 5', $conversations[1]->smallmessage);
+    }
+
+    /**
+     * Test message_get_recent_notifications.
+     */
+    public function test_message_get_recent_notifications() {
+        global $DB, $USER;
+
+        // Set this user as the admin.
+        $this->setAdminUser();
+
+        // Create a user to send messages from.
+        $user1 = $this->getDataGenerator()->create_user(array('firstname' => 'Test1', 'lastname' => 'user1'));
+
+        // Add two messages - will mark them as notifications later.
+        $m1 = message_post_message($user1, $USER, 'Message 1', FORMAT_PLAIN);
+        $m2 = message_post_message($user1, $USER, 'Message 2', FORMAT_PLAIN);
+
+        // Mark the second message as a notification.
+        $updatemessage = new stdClass();
+        $updatemessage->id = $m2;
+        $updatemessage->notification = 1;
+        $DB->update_record('message_read', $updatemessage);
+
+        // Mark the first message as a notification and change the timecreated to 0.
+        $updatemessage->id = $m1;
+        $updatemessage->notification = 1;
+        $updatemessage->timecreated = 0;
+        $DB->update_record('message_read', $updatemessage);
+
+        $notifications = message_get_recent_notifications($USER);
+
+        // Get the messages.
+        $firstmessage = array_shift($notifications);
+        $secondmessage = array_shift($notifications);
+
+        // Confirm that we have received the notifications with the maximum timecreated, rather than the max id.
+        $this->assertEquals('Message 2', $firstmessage->smallmessage);
+        $this->assertEquals('Message 1', $secondmessage->smallmessage);
+    }
 }
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 ae41671..54ae1bb 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);
@@ -1877,12 +1883,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 {
@@ -3056,7 +3062,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();
@@ -3426,7 +3433,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,
@@ -5125,7 +5133,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 &&
@@ -5308,7 +5317,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();
@@ -5406,8 +5416,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);
@@ -5884,7 +5898,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 ddfc224..42039d7 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -33,12 +32,15 @@ $ratingarea = optional_param('ratingarea', null, PARAM_AREA);
 $itemid     = required_param('itemid', PARAM_INT);
 $scaleid    = required_param('scaleid', PARAM_INT);
 $sort       = optional_param('sort', '', PARAM_ALPHA);
-$popup      = optional_param('popup', 0, PARAM_INT); //==1 if in a popup window?
+$popup      = optional_param('popup', 0, PARAM_INT); // Any non-zero value if in a popup window.
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 require_login($course, false, $cm);
 
-$url = new moodle_url('/rating/index.php', array('contextid'=>$contextid,'component'=>$component,'itemid'=>$itemid,'scaleid'=>$scaleid));
+$url = new moodle_url('/rating/index.php', array('contextid' => $contextid,
+                                                 'component' => $component,
+                                                 'itemid' => $itemid,
+                                                 'scaleid' => $scaleid));
 if (!empty($ratingarea)) {
     $url->param('ratingarea', $ratingarea);
 }
@@ -55,17 +57,22 @@ if ($popup) {
     $PAGE->set_pagelayout('popup');
 }
 
-if (!has_capability('moodle/rating:view',$context)) {
+if (!has_capability('moodle/rating:view', $context)) {
     print_error('noviewrate', 'rating');
 }
-if (!has_capability('moodle/rating:viewall',$context) and $USER->id != $item->userid) {
+if (!has_capability('moodle/rating:viewall', $context) and $USER->id != $item->userid) {
     print_error('noviewanyrate', 'rating');
 }
 
 switch ($sort) {
-    case 'firstname': $sqlsort = "u.firstname ASC"; break;
-    case 'rating':    $sqlsort = "r.rating ASC"; break;
-    default:          $sqlsort = "r.timemodified ASC";
+    case 'firstname':
+        $sqlsort = "u.firstname ASC";
+        break;
+    case 'rating':
+        $sqlsort = "r.rating ASC";
+        break;
+    default:
+        $sqlsort = "r.timemodified ASC";
 }
 
 $scalemenu = make_grades_menu($scaleid);
@@ -74,7 +81,7 @@ $strrating  = get_string('rating', 'rating');
 $strname    = get_string('name');
 $strtime    = get_string('time');
 
-$PAGE->set_title(get_string('allratingsforitem','rating'));
+$PAGE->set_title(get_string('allratingsforitem', 'rating'));
 echo $OUTPUT->header();
 
 $ratingoptions = new stdClass;
@@ -87,10 +94,10 @@ $ratingoptions->sort = $sqlsort;
 $rm = new rating_manager();
 $ratings = $rm->get_all_ratings_for_item($ratingoptions);
 if (!$ratings) {
-    $msg = get_string('noratings','rating');
-    echo html_writer::tag('div', $msg, array('class'=>'mdl-align'));
+    $msg = get_string('noratings', 'rating');
+    echo html_writer::tag('div', $msg, array('class' => 'mdl-align'));
 } else {
-    // To get the sort URL, copy the current URL and remove any previous sort
+    // To get the sort URL, copy the current URL and remove any previous sort.
     $sorturl = new moodle_url($url);
     $sorturl->remove_params('sort');
 
@@ -107,14 +114,14 @@ if (!$ratings) {
     $table->colclasses = array('', 'firstname', 'rating', 'time');
     $table->data = array();
 
-    // If the scale was changed after ratings were submitted some ratings may have a value above the current maximum
-    // We can't just do count($scalemenu) - 1 as custom scales start at index 1, not 0
+    // If the scale was changed after ratings were submitted some ratings may have a value above the current maximum.
+    // We can't just do count($scalemenu) - 1 as custom scales start at index 1, not 0.
     $maxrating = max(array_keys($scalemenu));
 
     foreach ($ratings as $rating) {
-        //Undo the aliasing of the user id column from user_picture::fields()
-        //we could clone the rating object or preserve the rating id if we needed it again
-        //but we don't
+        // Undo the aliasing of the user id column from user_picture::fields().
+        // We could clone the rating object or preserve the rating id if we needed it again
+        // but we don't.
         $rating->id = $rating->userid;
 
         $row = new html_table_row();
index 4aaa70b..b63b5b8 100644 (file)
@@ -25,7 +25,7 @@
 
 define('RATING_UNSET_RATING', -999);
 
-define ('RATING_AGGREGATE_NONE', 0); //no ratings
+define ('RATING_AGGREGATE_NONE', 0); // No ratings.
 define ('RATING_AGGREGATE_AVERAGE', 1);
 define ('RATING_AGGREGATE_COUNT', 2);
 define ('RATING_AGGREGATE_MAXIMUM', 3);
@@ -129,12 +129,12 @@ class rating implements renderable {
      * }
      */
     public function __construct($options) {
-        $this->context =    $options->context;
-        $this->component =  $options->component;
+        $this->context = $options->context;
+        $this->component = $options->component;
         $this->ratingarea = $options->ratingarea;
-        $this->itemid =     $options->itemid;
-        $this->scaleid =    $options->scaleid;
-        $this->userid =     $options->userid;
+        $this->itemid = $options->itemid;
+        $this->scaleid = $options->scaleid;
+        $this->userid = $options->userid;
 
         if (isset($options->settings)) {
             $this->settings = $options->settings;
@@ -176,7 +176,7 @@ class rating implements renderable {
         $ratingoptions->component = $this->component;
         $ratingoptions->ratingarea = $this->ratingarea;
         $ratingoptions->items = $items;
-        $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE;//we dont actually care what aggregation method is applied
+        $ratingoptions->aggregate = RATING_AGGREGATE_AVERAGE; // We dont actually care what aggregation method is applied.
         $ratingoptions->scaleid = $this->scaleid;
         $ratingoptions->userid = $this->userid;
 
@@ -185,7 +185,7 @@ class rating implements renderable {
         $firstitem = $items[0]->rating;
 
         if (empty($firstitem->id)) {
-            // Insert a new rating
+            // Insert a new rating.
             $data->contextid    = $this->context->id;
             $data->component    = $this->component;
             $data->ratingarea   = $this->ratingarea;
@@ -197,7 +197,7 @@ class rating implements renderable {
             $data->timemodified = $time;
             $DB->insert_record('rating', $data);
         } else {
-            // Update the rating
+            // Update the rating.
             $data->id           = $firstitem->id;
             $DB->update_record('rating', $data);
         }
@@ -222,12 +222,14 @@ class rating implements renderable {
         $aggregate = $this->aggregate;
         $method = $this->settings->aggregationmethod;
 
-        // only display aggregate if aggregation method isn't COUNT
+        // Only display aggregate if aggregation method isn't COUNT.
         $aggregatestr = '';
         if ($aggregate && $method != RATING_AGGREGATE_COUNT) {
             if ($method != RATING_AGGREGATE_SUM && !$this->settings->scale->isnumeric) {
-                $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)]; //round aggregate as we're using it as an index
-            } else { // aggregation is SUM or the scale is numeric
+
+                // Round aggregate as we're using it as an index.
+                $aggregatestr .= $this->settings->scale->scaleitems[round($aggregate)];
+            } else { // Aggregation is SUM or the scale is numeric.
                 $aggregatestr .= round($aggregate, 1);
             }
         }
@@ -246,20 +248,20 @@ class rating implements renderable {
             global $USER;
             $userid = $USER->id;
         }
-        // You can't rate your item
+        // You can't rate your item.
         if ($this->itemuserid == $userid) {
             return false;
         }
-        // You can't rate if you don't have the system cap
+        // You can't rate if you don't have the system cap.
         if (!$this->settings->permissions->rate) {
             return false;
         }
-        // You can't rate if you don't have the plugin cap
+        // You can't rate if you don't have the plugin cap.
         if (!$this->settings->pluginpermissions->rate) {
             return false;
         }
 
-        // You can't rate if the item was outside of the assessment times
+        // You can't rate if the item was outside of the assessment times.
         $timestart = $this->settings->assesstimestart;
         $timefinish = $this->settings->assesstimefinish;
         $timecreated = $this->itemtimecreated;
@@ -281,14 +283,20 @@ class rating implements renderable {
             $userid = $USER->id;
         }
 
-        // if the item doesnt belong to anyone or its another user's items and they can see the aggregate on items they don't own
-        // Note that viewany doesnt mean you can see the aggregate or ratings of your own items
-        if ((empty($this->itemuserid) or $this->itemuserid != $userid) && $this->settings->permissions->viewany && $this->settings->pluginpermissions->viewany ) {
+        // If the item doesnt belong to anyone or its another user's items and they can see the aggregate on items they don't own.
+        // Note that viewany doesnt mean you can see the aggregate or ratings of your own items.
+        if ((empty($this->itemuserid) or $this->itemuserid != $userid)
+            && $this->settings->permissions->viewany
+            && $this->settings->pluginpermissions->viewany ) {
+
             return true;
         }
 
-        // if its the current user's item and they have permission to view the aggregate on their own items
-        if ($this->itemuserid == $userid && $this->settings->permissions->view && $this->settings->pluginpermissions->view) {
+        // If its the current user's item and they have permission to view the aggregate on their own items.
+        if ($this->itemuserid == $userid
+            && $this->settings->permissions->view
+            && $this->settings->pluginpermissions->view) {
+
             return true;
         }
 
@@ -352,14 +360,7 @@ class rating implements renderable {
         return $url;
     }
 
-    /**
-    * Remove this rating from the database
-    * @return void
-    */
-    //public function delete_rating() {
-        //todo implement this if its actually needed
-    //}
-} //end rating class definition
+} // End rating class definition.
 
 /**
  * The rating_manager class provides the ability to retrieve sets of ratings from the database
@@ -414,7 +415,9 @@ class rating_manager {
     }
 
     /**
-     * Returns an array of ratings for a given item (forum post, glossary entry etc). This returns all users ratings for a single item
+     * Returns an array of ratings for a given item (forum post, glossary entry etc).
+     *
+     * This returns all users ratings for a single item
      *
      * @param stdClass $options {
      *            context => context the context in which the ratings exists [required]
@@ -442,7 +445,7 @@ class rating_manager {
         }
 
         $sortclause = '';
-        if( !empty($options->sort) ) {
+        if (!empty($options->sort)) {
             $sortclause = "ORDER BY $options->sort";
         }
 
@@ -469,16 +472,16 @@ class rating_manager {
      * Adds rating objects to an array of items (forum posts, glossary entries etc). Rating objects are available at $item->rating
      *
      * @param stdClass $options {
-     *            context          => context the context in which the ratings exists [required]
-     *            component        => the component name ie mod_forum [required]
-     *            ratingarea       => the ratingarea we are interested in [required]
-     *            items            => array an array of items such as forum posts or glossary items. They must have an 'id' member ie $items[0]->id[required]
-     *            aggregate        => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
-     *            scaleid          => int the scale from which the user can select a rating [required]
-     *            userid           => int the id of the current user [optional]
-     *            returnurl        => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
-     *            assesstimestart  => int only allow rating of items created after this timestamp [optional]
-     *            assesstimefinish => int only allow rating of items created before this timestamp [optional]
+     *      context          => context the context in which the ratings exists [required]
+     *      component        => the component name ie mod_forum [required]
+     *      ratingarea       => the ratingarea we are interested in [required]
+     *      items            => array items like forum posts or glossary items. Each item needs an 'id' ie $items[0]->id [required]
+     *      aggregate        => int aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
+     *      scaleid          => int the scale from which the user can select a rating [required]
+     *      userid           => int the id of the current user [optional]
+     *      returnurl        => string the url to return the user to after submitting a rating. Null for ajax requests [optional]
+     *      assesstimestart  => int only allow rating of items created after this timestamp [optional]
+     *      assesstimefinish => int only allow rating of items created before this timestamp [optional]
      * @return array the array of items with their ratings attached at $items[0]->rating
      */
     public function get_ratings($options) {
@@ -509,12 +512,12 @@ class rating_manager {
         if (!isset($options->aggregate)) {
             throw new coding_exception('The aggregate option is a required option when getting ratings.');
         } else if ($options->aggregate == RATING_AGGREGATE_NONE) {
-            // Ratings arn't enabled.
+            // Ratings are not enabled.
             return $options->items;
         }
         $aggregatestr = $this->get_aggregation_method($options->aggregate);
 
-        // Default the userid to the current user if it is not set
+        // Default the userid to the current user if it is not set.
         if (empty($options->userid)) {
             $userid = $USER->id;
         } else {
@@ -525,15 +528,20 @@ class rating_manager {
         // from the related component.
         list($type, $name) = core_component::normalize_component($options->component);
         $default = array(null, 'id', 'userid');
-        list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type, $name, 'rating', 'get_item_fields', array($options), $default);
-
-        // Create an array of item ids
+        list($itemtablename, $itemidcol, $itemuseridcol) = plugin_callback($type,
+                                                                           $name,
+                                                                           'rating',
+                                                                           'get_item_fields',
+                                                                           array($options),
+                                                                           $default);
+
+        // Create an array of item IDs.
         $itemids = array();
         foreach ($options->items as $item) {
             $itemids[] = $item->{$itemidcol};
         }
 
-        // get the items from the database
+        // Get the items from the database.
         list($itemidtest, $params) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
         $params['contextid'] = $options->context->id;
         $params['userid']    = $userid;
@@ -567,11 +575,11 @@ class rating_manager {
         $ratingoptions->settings = $this->generate_rating_settings_object($options);
         foreach ($options->items as $item) {
             $founduserrating = false;
-            foreach($userratings as $userrating) {
-                //look for an existing rating from this user of this item
+            foreach ($userratings as $userrating) {
+                // Look for an existing rating from this user of this item.
                 if ($item->{$itemidcol} == $userrating->itemid) {
-                    // Note: rec->scaleid = the id of scale at the time the rating was submitted
-                    // may be different from the current scale id
+                    // Note: rec->scaleid = the id of scale at the time the rating was submitted.
+                    // It may be different from the current scale id.
                     $ratingoptions->scaleid = $userrating->scaleid;
                     $ratingoptions->userid = $userrating->userid;
                     $ratingoptions->id = $userrating->id;
@@ -585,7 +593,7 @@ class rating_manager {
                 $ratingoptions->scaleid = null;
                 $ratingoptions->userid = null;
                 $ratingoptions->id = null;
-                $ratingoptions->rating =  null;
+                $ratingoptions->rating = null;
             }
 
             if (array_key_exists($item->{$itemidcol}, $aggregateratings)) {
@@ -617,9 +625,9 @@ class rating_manager {
      *      context           => context the context in which the ratings exists [required]
      *      component         => string The component the items belong to [required]
      *      ratingarea        => string The ratingarea the items belong to [required]
-     *      aggregate         => int what aggregation method should be applied. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
+     *      aggregate         => int Aggregation method to apply. RATING_AGGREGATE_AVERAGE, RATING_AGGREGATE_MAXIMUM etc [required]
      *      scaleid           => int the scale from which the user can select a rating [required]
-     *      returnurl         => string the url to return the user to after submitting a rating. Can be left null for ajax requests [optional]
+     *      returnurl         => string the url to return the user to after submitting a rating. Null for ajax requests [optional]
      *      assesstimestart   => int only allow rating of items created after this timestamp [optional]
      *      assesstimefinish  => int only allow rating of items created before this timestamp [optional]
      *      plugintype        => string plugin type ie 'mod' Used to find the permissions callback [optional]
@@ -645,14 +653,14 @@ class rating_manager {
             throw new coding_exception('The scaleid option is now a required option when generating a rating settings object.');
         }
 
-        // settings that are common to all ratings objects in this context
+        // Settings that are common to all ratings objects in this context.
         $settings = new stdClass;
-        $settings->scale             = $this->generate_rating_scale_object($options->scaleid); // the scale to use now
+        $settings->scale             = $this->generate_rating_scale_object($options->scaleid); // The scale to use now.
         $settings->aggregationmethod = $options->aggregate;
         $settings->assesstimestart   = null;
         $settings->assesstimefinish  = null;
 
-        // Collect options into the settings object
+        // Collect options into the settings object.
         if (!empty($options->assesstimestart)) {
             $settings->assesstimestart = $options->assesstimestart;
         }
@@ -663,15 +671,22 @@ class rating_manager {
             $settings->returnurl = $options->returnurl;
         }
 
-        // check site capabilities
+        // Check site capabilities.
         $settings->permissions = new stdClass;
-        $settings->permissions->view    = has_capability('moodle/rating:view', $options->context); // can view the aggregate of ratings of their own items
-        $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context); // can view the aggregate of ratings of other people's items
-        $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context); // can view individual ratings
-        $settings->permissions->rate    = has_capability('moodle/rating:rate', $options->context); // can submit ratings
-
-        // check module capabilities (mostly for backwards compatability with old modules that previously implemented their own ratings)
-        $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id, $options->component, $options->ratingarea);
+        // Can view the aggregate of ratings of their own items.
+        $settings->permissions->view    = has_capability('moodle/rating:view', $options->context);
+        // Can view the aggregate of ratings of other people's items.
+        $settings->permissions->viewany = has_capability('moodle/rating:viewany', $options->context);
+        // Can view individual ratings.
+        $settings->permissions->viewall = has_capability('moodle/rating:viewall', $options->context);
+        // Can submit ratings.
+        $settings->permissions->rate    = has_capability('moodle/rating:rate', $options->context);
+
+        // Check module capabilities
+        // This is mostly for backwards compatability with old modules that previously implemented their own ratings.
+        $pluginpermissionsarray = $this->get_plugin_permissions_array($options->context->id,
+                                                                      $options->component,
+                                                                      $options->ratingarea);
         $settings->pluginpermissions = new stdClass;
         $settings->pluginpermissions->view    = $pluginpermissionsarray['view'];
         $settings->pluginpermissions->viewany = $pluginpermissionsarray['viewany'];
@@ -700,25 +715,25 @@ class rating_manager {
             $scale->max = $scaleid;
 
             if ($scaleid < 0) {
-                // It is a proper scale (not numeric)
+                // It is a proper scale (not numeric).
                 $scalerecord = $DB->get_record('scale', array('id' => abs($scaleid)));
                 if ($scalerecord) {
-                    // We need to generate an array with string keys starting at 1
+                    // We need to generate an array with string keys starting at 1.
                     $scalearray = explode(',', $scalerecord->scale);
                     $c = count($scalearray);
                     for ($i = 0; $i < $c; $i++) {
-                        // treat index as a string to allow sorting without changing the value
+                        // Treat index as a string to allow sorting without changing the value.
                         $scale->scaleitems[(string)($i + 1)] = $scalearray[$i];
                     }
-                    krsort($scale->scaleitems); // have the highest grade scale item appear first
+                    krsort($scale->scaleitems); // Have the highest grade scale item appear first.
                     $scale->isnumeric = false;
                     $scale->name = $scalerecord->name;
                     $scale->courseid = $scalerecord->courseid;
                     $scale->max = count($scale->scaleitems);
                 }
             } else {
-                //generate an array of values for numeric scales
-                for($i = 0; $i <= (int)$scaleid; $i++) {
+                // Generate an array of values for numeric scales.
+                for ($i = 0; $i <= (int)$scaleid; $i++) {
                     $scale->scaleitems[(string)$i] = $i;
                 }
             }
@@ -738,13 +753,11 @@ class rating_manager {
      * @return int|null return null if the created time is unavailable, otherwise return a timestamp
      */
     protected function get_item_time_created($item) {
-        if( !empty($item->created) ) {
-            return $item->created;//the forum_posts table has created instead of timecreated
-        }
-        else if(!empty($item->timecreated)) {
+        if (!empty($item->created)) {
+            return $item->created; // The forum_posts table has created instead of timecreated.
+        } else if (!empty($item->timecreated)) {
             return $item->timecreated;
-        }
-        else {
+        } else {
             return null;
         }
     }
@@ -753,16 +766,16 @@ class rating_manager {
      * Returns an array of grades calculated by aggregating item ratings.
      *
      * @param stdClass $options {
-     *            userid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
-     *            aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
-     *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
-     *            itemtable => int the table containing the items [required]
-     *            itemtableusercolum => int the column of the user table containing the item owner's user id [required]
-     *            component => The component for the ratings [required]
-     *            ratingarea => The ratingarea for the ratings [required]
-     *            contextid => int the context in which the rated items exist [optional]
-     *            modulename => string the name of the module [optional]
-     *            moduleid => int the id of the module instance [optional]
+     *      userid => int the id of the user whose items were rated, NOT the user who submitted ratings. 0 to update all. [required]
+     *      aggregationmethod => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [required]
+     *      scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
+     *      itemtable => int the table containing the items [required]
+     *      itemtableusercolum => int the column of the user table containing the item owner's user id [required]
+     *      component => The component for the ratings [required]
+     *      ratingarea => The ratingarea for the ratings [required]
+     *      contextid => int the context in which the rated items exist [optional]
+     *      modulename => string the name of the module [optional]
+     *      moduleid => int the id of the module instance [optional]
      * }
      * @return array the array of the user's grades
      */
@@ -778,16 +791,10 @@ class rating_manager {
             throw new coding_exception('The ratingarea option is now a required option when getting user grades from ratings.');
         }
 
-        //if the calling code doesn't supply a context id we'll have to figure it out
-        if( !empty($options->contextid) ) {
+        // If the calling code doesn't supply a context id we'll have to figure it out.
+        if (!empty($options->contextid)) {
             $contextid = $options->contextid;
-        }
-        else if( !empty($options->cmid) ) {
-            //not implemented as not currently used although cmid is potentially available (the forum supplies it)
-            //Is there a convenient way to get a context id from a cm id?
-            //$cmidnumber = $options->cmidnumber;
-        }
-        else if ( !empty($options->modulename) && !empty($options->moduleid) ) {
+        } else if (!empty($options->modulename) && !empty($options->moduleid)) {
             $modulename = $options->modulename;
             $moduleid   = intval($options->moduleid);
 
@@ -814,16 +821,16 @@ class rating_manager {
         $scaleid              = $options->scaleid;
         $aggregationstring    = $this->get_aggregation_method($options->aggregationmethod);
 
-        //if userid is not 0 we only want the grade for a single user
+        // If userid is not 0 we only want the grade for a single user.
         $singleuserwhere = '';
         if ($options->userid != 0) {
             $params['userid1'] = intval($options->userid);
             $singleuserwhere = "AND i.{$itemtableusercolumn} = :userid1";
         }
 
-        //MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)"
-        //r.contextid will be null for users who haven't been rated yet
-        //no longer including users who haven't been rated to reduce memory requirements
+        // MDL-24648 The where line used to be "WHERE (r.contextid is null or r.contextid=:contextid)".
+        // r.contextid will be null for users who haven't been rated yet.
+        // No longer including users who haven't been rated to reduce memory requirements.
         $sql = "SELECT u.id as id, u.id AS userid, $aggregationstring(r.rating) AS rawgrade
                   FROM {user} u
              LEFT JOIN {{$itemtable}} i ON u.id=i.{$itemtableusercolumn}
@@ -840,10 +847,10 @@ class rating_manager {
             $scale = null;
             $max = 0;
             if ($options->scaleid >= 0) {
-                //numeric
+                // Numeric.
                 $max = $options->scaleid;
             } else {
-                //custom scales
+                // Custom scales.
                 $scale = $DB->get_record('scale', array('id' => -$options->scaleid));
                 if ($scale) {
                     $scale = explode(',', $scale->scale);
@@ -853,16 +860,17 @@ class rating_manager {
                 }
             }
 
-            // it could throw off the grading if count and sum returned a rawgrade higher than scale
-            // so to prevent it we review the results and ensure that rawgrade does not exceed the scale, if it does we set rawgrade = scale (i.e. full credit)
-            foreach ($results as $rid=>$result) {
+            // It could throw off the grading if count and sum returned a rawgrade higher than scale
+            // so to prevent it we review the results and ensure that rawgrade does not exceed the scale.
+            // If it does we set rawgrade = scale (i.e. full credit).
+            foreach ($results as $rid => $result) {
                 if ($options->scaleid >= 0) {
-                    //numeric
+                    // Numeric.
                     if ($result->rawgrade > $options->scaleid) {
                         $results[$rid]->rawgrade = $options->scaleid;
                     }
                 } else {
-                    //scales
+                    // Scales.
                     if (!empty($scale) && $result->rawgrade > $max) {
                         $results[$rid]->rawgrade = $max;
                     }
@@ -912,8 +920,8 @@ class rating_manager {
                 $aggregatestr = 'SUM';
                 break;
             default:
-                $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270
-                debugging('Incorrect call to get_aggregation_method(), was called with incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
+                $aggregatestr = 'AVG'; // Default to this to avoid real breakage - MDL-22270.
+                debugging('Incorrect call to get_aggregation_method(), incorrect aggregate method ' . $aggregate, DEBUG_DEVELOPER);
         }
         return $aggregatestr;
     }
@@ -928,10 +936,16 @@ class rating_manager {
      */
     public function get_plugin_permissions_array($contextid, $component, $ratingarea) {
         $pluginpermissionsarray = null;
-        $defaultpluginpermissions = array('rate'=>false,'view'=>false,'viewany'=>false,'viewall'=>false);//deny by default
+        // Deny by default.
+        $defaultpluginpermissions = array('rate' => false, 'view' => false, 'viewany' => false, 'viewall' => false);
         if (!empty($component)) {
             list($type, $name) = core_component::normalize_component($component);
-            $pluginpermissionsarray = plugin_callback($type, $name, 'rating', 'permissions', array($contextid, $component, $ratingarea), $defaultpluginpermissions);
+            $pluginpermissionsarray = plugin_callback($type,
+                                                      $name,
+                                                      'rating',
+                                                      'permissions',
+                                                      array($contextid, $component, $ratingarea),
+                                                      $defaultpluginpermissions);
         } else {
             $pluginpermissionsarray = $defaultpluginpermissions;
         }
@@ -942,15 +956,15 @@ class rating_manager {
      * Validates a submitted rating
      *
      * @param array $params submitted data
-     *            context => object the context in which the rated items exists [required]
-     *            component => The component the rating belongs to [required]
-     *            ratingarea => The ratingarea the rating is associated with [required]
-     *            itemid => int the ID of the object being rated [required]
-     *            scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
-     *            rating => int the submitted rating
-     *            rateduserid => int the id of the user whose items have been rated. NOT the user who submitted the ratings. 0 to update all. [required]
-     *            aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
-     * @return boolean true if the rating is valid. False if callback wasnt found and will throw rating_exception if rating is invalid
+     *      context => object the context in which the rated items exists [required]
+     *      component => The component the rating belongs to [required]
+     *      ratingarea => The ratingarea the rating is associated with [required]
+     *      itemid => int the ID of the object being rated [required]
+     *      scaleid => int the scale from which the user can select a rating. Used for bounds checking. [required]
+     *      rating => int the submitted rating
+     *      rateduserid => int the id of the user whose items have been rated. 0 to update all. [required]
+     *      aggregation => int the aggregation method to apply when calculating grades ie RATING_AGGREGATE_AVERAGE [optional]
+     * @return boolean true if the rating is valid, false if callback not found, throws rating_exception if rating is invalid
      */
     public function check_rating_is_valid($params) {
 
@@ -975,11 +989,11 @@ class rating_manager {
 
         list($plugintype, $pluginname) = core_component::normalize_component($params['component']);
 
-        //this looks for a function like forum_rating_validate() in mod_forum lib.php
-        //wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments
+        // This looks for a function like forum_rating_validate() in mod_forum lib.php
+        // wrapping the params array in another array as call_user_func_array() expands arrays into multiple arguments.
         $isvalid = plugin_callback($plugintype, $pluginname, 'rating', 'validate', array($params), null);
 
-        //if null then the callback doesn't exist
+        // If null then the callback does not exist.
         if ($isvalid === null) {
             $isvalid = false;
             debugging('rating validation callback not found for component '.  clean_param($component, PARAM_ALPHANUMEXT));
@@ -996,7 +1010,7 @@ class rating_manager {
     public function initialise_rating_javascript(moodle_page $page) {
         global $CFG;
 
-        //only needs to be initialized once
+        // Only needs to be initialized once.
         static $done = false;
         if ($done) {
             return true;
@@ -1037,10 +1051,10 @@ class rating_manager {
         return $aggregatelabel;
     }
 
-}//end rating_manager class definition
+} // End rating_manager class definition.
 
 /**
- * The rating_exception class provides the ability to generate exceptions that can be easily identified as coming from the ratings system
+ * The rating_exception class for exceptions specific to the ratings system
  *
  * @package   core_rating
  * @category  rating
@@ -1058,7 +1072,7 @@ class rating_exception extends moodle_exception {
      *
      * @param string $errorcode the error code to generate
      */
-    function __construct($errorcode) {
+    public function __construct($errorcode) {
         $this->errorcode = $errorcode;
         $this->message = get_string($errorcode, 'error');
     }
index f577f9e..00b4bb4 100644 (file)
@@ -1,7 +1,7 @@
-M.core_rating={
+M.core_rating = {
 
     Y : null,
-    api: M.cfg.wwwroot+'/rating/rate_ajax.php',
+    api: M.cfg.wwwroot + '/rating/rate_ajax.php',
 
     init : function(Y){
         this.Y = Y;
@@ -20,9 +20,8 @@ M.core_rating={
         var thedata = [];
 
         var inputssize = theinputs.size();
-        for ( var i=0; i<inputssize; i++ )
-        {
-            if(theinputs.item(i).get("name")!="returnurl") {//dont include return url for ajax requests
+        for (var i = 0; i < inputssize; i++) {
+            if(theinputs.item(i).get("name") != "returnurl") { // Dont include return url for ajax requests.
                 thedata[theinputs.item(i).get("name")] = theinputs.item(i).get("value");
             }
         }
@@ -44,24 +43,24 @@ M.core_rating={
                             if (data.itemid) { //do not test data.aggregate or data.count otherwise it doesn't refresh value=0 or no value
                                 var itemid = data.itemid;
 
-                                var node = scope.Y.one('#ratingaggregate'+itemid);
+                                var node = scope.Y.one('#ratingaggregate' + itemid);
                                 node.set('innerHTML',data.aggregate);
 
-                                //empty the count value if no ratings
-                                var node = scope.Y.one('#ratingcount'+itemid);
+                                // Empty the count value if no ratings.
+                                var node = scope.Y.one('#ratingcount' + itemid);
                                 if (data.count > 0) {
-                                    node.set('innerHTML',"("+data.count+")");
+                                    node.set('innerHTML', "(" + data.count + ")");
                                 } else {
-                                    node.set('innerHTML',"");
+                                    node.set('innerHTML', "");
                                 }
                             }
                             return true;
                         }
-                        else if (data.error){
+                        else if (data.error) {
                             alert(data.error);
                         }
                     } catch(e) {
-                        alert(e.message+" "+outcome.responseText);
+                        alert(e.message + " " + outcome.responseText);
                     }
                     return false;
                 }
@@ -76,4 +75,4 @@ M.core_rating={
         this.Y.io(this.api, cfg);
 
     }
-};
\ No newline at end of file
+};
index 6d59563..69ebd97 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -35,19 +34,19 @@ $ratingarea  = required_param('ratingarea', PARAM_AREA);
 $itemid      = required_param('itemid', PARAM_INT);
 $scaleid     = required_param('scaleid', PARAM_INT);
 $userrating  = required_param('rating', PARAM_INT);
-$rateduserid = required_param('rateduserid', PARAM_INT);//which user is being rated. Required to update their grade
-$returnurl   = required_param('returnurl', PARAM_LOCALURL);//required for non-ajax requests
+$rateduserid = required_param('rateduserid', PARAM_INT); // Which user is being rated. Required to update their grade.
+$returnurl   = required_param('returnurl', PARAM_LOCALURL); // Required for non-ajax requests.
 
 $result = new stdClass;
 
 list($context, $course, $cm) = get_context_info_array($contextid);
 require_login($course, false, $cm);
 
-$contextid = null;//now we have a context object throw away the id from the user
+$contextid = null; // Now we have a context object, throw away the id from the user.
 $PAGE->set_context($context);
 $PAGE->set_url('/rating/rate.php', array('contextid' => $context->id));
 
-if (!confirm_sesskey() || !has_capability('moodle/rating:rate',$context)) {
+if (!confirm_sesskey() || !has_capability('moodle/rating:rate', $context)) {
     echo $OUTPUT->header();
     echo get_string('ratepermissiondenied', 'rating');
     echo $OUTPUT->footer();
@@ -56,8 +55,8 @@ if (!confirm_sesskey() || !has_capability('moodle/rating:rate',$context)) {
 
 $rm = new rating_manager();
 
-//check the module rating permissions
-//doing this check here rather than within rating_manager::get_ratings() so we can return a json error response
+// Check the module rating permissions.
+// Doing this check here rather than within rating_manager::get_ratings() so we can return a json error response.
 $pluginpermissionsarray = $rm->get_plugin_permissions_array($context->id, $component, $ratingarea);
 
 if (!$pluginpermissionsarray['rate']) {
@@ -93,7 +92,7 @@ if ($userrating != RATING_UNSET_RATING) {
 
     $rating = new rating($ratingoptions);
     $rating->update_rating($userrating);
-} else { //delete the rating if the user set to Rate...
+} else { // Delete the rating if the user set to "Rate..."
     $options = new stdClass;
     $options->contextid = $context->id;
     $options->component = $component;
@@ -104,12 +103,10 @@ if ($userrating != RATING_UNSET_RATING) {
     $rm->delete_ratings($options);
 }
 
-//todo add a setting to turn grade updating off for those who don't want them in gradebook
-//note that this needs to be done in both rate.php and rate_ajax.php
 if (!empty($cm) && $context->contextlevel == CONTEXT_MODULE) {
-    //tell the module that its grades have changed
+    // Tell the module that its grades have changed.
     $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
-    $modinstance->cmidnumber = $cm->id; //MDL-12961
+    $modinstance->cmidnumber = $cm->id; // MDL-12961.
     $functionname = $cm->modname.'_update_grades';
     require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php");
     if (function_exists($functionname)) {
@@ -117,4 +114,4 @@ if (!empty($cm) && $context->contextlevel == CONTEXT_MODULE) {
     }
 }
 
-redirect($returnurl);
\ No newline at end of file
+redirect($returnurl);
index 204db6b..f56c03d 100644 (file)
@@ -1,5 +1,4 @@
 <?php
-
 // This file is part of Moodle - http://moodle.org/
 //
 // Moodle is free software: you can redistribute it and/or modify
@@ -16,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
-* This page receives ajax rating submissions
+ * This page receives ajax rating submissions
  *
  * It is similar to rate.php. Unlike rate.php a return url is NOT required.
  *
@@ -37,13 +36,13 @@ $ratingarea        = required_param('ratingarea', PARAM_AREA);
 $itemid            = required_param('itemid', PARAM_INT);
 $scaleid           = required_param('scaleid', PARAM_INT);
 $userrating        = required_param('rating', PARAM_INT);
-$rateduserid       = required_param('rateduserid', PARAM_INT);//which user is being rated. Required to update their grade
-$aggregationmethod = optional_param('aggregation', RATING_AGGREGATE_NONE, PARAM_INT);//we're going to calculate the aggregate and return it to the client
+$rateduserid       = required_param('rateduserid', PARAM_INT); // The user being rated. Required to update their grade.
+$aggregationmethod = optional_param('aggregation', RATING_AGGREGATE_NONE, PARAM_INT); // Used to calculate the aggregate to return.
 
 $result = new stdClass;
 
-//if session has expired and its an ajax request so we cant do a page redirect
-if( !isloggedin() ){
+// If session has expired and its an ajax request so we cant do a page redirect.
+if (!isloggedin()) {
     $result->error = get_string('sessionerroruser', 'error');
     echo json_encode($result);
     die();
@@ -52,11 +51,11 @@ if( !isloggedin() ){
 list($context, $course, $cm) = get_context_info_array($contextid);
 require_login($course, false, $cm);
 
-$contextid = null;//now we have a context object throw away the id from the user
+$contextid = null; // Now we have a context object, throw away the id from the user.
 $PAGE->set_context($context);
-$PAGE->set_url('/rating/rate_ajax.php', array('contextid'=>$context->id));
+$PAGE->set_url('/rating/rate_ajax.php', array('contextid' => $context->id));
 
-if (!confirm_sesskey() || !has_capability('moodle/rating:rate',$context)) {
+if (!confirm_sesskey() || !has_capability('moodle/rating:rate', $context)) {
     echo $OUTPUT->header();
     echo get_string('ratepermissiondenied', 'rating');
     echo $OUTPUT->footer();
@@ -65,8 +64,8 @@ if (!confirm_sesskey() || !has_capability('moodle/rating:rate',$context)) {
 
 $rm = new rating_manager();
 
-//check the module rating permissions
-//doing this check here rather than within rating_manager::get_ratings() so we can return a json error response
+// Check the module rating permissions.
+// Doing this check here rather than within rating_manager::get_ratings() so we can return a json error response.
 $pluginpermissionsarray = $rm->get_plugin_permissions_array($context->id, $component, $ratingarea);
 
 if (!$pluginpermissionsarray['rate']) {
@@ -91,7 +90,7 @@ if (!$pluginpermissionsarray['rate']) {
     }
 }
 
-//rating options used to update the rating then retrieve the aggregate
+// Rating options used to update the rating then retrieve the aggregate.
 $ratingoptions = new stdClass;
 $ratingoptions->context = $context;
 $ratingoptions->ratingarea = $ratingarea;
@@ -103,7 +102,7 @@ $ratingoptions->userid  = $USER->id;
 if ($userrating != RATING_UNSET_RATING) {
     $rating = new rating($ratingoptions);
     $rating->update_rating($userrating);
-} else { //delete the rating if the user set to Rate...
+} else { // Delete the rating if the user set to "Rate..."
     $options = new stdClass;
     $options->contextid = $context->id;
     $options->component = $component;
@@ -114,13 +113,13 @@ if ($userrating != RATING_UNSET_RATING) {
     $rm->delete_ratings($options);
 }
 
-// Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook
-// note that this would need to be done in both rate.php and rate_ajax.php
+// Future possible enhancement: add a setting to turn grade updating off for those who don't want them in gradebook.
+// Note that this would need to be done in both rate.php and rate_ajax.php.
 if ($context->contextlevel == CONTEXT_MODULE) {
-    //tell the module that its grades have changed
+    // Tell the module that its grades have changed.
     $modinstance = $DB->get_record($cm->modname, array('id' => $cm->instance));
     if ($modinstance) {
-        $modinstance->cmidnumber = $cm->id; //MDL-12961
+        $modinstance->cmidnumber = $cm->id; // MDL-12961.
         $functionname = $cm->modname.'_update_grades';
         require_once($CFG->dirroot."/mod/{$cm->modname}/lib.php");
         if (function_exists($functionname)) {
@@ -129,44 +128,44 @@ if ($context->contextlevel == CONTEXT_MODULE) {
     }
 }
 
-//object to return to client as json
+// Object to return to client as JSON.
 $result->success = true;
 
-//need to retrieve the updated item to get its new aggregate value
+// Need to retrieve the updated item to get its new aggregate value.
 $item = new stdClass;
 $item->id = $itemid;
 
-//most of $ratingoptions variables were previously set
+// Most of $ratingoptions variables were previously set.
 $ratingoptions->items = array($item);
 $ratingoptions->aggregate = $aggregationmethod;
 
 $items = $rm->get_ratings($ratingoptions);
 $firstrating = $items[0]->rating;
 
-//for custom scales return text not the value
-//this scales weirdness will go away when scales are refactored
+// For custom scales return text not the value.
+// This scales weirdness will go away when scales are refactored.
 $scalearray = null;
 $aggregatetoreturn = round($firstrating->aggregate, 1);
 
-// Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway
+// Output a dash if aggregation method == COUNT as the count is output next to the aggregate anyway.
 if ($firstrating->settings->aggregationmethod == RATING_AGGREGATE_COUNT or $firstrating->count == 0) {
     $aggregatetoreturn = ' - ';
-} else if ($firstrating->settings->scale->id < 0) { //if its non-numeric scale
-    //dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense
+} else if ($firstrating->settings->scale->id < 0) { // If its non-numeric scale.
+    // Dont use the scale item if the aggregation method is sum as adding items from a custom scale makes no sense.
     if ($firstrating->settings->aggregationmethod != RATING_AGGREGATE_SUM) {
         $scalerecord = $DB->get_record('scale', array('id' => -$firstrating->settings->scale->id));
         if ($scalerecord) {
             $scalearray = explode(',', $scalerecord->scale);
-            $aggregatetoreturn = $scalearray[$aggregatetoreturn-1];
+            $aggregatetoreturn = $scalearray[$aggregatetoreturn - 1];
         }
     }
 }
 
-//See if the user has permission to see the rating aggregate
+// See if the user has permission to see the rating aggregate.
 if ($firstrating->user_can_view_aggregate()) {
     $result->aggregate = $aggregatetoreturn;
     $result->count = $firstrating->count;
     $result->itemid = $itemid;
 }
 
-echo json_encode($result);
\ No newline at end of file
+echo json_encode($result);
index 8402989..b725c72 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-// Include all the needed stuff
+// Include all the needed stuff.
 global $CFG;
 require_once($CFG->dirroot . '/rating/lib.php');
 
@@ -51,38 +51,90 @@ class core_rating_testcase extends advanced_testcase {
     /**
      * Test the current get_ratings method main sql
      */
-    function test_get_ratings_sql() {
+    public function test_get_ratings_sql() {
         global $DB;
 
-        // We load 3 items. Each is rated twice. For simplicity itemid == user id of the item owner
+        // We load 3 items. Each is rated twice. For simplicity itemid == user id of the item owner.
         $ctxid = context_system::instance()->id;
         $ratings = array(
-            //user 1's items. Average == 2
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>1 ,'scaleid'=>10 ,'rating'=>1 ,'userid'=>2 ,'timecreated'=>1 ,'timemodified'=>1),
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>1 ,'scaleid'=>10 ,'rating'=>3 ,'userid'=>3 ,'timecreated'=>1 ,'timemodified'=>1),
-            //user 2's items. Average == 3
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>2 ,'scaleid'=>10 ,'rating'=>1 ,'userid'=>1 ,'timecreated'=>1 ,'timemodified'=>1),
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>2 ,'scaleid'=>10 ,'rating'=>5 ,'userid'=>3 ,'timecreated'=>1 ,'timemodified'=>1),
-            //user 3's items. Average == 4
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>3 ,'scaleid'=>10 ,'rating'=>3 ,'userid'=>1 ,'timecreated'=>1 ,'timemodified'=>1),
-            array('contextid'=>$ctxid ,'component'=>'mod_forum','ratingarea'=>'post','itemid'=>3 ,'scaleid'=>10 ,'rating'=>5 ,'userid'=>2 ,'timecreated'=>1 ,'timemodified'=>1)
+            // User 1's items. Average == 2.
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 1,
+                  'scaleid' => 10,
+                  'rating' => 1,
+                  'userid' => 2,
+                  'timecreated' => 1,
+                  'timemodified' => 1),
+
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 1,
+                  'scaleid' => 10,
+                  'rating' => 3,
+                  'userid' => 3,
+                  'timecreated' => 1,
+                  'timemodified' => 1),
+
+            // User 2's items. Average == 3.
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 2,
+                  'scaleid' => 10,
+                  'rating' => 1,
+                  'userid' => 1,
+                  'timecreated' => 1,
+                  'timemodified' => 1),
+
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 2,
+                  'scaleid' => 10,
+                  'rating' => 5,
+                  'userid' => 3,
+                  'timecreated' => 1,
+                  'timemodified' => 1),
+
+            // User 3's items. Average == 4.
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 3,
+                  'scaleid' => 10,
+                  'rating' => 3,
+                  'userid' => 1,
+                  'timecreated' => 1,
+                  'timemodified' => 1),
+
+            array('contextid' => $ctxid,
+                  'component' => 'mod_forum',
+                  'ratingarea' => 'post',
+                  'itemid' => 3,
+                  'scaleid' => 10,
+                  'rating' => 5,
+                  'userid' => 2,
+                  'timecreated' => 1,
+                  'timemodified' => 1)
         );
         foreach ($ratings as $rating) {
             $DB->insert_record('rating', $rating);
         }
 
-
-        // a post (item) by user 1 (rated above by user 2 and 3 with average = 2)
+        // A post (item) by user 1 (rated above by user 2 and 3 with average = 2).
         $user1posts = array(
             (object)array('id' => 1, 'userid' => 1, 'message' => 'hello'));
-        // a post (item) by user 2 (rated above by user 1 and 3 with average = 3)
+        // A post (item) by user 2 (rated above by user 1 and 3 with average = 3).
         $user2posts = array(
             (object)array('id' => 2, 'userid' => 2, 'message' => 'world'));
-        // a post (item) by user 3 (rated above by user 1 and 2 with average = 4)
+        // A post (item) by user 3 (rated above by user 1 and 2 with average = 4).
         $user3posts = array(
             (object)array('id' => 3, 'userid' => 3, 'message' => 'moodle'));
 
-        // Prepare the default options
+        // Prepare the default options.
         $defaultoptions = array (
             'context'    => context_system::instance(),
             'component'  => 'mod_forum',
@@ -92,9 +144,9 @@ class core_rating_testcase extends advanced_testcase {
 
         $rm = new mockup_rating_manager();
 
-        // STEP 1: Retreive ratings using the current user
+        // STEP 1: Retreive ratings using the current user.
 
-        // Get results for user 1's item (expected average 1 + 3 / 2 = 2)
+        // Get results for user 1's item (expected average 1 + 3 / 2 = 2).
         $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts));
         $result = $rm->get_ratings($toptions);
         $this->assertEquals(count($result), count($user1posts));
@@ -103,10 +155,10 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $user1posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 2);
-        // Note that $result[0]->rating->rating is somewhat random
-        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests
+        // Note that $result[0]->rating->rating is somewhat random.
+        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests.
 
-        // Get results for items of user 2 (expected average 1 + 5 / 2 = 3)
+        // Get results for items of user 2 (expected average 1 + 5 / 2 = 3).
         $toptions = (object)array_merge($defaultoptions, array('items' => $user2posts));
         $result = $rm->get_ratings($toptions);
         $this->assertEquals(count($result), count($user2posts));
@@ -115,10 +167,10 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $user2posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 3);
-        // Note that $result[0]->rating->rating is somewhat random
-        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests
+        // Note that $result[0]->rating->rating is somewhat random.
+        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests.
 
-        // Get results for items of user 3 (expected average 3 + 5 / 2 = 4)
+        // Get results for items of user 3 (expected average 3 + 5 / 2 = 4).
         $toptions = (object)array_merge($defaultoptions, array('items' => $user3posts));
         $result = $rm->get_ratings($toptions);
         $this->assertEquals(count($result), count($user3posts));
@@ -127,10 +179,10 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $user3posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 4);
-        // Note that $result[0]->rating->rating is somewhat random
-        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests
+        // Note that $result[0]->rating->rating is somewhat random.
+        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests.
 
-        // Get results for items of user 1 & 2 together (expected averages are 2 and 3, as tested above)
+        // Get results for items of user 1 & 2 together (expected averages are 2 and 3, as tested above).
         $posts = array_merge($user1posts, $user2posts);
         $toptions = (object)array_merge($defaultoptions, array('items' => $posts));
         $result = $rm->get_ratings($toptions);
@@ -140,21 +192,21 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 2);
-        // Note that $result[0]->rating->rating is somewhat random
-        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests
+        // Note that $result[0]->rating->rating is somewhat random.
+        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests.
 
         $this->assertEquals($result[1]->id, $posts[1]->id);
         $this->assertEquals($result[1]->userid, $posts[1]->userid);
         $this->assertEquals($result[1]->message, $posts[1]->message);
         $this->assertEquals($result[1]->rating->count, 2);
         $this->assertEquals($result[1]->rating->aggregate, 3);
-        // Note that $result[0]->rating->rating is somewhat random
-        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests
+        // Note that $result[0]->rating->rating is somewhat random.
+        // We didn't supply a user ID so $USER was used which will vary depending on who runs the tests.
 
-        // STEP 2: Retrieve ratings by a specified user
-        //         We still expect complete aggregations and counts
+        // STEP 2: Retrieve ratings by a specified user.
+        //         We still expect complete aggregations and counts.
 
-        // Get results for items of user 1 rated by user 2 (avg 2, rating 1)
+        // Get results for items of user 1 rated by user 2 (avg 2, rating 1).
         $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts, 'userid' => 2));
         $result = $rm->get_ratings($toptions);
         $this->assertEquals(count($result), count($user1posts));
@@ -163,10 +215,10 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $user1posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 2);
-        $this->assertEquals($result[0]->rating->rating, 1); //user 2 rated user 1 "1"
-        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid
+        $this->assertEquals($result[0]->rating->rating, 1); // User 2 rated user 1 "1".
+        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid.
 
-        // Get results for items of user 1 rated by user 3
+        // Get results for items of user 1 rated by user 3.
         $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts, 'userid' => 3));
         $result = $rm->get_ratings($toptions);
         $this->assertEquals(count($result), count($user1posts));
@@ -175,10 +227,10 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $user1posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 2);
-        $this->assertEquals($result[0]->rating->rating, 3); //user 3 rated user 1 "3"
-        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid
+        $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 1 "3".
+        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid.
 
-        // Get results for items of user 1 & 2 together rated by user 3
+        // Get results for items of user 1 & 2 together rated by user 3.
         $posts = array_merge($user1posts, $user2posts);
         $toptions = (object)array_merge($defaultoptions, array('items' => $posts, 'userid' => 3));
         $result = $rm->get_ratings($toptions);
@@ -188,37 +240,37 @@ class core_rating_testcase extends advanced_testcase {
         $this->assertEquals($result[0]->message, $posts[0]->message);
         $this->assertEquals($result[0]->rating->count, 2);
         $this->assertEquals($result[0]->rating->aggregate, 2);
-        $this->assertEquals($result[0]->rating->rating, 3); //user 3 rated user 1 "3"
-        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid
+        $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 1 "3".
+        $this->assertEquals($result[0]->rating->userid, $toptions->userid); // Must be the passed userid.
 
         $this->assertEquals($result[1]->id, $posts[1]->id);
         $this->assertEquals($result[1]->userid, $posts[1]->userid);
         $this->assertEquals($result[1]->message, $posts[1]->message);
         $this->assertEquals($result[1]->rating->count, 2);
         $this->assertEquals($result[1]->rating->aggregate, 3);
-        $this->assertEquals($result[0]->rating->rating, 3); //user 3 rated user 2 "5"
-        $this->assertEquals($result[1]->rating->userid, $toptions->userid); // Must be the passed userid
+        $this->assertEquals($result[0]->rating->rating, 3); // User 3 rated user 2 "5".
+        $this->assertEquals($result[1]->rating->userid, $toptions->userid); // Must be the passed userid.
 
-        // STEP 3: Some special cases
+        // STEP 3: Some special cases.
 
-        // Get results for user 1's items (expected average 1 + 3 / 2 = 2)
-        // supplying a non-existent user id so no rating from that user should be found
+        // Get results for user 1's items (expected average 1 + 3 / 2 = 2).
+        // Supplying a non-existent user id so no rating from that user should be found.
         $toptions = (object)array_merge($defaultoptions, array('items' => $user1posts));
-        $toptions->userid = 123456; //non-existent user
+        $toptions->userid = 123456; // Non-existent user.
         $result = $rm->get_ratings($toptions);
         $this->assertNull($result[0]->rating->userid);
         $this->assertNull($result[0]->rating->rating);
-        $this->assertEquals($result[0]->rating->aggregate, 2);//should still get the aggregate
+        $this->assertEquals($result[0]->rating->aggregate, 2); // Should still get the aggregate.
 
-        // Get results for items of user 2 (expected average 1 + 5 / 2 = 3)
-        // Supplying the user id of the user who owns the items so no rating should be found
+        // Get results for items of user 2 (expected average 1 + 5 / 2 = 3).
+        // Supplying the user id of the user who owns the items so no rating should be found.
         $toptions = (object)array_merge($defaultoptions, array('items' => $user2posts));
-        $toptions->userid = 2; //user 2 viewing the ratings of their own item
+        $toptions->userid = 2; // User 2 viewing the ratings of their own item.
         $result = $rm->get_ratings($toptions);
-        //these should be null as the user is viewing their own item and thus cannot rate
+        // These should be null as the user is viewing their own item and thus cannot rate.
         $this->assertNull($result[0]->rating->userid);
         $this->assertNull($result[0]->rating->rating);
-        $this->assertEquals($result[0]->rating->aggregate, 3);//should still get the aggregate
+        $this->assertEquals($result[0]->rating->aggregate, 3); // Should still get the aggregate.
     }
 }
 
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 24daf4f..7b298e8 100644 (file)
@@ -1119,16 +1119,6 @@ class core_user_external extends external_api {
             return $warnings;
         }
 
-        // The same key can't exists for the same platform.
-        if ($DB->get_record('user_devices', array('pushid' => $params['pushid'], 'platform' => $params['platform']))) {
-            $warnings['warning'][] = array(
-                'item' => $params['pushid'],
-                'warningcode' => 'existingkeyforplatform',
-                'message' => 'This key is already stored for other device using the same platform'
-            );
-            return $warnings;
-        }
-
         $userdevice = new stdclass;
         $userdevice->userid     = $USER->id;
         $userdevice->appid      = $params['appid'];
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();
index 3e723cb..ec41150 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2014072400.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2014072400.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.