Merge branch 'm28_MDL-46431' of https://github.com/totara/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 28 Jul 2014 11:01:24 +0000 (12:01 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 28 Jul 2014 11:01:24 +0000 (12:01 +0100)
134 files changed:
admin/index.php
admin/renderer.php
backup/moodle2/restore_stepslib.php
backup/util/helper/restore_logs_processor.class.php
backup/util/ui/backup_ui_setting.class.php
blocks/login/block_login.php
blog/lib.php
cache/admin.php
cache/classes/helper.php
cache/classes/store.php
cache/locallib.php
cache/renderer.php
cache/stores/memcache/lang/en/cachestore_memcache.php
cache/stores/memcache/lib.php
cache/stores/memcached/lang/en/cachestore_memcached.php
cache/stores/memcached/lib.php
course/classes/management/helper.php
course/classes/management_renderer.php
course/lib.php
course/management.php
course/renderer.php
course/tests/behat/category_resort.feature
course/tests/behat/course_category_management_listing.feature
course/tests/behat/course_resort.feature
enrol/category/db/access.php
enrol/category/lang/en/enrol_category.php
enrol/category/lib.php
enrol/category/version.php
enrol/cohort/lib.php
enrol/cohort/version.php
enrol/cohort/yui/quickenrolment/quickenrolment.js
enrol/database/db/access.php
enrol/database/lang/en/enrol_database.php
enrol/database/lib.php
enrol/database/version.php
enrol/flatfile/lib.php
enrol/flatfile/version.php
enrol/guest/lib.php
enrol/guest/version.php
enrol/imsenterprise/db/access.php [new file with mode: 0644]
enrol/imsenterprise/lang/en/enrol_imsenterprise.php
enrol/imsenterprise/lib.php
enrol/imsenterprise/version.php
enrol/instances.php
enrol/ldap/lib.php
enrol/ldap/version.php
enrol/manual/lib.php
enrol/manual/version.php
enrol/meta/lib.php
enrol/meta/version.php
enrol/mnet/db/access.php [new file with mode: 0644]
enrol/mnet/lang/en/enrol_mnet.php
enrol/mnet/lib.php
enrol/mnet/version.php
enrol/paypal/lib.php
enrol/paypal/version.php
enrol/self/lib.php
enrol/self/version.php
enrol/upgrade.txt
filter/tex/filter.php
filter/tex/lang/en/filter_tex.php
filter/tex/latex.php
filter/tex/lib.php
filter/tex/pix.php
filter/tex/settings.php
filter/tex/texdebug.php
install/lang/ru/install.php
install/lang/te/install.php
install/lang/uk/admin.php
install/lang/vi/admin.php
lang/en/moodle.php
lib/classes/collator.php
lib/classes/grades_external.php
lib/classes/task/manager.php
lib/classes/task/scheduled_task.php
lib/coursecatlib.php
lib/csvlib.class.php
lib/enrollib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/moodlelib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/tests/collator_test.php
lib/tests/coursecatlib_test.php
lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js
lib/yui/build/moodle-core-dock/moodle-core-dock-min.js
lib/yui/build/moodle-core-dock/moodle-core-dock.js
lib/yui/src/dock/js/dock.js
login/index_form.html
mod/assign/backup/moodle2/backup_assign_stepslib.php
mod/assign/backup/moodle2/restore_assign_stepslib.php
mod/assign/db/access.php
mod/assign/lang/en/assign.php
mod/assign/locallib.php
mod/assign/mod_form.php
mod/assign/renderable.php
mod/assign/renderer.php
mod/assign/tests/base_test.php
mod/assign/tests/locallib_test.php
mod/assign/version.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
notes/delete.php
notes/edit.php
notes/edit_form.php
notes/externallib.php
notes/index.php
notes/lib.php
question/qengine.js
rss/file.php
rss/index.html [deleted file]
rss/renderer.php
theme/base/style/core.css
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/style/moodle.css
user/filters/lib.php
user/profile.php
user/view.php
version.php

index aaa530a..34c0f95 100644 (file)
@@ -589,10 +589,14 @@ $availableupdatesfetch = $updateschecker->get_last_timefetched();
 $buggyiconvnomb = (!function_exists('mb_convert_encoding') and @iconv('UTF-8', 'UTF-8//IGNORE', '100'.chr(130).'€') !== '100€');
 //check if the site is registered on Moodle.org
 $registered = $DB->count_records('registration_hubs', array('huburl' => HUB_MOODLEORGHUBURL, 'confirmed' => 1));
+// Check if there are any cache warnings.
+$cachewarnings = cache_helper::warnings();
 
 admin_externalpage_setup('adminnotifications');
 
+/* @var core_admin_renderer $output */
 $output = $PAGE->get_renderer('core', 'admin');
-echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
-        $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
-        $registered);
+
+echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
+                                       $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
+                                       $registered, $cachewarnings);
index 954b977..99109a2 100644 (file)
@@ -303,12 +303,13 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $buggyiconvnomb warn iconv problems
      * @param array|null $availableupdates array of \core\update\info objects or null
      * @param int|null $availableupdatesfetch timestamp of the most recent updates fetch or null (unknown)
+     * @param string[] $cachewarnings An array containing warnings from the Cache API.
      *
      * @return string HTML to output.
      */
     public function admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed,
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
-            $buggyiconvnomb, $registered) {
+            $buggyiconvnomb, $registered, array $cachewarnings = array()) {
         global $CFG;
         $output = '';
 
@@ -321,6 +322,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->cron_overdue_warning($cronoverdue);
         $output .= $this->db_problems($dbproblems);
         $output .= $this->maintenance_mode_warning($maintenancemode);
+        $output .= $this->cache_warnings($cachewarnings);
         $output .= $this->registration_warning($registered);
 
         //////////////////////////////////////////////////////////////////////////////////////////////////
@@ -595,6 +597,19 @@ class core_admin_renderer extends plugin_renderer_base {
         return $this->warning($dbproblems);
     }
 
+    /**
+     * Renders cache warnings if there are any.
+     *
+     * @param string[] $cachewarnings
+     * @return string
+     */
+    public function cache_warnings(array $cachewarnings) {
+        if (!count($cachewarnings)) {
+            return '';
+        }
+        return join("\n", array_map(array($this, 'warning'), $cachewarnings));
+    }
+
     /**
      * Render an appropriate message if the site in in maintenance mode.
      * @param bool $maintenancemode
index 6e7a461..d0036cf 100644 (file)
@@ -2609,7 +2609,18 @@ class restore_course_logs_structure_step extends restore_structure_step {
 
         // If we have data, insert it, else something went wrong in the restore_logs_processor
         if ($data) {
-            $DB->insert_record('log', $data);
+            if (empty($data->url)) {
+                $data->url = '';
+            }
+            if (empty($data->info)) {
+                $data->info = '';
+            }
+            // Store the data in the legacy log table if we are still using it.
+            $manager = get_log_manager();
+            if (method_exists($manager, 'legacy_add_to_log')) {
+                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
+                    $data->info, $data->cmid, $data->userid);
+            }
         }
     }
 }
@@ -2647,7 +2658,18 @@ class restore_activity_logs_structure_step extends restore_course_logs_structure
 
         // If we have data, insert it, else something went wrong in the restore_logs_processor
         if ($data) {
-            $DB->insert_record('log', $data);
+            if (empty($data->url)) {
+                $data->url = '';
+            }
+            if (empty($data->info)) {
+                $data->info = '';
+            }
+            // Store the data in the legacy log table if we are still using it.
+            $manager = get_log_manager();
+            if (method_exists($manager, 'legacy_add_to_log')) {
+                $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
+                    $data->info, $data->cmid, $data->userid);
+            }
         }
     }
 }
index 63ff801..585d9b4 100644 (file)
@@ -89,11 +89,11 @@ class restore_logs_processor {
             }
             // Arrived here log is empty, no rule was able to perform the conversion, log the problem
             if (empty($newlog)) {
-                self::$task->log('Log module-action "' . $keyname . ' process problem. Not restored', backup::LOG_DEBUG);
+                self::$task->log('Log module-action "' . $keyname . '" process problem. Not restored', backup::LOG_DEBUG);
             }
 
         } else { // Action not found log the problem
-            self::$task->log('Log module-action "' . $keyname . ' unknown. Not restored', backup::LOG_DEBUG);
+            self::$task->log('Log module-action "' . $keyname . '" unknown. Not restored', backup::LOG_DEBUG);
             $newlog = false;
 
         }
index 9ef5865..1712a73 100644 (file)
@@ -139,7 +139,8 @@ class base_setting_ui {
      * @param string $label
      */
     public function set_label($label) {
-        if ((string)$label === '' || $label !== clean_param($label, PARAM_TEXT)) {
+        $label = (string)$label;
+        if ($label === '' || $label !== clean_param($label, PARAM_TEXT)) {
             throw new base_setting_ui_exception('setting_invalid_ui_label');
         }
         $this->label = $label;
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 c9c35db..775ce25 100644 (file)
@@ -52,7 +52,7 @@ $locks = cache_administration_helper::get_lock_summaries();
 
 $title = new lang_string('cacheadmin', 'cache');
 $mform = null;
-$notification = null;
+$notifications = array();
 $notifysuccess = true;
 
 if (!empty($action) && confirm_sesskey()) {
@@ -110,10 +110,10 @@ if (!empty($action) && confirm_sesskey()) {
 
             if (!array_key_exists($store, $stores)) {
                 $notifysuccess = false;
-                $notification = get_string('invalidstore', 'cache');
+                $notifications[] = array(get_string('invalidstore', 'cache'), false);
             } else if ($stores[$store]['mappings'] > 0) {
                 $notifysuccess = false;
-                $notification = get_string('deletestorehasmappings', 'cache');
+                $notifications[] = array(get_string('deletestorehasmappings', 'cache'), false);
             }
 
             if ($notifysuccess) {
@@ -250,10 +250,10 @@ if (!empty($action) && confirm_sesskey()) {
             $confirm = optional_param('confirm', false, PARAM_BOOL);
             if (!array_key_exists($lock, $locks)) {
                 $notifysuccess = false;
-                $notification = get_string('invalidlock', 'cache');
+                $notifications[] = array(get_string('invalidlock', 'cache'), false);
             } else if ($locks[$lock]['uses'] > 0) {
                 $notifysuccess = false;
-                $notification = get_string('deletelockhasuses', 'cache');
+                $notifications[] = array(get_string('deletelockhasuses', 'cache'), false);
             }
             if ($notifysuccess) {
                 if (!$confirm) {
@@ -280,6 +280,12 @@ if (!empty($action) && confirm_sesskey()) {
     }
 }
 
+// Add cache store warnings to the list of notifications.
+// Obviously as these are warnings they are show as failures.
+foreach (cache_helper::warnings($stores) as $warning) {
+    $notifications[] = array($warning, false);
+}
+
 $PAGE->set_title($title);
 $PAGE->set_heading($SITE->fullname);
 /* @var core_cache_renderer $renderer */
@@ -287,10 +293,7 @@ $renderer = $PAGE->get_renderer('core_cache');
 
 echo $renderer->header();
 echo $renderer->heading($title);
-
-if (!is_null($notification)) {
-    echo $renderer->notification($notification, ($notifysuccess)?'notifysuccess' : 'notifyproblem');
-}
+echo $renderer->notifications($notifications);
 
 if ($mform instanceof moodleform) {
     $mform->display();
index 02bd243..dd150b7 100644 (file)
@@ -736,4 +736,28 @@ class cache_helper {
         }
         return $stores;
     }
+
+    /**
+     * Returns an array of warnings from the cache API.
+     *
+     * The warning returned here are for things like conflicting store instance configurations etc.
+     * These get shown on the admin notifications page for example.
+     *
+     * @param array|null $stores An array of stores to get warnings for, or null for all.
+     * @return string[]
+     */
+    public static function warnings(array $stores = null) {
+        global $CFG;
+        if ($stores === null) {
+            require_once($CFG->dirroot.'/cache/locallib.php');
+            $stores = cache_administration_helper::get_store_instance_summaries();
+        }
+        $warnings = array();
+        foreach ($stores as $store) {
+            if (!empty($store['warnings'])) {
+                $warnings = array_merge($warnings, $store['warnings']);
+            }
+        }
+        return $warnings;
+    }
 }
index b5cfcc1..9d124d3 100644 (file)
@@ -365,4 +365,16 @@ abstract class cache_store implements cache_store_interface {
     public static function initialise_unit_test_instance(cache_definition $definition) {
         return static::initialise_test_instance($definition);
     }
+
+    /**
+     * Can be overridden to return any warnings this store instance should make to the admin.
+     *
+     * This should be used to notify things like configuration conflicts etc.
+     * The warnings returned here will be displayed on the cache configuration screen.
+     *
+     * @return string[] An array of warning strings from the store instance.
+     */
+    public function get_warnings() {
+        return array();
+    }
 }
index 042b891..58dcdd8 100644 (file)
@@ -709,7 +709,8 @@ abstract class cache_administration_helper extends cache_helper {
                     'nativelocking' => ($store instanceof cache_is_lockable),
                     'keyawareness' => ($store instanceof cache_is_key_aware),
                     'searchable' => ($store instanceof cache_is_searchable)
-                )
+                ),
+                'warnings' => $store->get_warnings()
             );
             if (empty($details['default'])) {
                 $return[$name] = $record;
index 685b838..1ccb4bb 100644 (file)
@@ -235,6 +235,8 @@ class core_cache_renderer extends plugin_renderer_base {
         );
         $table->data = array();
 
+        core_collator::asort_array_of_arrays_by_key($definitions, 'name');
+
         $none = new lang_string('none', 'cache');
         foreach ($definitions as $id => $definition) {
             $actions = cache_administration_helper::get_definition_actions($context, $definition);
@@ -372,4 +374,30 @@ class core_cache_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('div');
         return $html;
     }
+
+    /**
+     * Renders an array of notifications for the cache configuration screen.
+     *
+     * Takes an array of notifications with the form:
+     * $notifications = array(
+     *     array('This is a success message', true),
+     *     array('This is a failure message', false),
+     * );
+     *
+     * @param array $notifications
+     * @return string
+     */
+    public function notifications(array $notifications = array()) {
+        if (count($notifications) === 0) {
+            // There are no notifications to render.
+            return '';
+        }
+        $html = html_writer::start_div('notifications');
+        foreach ($notifications as $notification) {
+            list($message, $notifysuccess) = $notification;
+            $html .= $this->notification($message, ($notifysuccess) ? 'notifysuccess' : 'notifyproblem');
+        }
+        $html .= html_writer::end_div();
+        return $html;
+    }
 }
\ No newline at end of file
index 78d3947..26f1c9e 100644 (file)
@@ -64,6 +64,7 @@ For example:
 server.url.com
 ipaddress:port
 </pre>';
+$string['sessionhandlerconflict'] = 'Warning: A memcache instance ({$a}) has being configured to use the same memcached server as sessions. Purging all caches will lead to sessions also being purged.';
 $string['testservers'] = 'Test servers';
 $string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
 If no port is provided then the default port (11211) is used.';
\ No newline at end of file
index 3a7da5f..aa5c15d 100644 (file)
@@ -573,4 +573,28 @@ class cachestore_memcache extends cache_store implements cache_is_configurable {
     public function my_name() {
         return $this->name;
     }
+
+    /**
+     * Used to notify of configuration conflicts.
+     *
+     * The warnings returned here will be displayed on the cache configuration screen.
+     *
+     * @return string[] Returns an array of warnings (strings)
+     */
+    public function get_warnings() {
+        global $CFG;
+        $warnings = array();
+        if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
+            $bits = explode(':', $CFG->session_memcached_save_path, 3);
+            $host = array_shift($bits);
+            $port = (count($bits)) ? array_shift($bits) : '11211';
+            foreach ($this->servers as $server) {
+                if ($server[0] === $host && $server[1] == $port) {
+                    $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcache', $this->my_name());
+                    break;
+                }
+            }
+        }
+        return $warnings;
+    }
 }
index 5d35c1c..97fe5b6 100644 (file)
@@ -78,6 +78,7 @@ For example:
 server.url.com
 ipaddress:port
 </pre>';
+$string['sessionhandlerconflict'] = 'Warning: A memcached instance ({$a}) has being configured to use the same memcached server as sessions. Purging all caches will lead to sessions also being purged.';
 $string['testservers'] = 'Test servers';
 $string['testservers_desc'] = 'The test servers get used for unit tests and for performance tests. It is entirely optional to set up test servers. Servers should be defined one per line and consist of a server address and optionally a port and weight.
 If no port is provided then the default port (11211) is used.';
index 284f3c8..b379b26 100644 (file)
@@ -668,4 +668,29 @@ class cachestore_memcached extends cache_store implements cache_is_configurable
     public function my_name() {
         return $this->name;
     }
+
+    /**
+     * Used to notify of configuration conflicts.
+     *
+     * The warnings returned here will be displayed on the cache configuration screen.
+     *
+     * @return string[] Returns an array of warnings (strings)
+     */
+    public function get_warnings() {
+        global $CFG;
+        $warnings = array();
+        if (isset($CFG->session_memcached_save_path) && count($this->servers)) {
+            $bits = explode(':', $CFG->session_memcached_save_path, 3);
+            $host = array_shift($bits);
+            $port = (count($bits)) ? array_shift($bits) : '11211';
+
+            foreach ($this->servers as $server) {
+                if ((string)$server[0] === $host && (string)$server[1] === $port) {
+                    $warnings[] = get_string('sessionhandlerconflict', 'cachestore_memcached', $this->my_name());
+                    break;
+                }
+            }
+        }
+        return $warnings;
+    }
 }
index 9a02d19..14de49b 100644 (file)
@@ -221,12 +221,22 @@ class helper {
             $actions['resortbyname'] = array(
                 'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'name')),
                 'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
-                'string' => new \lang_string('resortsubcategoriesbyname', 'moodle')
+                'string' => new \lang_string('resortsubcategoriesby', 'moodle' , get_string('categoryname'))
+            );
+            $actions['resortbynamedesc'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'namedesc')),
+                'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
+                'string' => new \lang_string('resortsubcategoriesbyreverse', 'moodle', get_string('categoryname'))
             );
             $actions['resortbyidnumber'] = array(
                 'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'idnumber')),
                 'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
-                'string' => new \lang_string('resortsubcategoriesbyidnumber', 'moodle')
+                'string' => new \lang_string('resortsubcategoriesby', 'moodle', get_string('idnumbercoursecategory'))
+            );
+            $actions['resortbyidnumberdesc'] = array(
+                'url' => new \moodle_url($baseurl, array('action' => 'resortcategories', 'resort' => 'idnumberdesc')),
+                'icon' => new \pix_icon('t/sort', new \lang_string('sort')),
+                'string' => new \lang_string('resortsubcategoriesbyreverse', 'moodle', get_string('idnumbercoursecategory'))
             );
         }
 
index 688e304..6ed18dd 100644 (file)
@@ -407,8 +407,10 @@ class core_course_management_renderer extends plugin_renderer_base {
             $form .= html_writer::div(
                 html_writer::select(
                     array(
-                        'name' => get_string('sortcategoriesbyname'),
-                        'idnumber' => get_string('sortcategoriesbyidnumber'),
+                        'name' => get_string('sortbyx', 'moodle', get_string('categoryname')),
+                        'namedesc' => get_string('sortbyxreverse', 'moodle', get_string('categoryname')),
+                        'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercoursecategory')),
+                        'idnumberdesc' => get_string('sortbyxreverse' , 'moodle' , get_string('idnumbercoursecategory')),
                         'none' => get_string('dontsortcategories')
                     ),
                     'resortcategoriesby',
@@ -420,9 +422,14 @@ class core_course_management_renderer extends plugin_renderer_base {
             $form .= html_writer::div(
                 html_writer::select(
                     array(
-                        'fullname' => get_string('sortcoursesbyfullname'),
-                        'shortname' => get_string('sortcoursesbyshortname'),
-                        'idnumber' => get_string('sortcoursesbyidnumber'),
+                        'fullname' => get_string('sortbyx', 'moodle', get_string('fullnamecourse')),
+                        'fullnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse')),
+                        'shortname' => get_string('sortbyx', 'moodle', get_string('shortnamecourse')),
+                        'shortnamedesc' => get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse')),
+                        'idnumber' => get_string('sortbyx', 'moodle', get_string('idnumbercourse')),
+                        'idnumberdesc' => get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse')),
+                        'timecreated' => get_string('sortbyx', 'moodle', get_string('timecreatedcourse')),
+                        'timecreateddesc' => get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')),
                         'none' => get_string('dontsortcourses')
                     ),
                     'resortcoursesby',
@@ -676,12 +683,38 @@ class core_course_management_renderer extends plugin_renderer_base {
             $params['sesskey'] = sesskey();
             $baseurl = new moodle_url('/course/management.php', $params);
             $fullnameurl = new moodle_url($baseurl, array('resort' => 'fullname'));
+            $fullnameurldesc = new moodle_url($baseurl, array('resort' => 'fullnamedesc'));
             $shortnameurl = new moodle_url($baseurl, array('resort' => 'shortname'));
+            $shortnameurldesc = new moodle_url($baseurl, array('resort' => 'shortnamedesc'));
             $idnumberurl = new moodle_url($baseurl, array('resort' => 'idnumber'));
+            $idnumberdescurl = new moodle_url($baseurl, array('resort' => 'idnumberdesc'));
+            $timecreatedurl = new moodle_url($baseurl, array('resort' => 'timecreated'));
+            $timecreateddescurl = new moodle_url($baseurl, array('resort' => 'timecreateddesc'));
             $menu = new action_menu(array(
-                new action_menu_link_secondary($fullnameurl, null, get_string('resortbyfullname')),
-                new action_menu_link_secondary($shortnameurl, null, get_string('resortbyshortname')),
-                new action_menu_link_secondary($idnumberurl, null, get_string('resortbyidnumber'))
+                new action_menu_link_secondary($fullnameurl,
+                                               null,
+                                               get_string('sortbyx', 'moodle', get_string('fullnamecourse'))),
+                new action_menu_link_secondary($fullnameurldesc,
+                                               null,
+                                               get_string('sortbyxreverse', 'moodle', get_string('fullnamecourse'))),
+                new action_menu_link_secondary($shortnameurl,
+                                               null,
+                                               get_string('sortbyx', 'moodle', get_string('shortnamecourse'))),
+                new action_menu_link_secondary($shortnameurldesc,
+                                               null,
+                                               get_string('sortbyxreverse', 'moodle', get_string('shortnamecourse'))),
+                new action_menu_link_secondary($idnumberurl,
+                                               null,
+                                               get_string('sortbyx', 'moodle', get_string('idnumbercourse'))),
+                new action_menu_link_secondary($idnumberdescurl,
+                                               null,
+                                               get_string('sortbyxreverse', 'moodle', get_string('idnumbercourse'))),
+                new action_menu_link_secondary($timecreatedurl,
+                                               null,
+                                               get_string('sortbyx', 'moodle', get_string('timecreatedcourse'))),
+                new action_menu_link_secondary($timecreateddescurl,
+                                               null,
+                                               get_string('sortbyxreverse', 'moodle', get_string('timecreatedcourse')))
             ));
             $menu->set_menu_trigger(get_string('resortcourses'));
             $actions[] = $this->render($menu);
@@ -1203,6 +1236,51 @@ class core_course_management_renderer extends plugin_renderer_base {
         return html_writer::span(join('', $actions), 'course-item-actions item-actions');
     }
 
+    /**
+     * Renders html to display a course search form
+     *
+     * @param string $value default value to populate the search field
+     * @param string $format display format - 'plain' (default), 'short' or 'navbar'
+     * @return string
+     */
+    public function course_search_form($value = '', $format = 'plain') {
+        static $count = 0;
+        $formid = 'coursesearch';
+        if ((++$count) > 1) {
+            $formid .= $count;
+        }
+
+        switch ($format) {
+            case 'navbar' :
+                $formid = 'coursesearchnavbar';
+                $inputid = 'navsearchbox';
+                $inputsize = 20;
+                break;
+            case 'short' :
+                $inputid = 'shortsearchbox';
+                $inputsize = 12;
+                break;
+            default :
+                $inputid = 'coursesearchbox';
+                $inputsize = 30;
+        }
+
+        $strsearchcourses = get_string("searchcourses");
+        $searchurl = new moodle_url('/course/management.php');
+
+        $output = html_writer::start_tag('form', array('id' => $formid, 'action' => $searchurl, 'method' => 'get'));
+        $output .= html_writer::start_tag('fieldset', array('class' => 'coursesearchbox invisiblefieldset'));
+        $output .= html_writer::tag('label', $strsearchcourses.': ', array('for' => $inputid));
+        $output .= html_writer::empty_tag('input', array('type' => 'text', 'id' => $inputid,
+            'size' => $inputsize, 'name' => 'search', 'value' => s($value)));
+        $output .= html_writer::empty_tag('input', array('type' => 'submit',
+            'value' => get_string('go')));
+        $output .= html_writer::end_tag('fieldset');
+        $output .= html_writer::end_tag('form');
+
+        return $output;
+    }
+
     /**
      * Creates access hidden skip to links for the displayed sections.
      *
index ffde87b..1915299 100644 (file)
@@ -2517,7 +2517,8 @@ function create_course($data, $editoroptions = NULL) {
         }
     }
 
-    $data->timecreated  = time();
+    // Check if timecreated is given.
+    $data->timecreated  = !empty($data->timecreated) ? $data->timecreated : time();
     $data->timemodified = $data->timecreated;
 
     // place at beginning of any category
index c48158d..85a2494 100644 (file)
@@ -354,10 +354,14 @@ if ($action !== false && confirm_sesskey()) {
                     // They're not sorting anything.
                     break;
                 }
-                if (!in_array($sortcategoriesby, array('idnumber', 'name'))) {
+                if (!in_array($sortcategoriesby, array('idnumber', 'idnumberdesc',
+                                                       'name', 'namedesc'))) {
                     $sortcategoriesby = false;
                 }
-                if (!in_array($sortcoursesby, array('idnumber', 'fullname', 'shortname'))) {
+                if (!in_array($sortcoursesby, array('timecreated', 'timecreateddesc',
+                                                    'idnumber', 'idnumberdesc',
+                                                    'fullname', 'fullnamedesc',
+                                                    'shortname', 'shortnamedesc'))) {
                     $sortcoursesby = false;
                 }
 
@@ -508,4 +512,6 @@ echo $renderer->grid_end();
 
 // End of the management form.
 echo $renderer->management_form_end();
+echo $renderer->course_search_form($search);
+
 echo $renderer->footer();
index 96f7359..55d242f 100644 (file)
@@ -1098,8 +1098,9 @@ class core_course_renderer extends plugin_renderer_base {
             foreach ($moduleshtml as $modnumber => $modulehtml) {
                 if ($ismoving) {
                     $movingurl = new moodle_url('/course/mod.php', array('moveto' => $modnumber, 'sesskey' => sesskey()));
-                    $sectionoutput .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
-                            array('class' => 'movehere', 'title' => $strmovefull));
+                    $sectionoutput .= html_writer::tag('li',
+                            html_writer::link($movingurl, $this->output->render($movingpix), array('title' => $strmovefull)),
+                            array('class' => 'movehere'));
                 }
 
                 $sectionoutput .= $modulehtml;
@@ -1107,8 +1108,9 @@ class core_course_renderer extends plugin_renderer_base {
 
             if ($ismoving) {
                 $movingurl = new moodle_url('/course/mod.php', array('movetosection' => $section->id, 'sesskey' => sesskey()));
-                $sectionoutput .= html_writer::tag('li', html_writer::link($movingurl, $this->output->render($movingpix)),
-                        array('class' => 'movehere', 'title' => $strmovefull));
+                $sectionoutput .= html_writer::tag('li',
+                        html_writer::link($movingurl, $this->output->render($movingpix), array('title' => $strmovefull)),
+                        array('class' => 'movehere'));
             }
         }
 
index 01d6857..781186a 100644 (file)
@@ -25,8 +25,10 @@ Feature: Test we can resort categories in the management interface.
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "Sort categories by name"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "Sort categories by ID number"   | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "Sort by Category name ascending"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Category name descending"      | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Category ID number ascending"  | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Category ID number descending" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   Scenario Outline: Test bulk sorting current category.
     Given the following "categories" exist:
@@ -52,8 +54,10 @@ Feature: Test we can resort categories in the management interface.
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "Sort categories by name"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "Sort categories by ID number"   | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "Sort by Category name ascending"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Category name descending"      | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Category ID number ascending"  | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Category ID number descending" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   Scenario Outline: Test resorting subcategories.
     Given the following "categories" exist:
@@ -77,8 +81,10 @@ Feature: Test we can resort categories in the management interface.
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "resortbyname"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "resortbyidnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "resortbyname"         | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "resortbynamedesc"     | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "resortbyidnumber"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "resortbyidnumberdesc" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   @javascript
   Scenario Outline: Test resorting subcategories with JS enabled.
@@ -103,8 +109,10 @@ Feature: Test we can resort categories in the management interface.
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "resortbyname"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "resortbyidnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "resortbyname"         | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "resortbynamedesc"     | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "resortbyidnumber"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "resortbyidnumberdesc" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   # The scenario below this is the same but with JS enabled.
   Scenario: Test moving categories up and down by one.
index 818619b..a07af93 100644 (file)
@@ -255,8 +255,10 @@ Feature: Course category management interface performs as expected
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "Sort categories by name"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "Sort categories by ID number"   | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "Sort by Category name ascending"       | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Category name descending"      | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Category ID number ascending"  | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Category ID number descending" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   @javascript
   Scenario Outline: Sub categories are displayed correctly when resorted
@@ -281,8 +283,10 @@ Feature: Course category management interface performs as expected
 
   Examples:
     | sortby | cat1 | cat2 | cat3 |
-    | "resortbyname"            | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "resortbyidnumber"        | "Extended social studies" | "Social studies" | "Applied sciences" |
+    | "resortbyname"         | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "resortbynamedesc"     | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "resortbyidnumber"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "resortbyidnumberdesc" | "Applied sciences"        | "Social studies"          | "Extended social studies" |
 
   @javascript
   Scenario Outline: Test courses are displayed correctly after being resorted.
@@ -290,10 +294,10 @@ Feature: Course category management interface performs as expected
       | name | category 0| idnumber |
       | Cat 1 | 0 | CAT1 |
     And the following "courses" exist:
-      | category | fullname | shortname | idnumber | sortorder |
-      | CAT1 | Social studies | Senior school | Ext003 | 1 |
-      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 |
-      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 |
+      | category | fullname | shortname | idnumber | sortorder | timecreated |
+      | CAT1 | Social studies | Senior school | Ext003 | 1 | 10000000001 |
+      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 | 10000000002 |
+      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 | 10000000003 |
 
     And I log in as "admin"
     And I go to the courses management page
@@ -302,9 +306,14 @@ Feature: Course category management interface performs as expected
   # Redirect.
     And I should see the "Course categories and courses" management page
     And I click on "Sort courses" "link"
-    And I should see "By fullname" in the ".course-listing-actions" "css_element"
-    And I should see "By shortname" in the ".course-listing-actions" "css_element"
-    And I should see "By idnumber" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created descending" in the ".course-listing-actions" "css_element"
     And I click on <sortby> "link" in the ".course-listing-actions" "css_element"
   # Redirect.
     And I should see the "Course categories and courses" management page
@@ -313,9 +322,14 @@ Feature: Course category management interface performs as expected
 
   Examples:
     | sortby | course1 | course2 | course3 |
-    | "By fullname"        | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "By shortname"       | "Extended social studies" | "Applied sciences"        | "Social studies" |
-    | "By idnumber"        | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course full name ascending"     | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Course full name descending"    | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Course short name ascending"    | "Extended social studies" | "Applied sciences"        | "Social studies" |
+    | "Sort by Course short name descending"   | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course ID number ascending"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course ID number descending"    | "Applied sciences"        | "Social studies"          | "Extended social studies" |
+    | "Sort by Course time created ascending"  | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course time created descending" | "Extended social studies" | "Applied sciences"        | "Social studies" |
 
   @javascript
   Scenario: Test course pagination
@@ -344,7 +358,7 @@ Feature: Course category management interface performs as expected
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I click on "Sort courses" "link"
-    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    And I click on "Sort by Course ID number ascending" "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see "Per page: 20" in the ".course-listing-actions" "css_element"
     And I should see course listing "Course 1" before "Course 2"
@@ -527,7 +541,7 @@ Feature: Course category management interface performs as expected
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I click on "Sort courses" "link"
-    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    And I click on "Sort by Course ID number ascending" "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see "Per page: 20" in the ".course-listing-actions" "css_element"
     And I should see course listing "Course 1" before "Course 2"
@@ -592,7 +606,7 @@ Feature: Course category management interface performs as expected
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I click on "Sort courses" "link"
-    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    And I click on "Sort by Course ID number ascending" "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I should see "Per page: 20" in the ".course-listing-actions" "css_element"
index b530e5b..9cb31a9 100644 (file)
@@ -10,10 +10,10 @@ Feature: Test we can resort course in the management interface.
       | name | category 0| idnumber |
       | Cat 1 | 0 | CAT1 |
     And the following "courses" exist:
-      | category | fullname | shortname | idnumber | sortorder |
-      | CAT1 | Social studies | Senior school | Ext003 | 1 |
-      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 |
-      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 |
+      | category | fullname | shortname | idnumber | sortorder | timecreated |
+      | CAT1 | Social studies | Senior school | Ext003 | 1 | 10000000001 |
+      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 | 10000000002 |
+      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 | 10000000003 |
 
     And I log in as "admin"
     And I go to the courses management page
@@ -22,9 +22,14 @@ Feature: Test we can resort course in the management interface.
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I should see "Sort courses" in the ".course-listing-actions" "css_element"
-    And I should see "By fullname" in the ".course-listing-actions" "css_element"
-    And I should see "By shortname" in the ".course-listing-actions" "css_element"
-    And I should see "By idnumber" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created descending" in the ".course-listing-actions" "css_element"
     And I click on <sortby> "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see the "Course categories and courses" management page
@@ -33,9 +38,14 @@ Feature: Test we can resort course in the management interface.
 
   Examples:
     | sortby | course1 | course2 | course3 |
-    | "By fullname"        | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "By shortname"       | "Extended social studies" | "Applied sciences"        | "Social studies" |
-    | "By idnumber"        | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course full name ascending"     | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Course full name descending"    | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Course short name ascending"    | "Extended social studies" | "Applied sciences"        | "Social studies" |
+    | "Sort by Course short name descending"   | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course ID number ascending"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course ID number descending"    | "Applied sciences"        | "Social studies"          | "Extended social studies" |
+    | "Sort by Course time created ascending"  | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course time created descending" | "Extended social studies" | "Applied sciences"        | "Social studies" |
 
   @javascript
   Scenario Outline: Resort courses with JavaScript enabled.
@@ -43,10 +53,10 @@ Feature: Test we can resort course in the management interface.
       | name | category 0| idnumber |
       | Cat 1 | 0 | CAT1 |
     And the following "courses" exist:
-      | category | fullname | shortname | idnumber | sortorder |
-      | CAT1 | Social studies | Senior school | Ext003 | 1 |
-      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 |
-      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 |
+      | category | fullname | shortname | idnumber | sortorder | timecreated |
+      | CAT1 | Social studies | Senior school | Ext003 | 1 | 10000000001 |
+      | CAT1 | Applied sciences  | Middle school | Sci001 | 2 | 10000000002 |
+      | CAT1 | Extended social studies  | Junior school | Ext002 | 3 | 10000000003 |
 
     And I log in as "admin"
     And I go to the courses management page
@@ -55,13 +65,23 @@ Feature: Test we can resort course in the management interface.
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I should see "Sort courses" in the ".course-listing-actions" "css_element"
-    And I should not see "By fullname" in the ".course-listing-actions" "css_element"
-    And I should not see "By shortname" in the ".course-listing-actions" "css_element"
-    And I should not see "By idnumber" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course full name ascending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course full name descending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course short name ascending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course short name descending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course ID number ascending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course ID number descending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course time created ascending" in the ".course-listing-actions" "css_element"
+    And I should not see "Sort by Course time created descending" in the ".course-listing-actions" "css_element"
     And I click on "Sort courses" "link"
-    And I should see "By fullname" in the ".course-listing-actions" "css_element"
-    And I should see "By shortname" in the ".course-listing-actions" "css_element"
-    And I should see "By idnumber" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course full name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course short name descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course ID number descending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created ascending" in the ".course-listing-actions" "css_element"
+    And I should see "Sort by Course time created descending" in the ".course-listing-actions" "css_element"
     And I click on <sortby> "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see the "Course categories and courses" management page
@@ -70,9 +90,14 @@ Feature: Test we can resort course in the management interface.
 
   Examples:
     | sortby | course1 | course2 | course3 |
-    | "By fullname"        | "Applied sciences"        | "Extended social studies" | "Social studies" |
-    | "By shortname"       | "Extended social studies" | "Applied sciences"        | "Social studies" |
-    | "By idnumber"        | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course full name ascending"     | "Applied sciences"        | "Extended social studies" | "Social studies" |
+    | "Sort by Course full name descending"    | "Social studies"          | "Extended social studies" | "Applied sciences" |
+    | "Sort by Course short name ascending"    | "Extended social studies" | "Applied sciences"        | "Social studies" |
+    | "Sort by Course short name descending"   | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course ID number ascending"     | "Extended social studies" | "Social studies"          | "Applied sciences" |
+    | "Sort by Course ID number descending"    | "Applied sciences"        | "Social studies"          | "Extended social studies" |
+    | "Sort by Course time created ascending"  | "Social studies"          | "Applied sciences"        | "Extended social studies" |
+    | "Sort by Course time created descending" | "Extended social studies" | "Applied sciences"        | "Social studies" |
 
   Scenario: Test moving courses up and down by one.
     Given the following "categories" exist:
@@ -93,7 +118,7 @@ Feature: Test we can resort course in the management interface.
     And I should see "Course categories" in the "#category-listing h3" "css_element"
     And I should see "Cat 1" in the "#category-listing" "css_element"
     And I click on "Sort courses" "link"
-    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    And I click on "Sort by Course ID number ascending" "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I should see course listing "Course 1" before "Course 2"
@@ -130,7 +155,7 @@ Feature: Test we can resort course in the management interface.
     And I should see "Course categories" in the "#category-listing h3" "css_element"
     And I should see "Cat 1" in the "#category-listing" "css_element"
     And I click on "Sort courses" "link"
-    And I click on "By idnumber" "link" in the ".course-listing-actions" "css_element"
+    And I click on "Sort by Course ID number ascending" "link" in the ".course-listing-actions" "css_element"
     # Redirect.
     And I should see the "Course categories and courses" management page
     And I should see course listing "Course 1" before "Course 2"
index 7feea4d..305ba25 100644 (file)
@@ -34,6 +34,14 @@ $capabilities = array(
         'archetypes' => array(
         )
     ),
+    'enrol/category:config' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
 );
 
 
index 417b28d..badd2a5 100644 (file)
@@ -22,6 +22,7 @@
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['category:config'] = 'Configure category enrol instances';
 $string['category:synchronised'] = 'Role assignments synchronised to course enrolment';
 $string['pluginname'] = 'Category enrolments';
 $string['pluginname_desc'] = 'Category enrolment plugin is a legacy solution for enrolments at the course category level via role assignments. It is recommended to use cohort synchronisation instead.';
index b806eae..f25b60b 100644 (file)
@@ -38,9 +38,14 @@ class enrol_category_plugin extends enrol_plugin {
      * @param stdClass $instance
      * @return bool
      */
-    public function instance_deleteable($instance) {
+    public function can_delete_instance($instance) {
         global $DB;
 
+        $context = context_course::instance($instance->courseid);
+        if (!has_capability('enrol/database:config', $context)) {
+            return false;
+        }
+
         if (!enrol_is_enabled('category')) {
             return true;
         }
index cb462c0..54262d8 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_category';  // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60;
index ef6d5ae..e6a3026 100644 (file)
@@ -30,6 +30,18 @@ defined('MOODLE_INTERNAL') || die();
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class enrol_cohort_plugin extends enrol_plugin {
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param stdClass $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/cohort:config', $context);
+    }
+
     /**
      * Returns localised name of enrol instance.
      *
index 7961267..02cf824 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_cohort';    // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
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 5a6855e..922beb3 100644 (file)
@@ -34,4 +34,12 @@ $capabilities = array(
             'manager' => CAP_ALLOW,
         )
     ),
+    'enrol/database:config' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
 );
index 5bae9ea..22efa55 100644 (file)
@@ -22,6 +22,7 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['database:config'] = 'Configure database enrol instances';
 $string['database:unenrol'] = 'Unenrol suspended users';
 $string['dbencoding'] = 'Database encoding';
 $string['dbhost'] = 'Database host';
index 9c51607..de5db32 100644 (file)
@@ -38,7 +38,11 @@ class enrol_database_plugin extends enrol_plugin {
      * @param stdClass $instance
      * @return bool
      */
-    public function instance_deleteable($instance) {
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        if (!has_capability('enrol/database:config', $context)) {
+            return false;
+        }
         if (!enrol_is_enabled('database')) {
             return true;
         }
index b4cfdcf..9dff389 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_database';  // Full name of the plugin (used for diagnostics)
 //TODO: should we add cron sync?
index 335a766..fa46832 100644 (file)
@@ -101,8 +101,9 @@ class enrol_flatfile_plugin extends enrol_plugin {
      * @param object $instance
      * @return bool
      */
-    public function instance_deleteable($instance) {
-        return true;
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/flatfile:manage', $context);
     }
 
     /**
index 5289f09..05fb255 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_flatfile';  // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60;
index 2f0d444..df60177 100644 (file)
@@ -391,4 +391,16 @@ class enrol_guest_plugin extends enrol_plugin {
         // No need to set mapping, we do not restore users or roles here.
         $step->set_mapping('enrol', $oldid, 0);
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/guest:config', $context);
+    }
+
 }
index b506dd2..1ef76e0 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_guest';     // Full name of the plugin (used for diagnostics)
diff --git a/enrol/imsenterprise/db/access.php b/enrol/imsenterprise/db/access.php
new file mode 100644 (file)
index 0000000..09b9a44
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// 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/>.
+
+/**
+ * Capabilities for imsenterprise enrolment plugin.
+ *
+ * @package    enrol_imsenterprise
+ * @copyright  2014 Daniel Neis Araujo
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+    'enrol/imsenterprise:config' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
+);
+
index 8564667..b7a8659 100644 (file)
@@ -46,6 +46,7 @@ $string['filelockedmail'] = 'The text file you are using for IMS-file-based enro
 $string['filelockedmailsubject'] = 'Important error: Enrolment file';
 $string['fixcasepersonalnames'] = 'Change personal names to Title Case';
 $string['fixcaseusernames'] = 'Change usernames to lower case';
+$string['imsenterprise:config'] = 'Configure IMS Enterprise enrol instances';
 $string['imsrolesdescription'] = 'The IMS Enterprise specification includes 8 distinct role types. Please choose how you want them to be assigned in Moodle, including whether any of them should be ignored.';
 $string['location'] = 'File location';
 $string['logtolocation'] = 'Log file output location (blank for no logging)';
index 6db0851..8b98a92 100644 (file)
@@ -270,13 +270,16 @@ class enrol_imsenterprise_plugin extends enrol_plugin {
      * @param string $tagcontents The raw contents of the XML element
      */
     protected function process_group_tag($tagcontents) {
-        global $DB;
+        global $DB, $CFG;
 
         // Get configs.
         $truncatecoursecodes    = $this->get_config('truncatecoursecodes');
         $createnewcourses       = $this->get_config('createnewcourses');
         $createnewcategories    = $this->get_config('createnewcategories');
 
+        if ($createnewcourses) {
+            require_once("$CFG->dirroot/course/lib.php");
+        }
         // Process tag contents.
         $group = new stdClass();
         if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
@@ -788,4 +791,15 @@ class enrol_imsenterprise_plugin extends enrol_plugin {
 
         return $defaultcategoryid;
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/imsenterprise:config', $context);
+    }
 }
index 6d978c6..6ab78fb 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;
-$plugin->requires  = 2014050800;
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_imsenterprise';
 $plugin->cron      = 60;
index 09ed549..825a9e8 100644 (file)
@@ -96,34 +96,52 @@ if ($canconfig and $action and confirm_sesskey()) {
             $instance = $instances[$instanceid];
             $plugin = $plugins[$instance->enrol];
 
-            if ($confirm) {
-                if (enrol_accessing_via_instance($instance)) {
-                    if (!$confirm2) {
-                        $yesurl = new moodle_url('/enrol/instances.php', array('id'=>$course->id, 'action'=>'delete', 'instance'=>$instance->id, 'confirm'=>1, 'confirm2'=>1, 'sesskey'=>sesskey()));
-                        $displayname = $plugin->get_instance_name($instance);
-                        $message = markdown_to_html(get_string('deleteinstanceconfirmself', 'enrol', array('name'=>$displayname)));
-                        echo $OUTPUT->header();
-                        echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
-                        echo $OUTPUT->footer();
-                        die();
+            if ($plugin->can_delete_instance($instance)) {
+                if ($confirm) {
+                    if (enrol_accessing_via_instance($instance)) {
+                        if (!$confirm2) {
+                            $yesurl = new moodle_url('/enrol/instances.php',
+                                                     array('id' => $course->id,
+                                                           'action' => 'delete',
+                                                           'instance' => $instance->id,
+                                                           'confirm' => 1,
+                                                           'confirm2' => 1,
+                                                           'sesskey' => sesskey()));
+                            $displayname = $plugin->get_instance_name($instance);
+                            $message = markdown_to_html(get_string('deleteinstanceconfirmself',
+                                                                   'enrol',
+                                                                   array('name' => $displayname)));
+                            echo $OUTPUT->header();
+                            echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
+                            echo $OUTPUT->footer();
+                            die();
+                        }
                     }
+                    $plugin->delete_instance($instance);
+                    redirect($PAGE->url);
                 }
-                $plugin->delete_instance($instance);
-                redirect($PAGE->url);
-            }
 
-            echo $OUTPUT->header();
-            $yesurl = new moodle_url('/enrol/instances.php', array('id'=>$course->id, 'action'=>'delete', 'instance'=>$instance->id, 'confirm'=>1,'sesskey'=>sesskey()));
-            $displayname = $plugin->get_instance_name($instance);
-            $users = $DB->count_records('user_enrolments', array('enrolid'=>$instance->id));
-            if ($users) {
-                $message = markdown_to_html(get_string('deleteinstanceconfirm', 'enrol', array('name'=>$displayname, 'users'=>$users)));
-            } else {
-                $message = markdown_to_html(get_string('deleteinstancenousersconfirm', 'enrol', array('name'=>$displayname)));
+                echo $OUTPUT->header();
+                $yesurl = new moodle_url('/enrol/instances.php',
+                                         array('id' => $course->id,
+                                               'action' => 'delete',
+                                               'instance' => $instance->id,
+                                               'confirm' => 1,
+                                               'sesskey' => sesskey()));
+                $displayname = $plugin->get_instance_name($instance);
+                $users = $DB->count_records('user_enrolments', array('enrolid' => $instance->id));
+                if ($users) {
+                    $message = markdown_to_html(get_string('deleteinstanceconfirm', 'enrol',
+                                                           array('name' => $displayname,
+                                                                 'users' => $users)));
+                } else {
+                    $message = markdown_to_html(get_string('deleteinstancenousersconfirm', 'enrol',
+                                                           array('name' => $displayname)));
+                }
+                echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
+                echo $OUTPUT->footer();
+                die();
             }
-            echo $OUTPUT->confirm($message, $yesurl, $PAGE->url);
-            echo $OUTPUT->footer();
-            die();
 
         } else if ($action === 'disable') {
             $instance = $instances[$instanceid];
@@ -212,8 +230,7 @@ foreach ($instances as $instance) {
         }
         ++$updowncount;
 
-        // edit links
-        if ($plugin->instance_deleteable($instance)) {
+        if ($plugin->can_delete_instance($instance)) {
             $aurl = new moodle_url($url, array('action'=>'delete', 'instance'=>$instance->id));
             $edit[] = $OUTPUT->action_icon($aurl, new pix_icon('t/delete', $strdelete, 'core', array('class' => 'iconsmall')));
         }
@@ -234,7 +251,7 @@ foreach ($instances as $instance) {
     }
 
     // link to instance management
-    if (enrol_is_enabled($instance->enrol)) {
+    if (enrol_is_enabled($instance->enrol) && $canconfig) {
         if ($icons = $plugin->get_action_icons($instance)) {
             $edit = array_merge($edit, $icons);
         }
index b4bf0b9..21d5b1e 100644 (file)
@@ -106,7 +106,12 @@ class enrol_ldap_plugin extends enrol_plugin {
      * @param object $instance
      * @return bool
      */
-    public function instance_deleteable($instance) {
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        if (!has_capability('enrol/database:config', $context)) {
+            return false;
+        }
+
         if (!enrol_is_enabled('ldap')) {
             return true;
         }
@@ -1155,4 +1160,3 @@ class enrol_ldap_plugin extends enrol_plugin {
         }
     }
 }
-
index a1b47dc..76505ab 100644 (file)
@@ -25,6 +25,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_ldap';      // Full name of the plugin (used for diagnostics)
index 805ed05..4066f9d 100644 (file)
@@ -553,4 +553,15 @@ class enrol_manual_plugin extends enrol_plugin {
 
         groups_add_member($groupid, $userid);
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/manual:config', $context);
+    }
 }
index 4089156..963d30d 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_manual';    // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 600;
index 258232a..30d2731 100644 (file)
@@ -148,5 +148,16 @@ class enrol_meta_plugin extends enrol_plugin {
         require_once("$CFG->dirroot/enrol/meta/locallib.php");
         enrol_meta_sync();
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/meta:config', $context);
+    }
 }
 
index 1fab182..4951f33 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_meta';      // Full name of the plugin (used for diagnostics)
-$plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
\ No newline at end of file
+$plugin->cron      = 60*60;             // run cron every hour by default, it is not out-of-sync often
diff --git a/enrol/mnet/db/access.php b/enrol/mnet/db/access.php
new file mode 100644 (file)
index 0000000..f980979
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+// 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/>.
+
+/**
+ * Capabilities for mnet enrolment plugin.
+ *
+ * @package    enrol_mnet
+ * @copyright  2014 Daniel Neis Araujo
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$capabilities = array(
+    'enrol/mnet:config' => array(
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_COURSE,
+        'archetypes' => array(
+            'manager' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+        )
+    ),
+);
index 9f086d1..2fb3ed0 100644 (file)
@@ -25,6 +25,7 @@
 $string['error_multiplehost'] = 'Some instance of MNet enrolment plugin already exists for this host. Only one instance per host and/or one instance for \'All hosts\' is allowed.';
 $string['instancename'] = 'Enrolment method name';
 $string['instancename_help'] = 'You can optionally rename this instance of the MNet enrolment method. If you leave this field empty, the default instance name will be used, containing the name of the remote host and the assigned role for their users.';
+$string['mnet:config'] = 'Configure MNet enrol instances';
 $string['mnet_enrol_description'] = 'Publish this service to allow administrators at {$a} to enrol their students in courses you have created on your server.<br/><ul><li><em>Dependency</em>: You must also <strong>subscribe</strong> to the SSO (Identity Provider) service on {$a}.</li><li><em>Dependency</em>: You must also <strong>publish</strong> the SSO (Service Provider) service to {$a}.</li></ul><br/>Subscribe to this service to be able to enrol your students in courses  on {$a}.<br/><ul><li><em>Dependency</em>: You must also <strong>publish</strong> the SSO (Identity Provider) service to {$a}.</li><li><em>Dependency</em>: You must also <strong>subscribe</strong> to the SSO (Service Provider) service on {$a}.</li></ul><br/>';
 $string['mnet_enrol_name'] = 'Remote enrolment service';
 $string['pluginname'] = 'MNet remote enrolments';
index 28578a2..2d3c55b 100644 (file)
@@ -88,4 +88,15 @@ class enrol_mnet_plugin extends enrol_plugin {
 
         return new moodle_url('/enrol/mnet/addinstance.php', array('id'=>$courseid));
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/mnet:config', $context);
+    }
 }
index 8caa9a8..c78a309 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_mnet';      // Full name of the plugin (used for diagnostics)
index f3762d7..71ac2e4 100644 (file)
@@ -307,4 +307,15 @@ class enrol_paypal_plugin extends enrol_plugin {
         $this->process_expirations($trace);
         return 0;
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/paypal:config', $context);
+    }
 }
index 4fd1649..abce57d 100644 (file)
@@ -25,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_paypal';    // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 60;
index edc114f..e2ad583 100644 (file)
@@ -644,4 +644,15 @@ class enrol_self_plugin extends enrol_plugin {
         // we do not use component in manual or self enrol.
         role_assign($roleid, $userid, $contextid, '', 0);
     }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        $context = context_course::instance($instance->courseid);
+        return has_capability('enrol/self:config', $context);
+    }
 }
index 9e01613..47de8ce 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014051200;        // The current plugin version (Date: YYYYMMDDXX)
-$plugin->requires  = 2014050800;        // Requires this Moodle version
+$plugin->version   = 2014072200;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->requires  = 2014072200;        // Requires this Moodle version
 $plugin->component = 'enrol_self';      // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 600;
index dc6d2c0..2bed0e7 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /enrol/* - plugins,
 information provided here is intended especially for developers.
 
+=== 2.8 ===
+
+* enrol_plugin::instance_deleteable() is deprecated and has been replaced by enrol_plugin::can_delete_instance()
+
 === 2.6 ===
 
 * Enrolment plugin which supports self enrolment should implement can_self_enrol()
index 05eae5d..924837f 100644 (file)
@@ -34,6 +34,8 @@
 
 defined('MOODLE_INTERNAL') || die;
 
+require_once($CFG->libdir . '/classes/useragent.php');
+
 /**
  * Create TeX image link.
  *
@@ -100,6 +102,7 @@ function filter_text_image($imagefile, $tex, $height, $width, $align, $alt) {
         $action = new popup_action('click', $link, 'popup', array('width'=>320,'height'=>240));
     }
     $output = $OUTPUT->action_link($link, $anchorcontents, $action, array('title'=>'TeX')); //TODO: the popups do not work when text caching is enabled!!
+    $output = "<span class=\"MathJax_Preview\">$output</span><script type=\"math/tex\">$tex</script>";
 
     return $output;
 }
@@ -197,6 +200,9 @@ class filter_tex extends moodle_text_filter {
                 $DB->insert_record("cache_filters", $texcache, false);
             }
             $convertformat = get_config('filter_tex', 'convertformat');
+            if ($convertformat == 'svg' && !core_useragent::supports_svg()) {
+                $convertformat = 'png';
+            }
             $filename = $md5.".{$convertformat}";
             $text = str_replace( $matches[0][$i], filter_text_image($filename, $texexp, 0, 0, $align, $alt), $text);
         }
index 49fe7d1..c06a83d 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['configconvertformat'] = 'If <i>latex</i>, <i>dvips</i> and <i>convert</i> are available, the images are created using the specified format. If it is not, mimeTeX will be used and it will create GIF images.';
-$string['convertformat'] = '<i>Convert</i> output format';
+$string['configconvertformat'] = 'If <i>latex</i> and <i>dvips</i> and are available, they are used to GIF or PNG images if <i>convert</i> is also available or if <i>dvisvgm</i> is available SVG images as specified. Otherwise <i>mimeTeX</i> will be used, and it will create GIF images.';
+$string['convertformat'] = 'Output image format';
 $string['latexpreamble'] = 'LaTeX preamble';
 $string['latexsettings'] = 'LaTeX renderer Settings';
 $string['filtername'] = 'TeX notation';
 $string['pathconvert'] = 'Path of <i>convert</i> binary';
 $string['pathdvips'] = 'Path of <i>dvips</i> binary';
+$string['pathdvisvgm'] = 'Path of <i>dvisvgm</i> binary';
 $string['pathlatex'] = 'Path of <i>latex</i> binary';
 $string['pathmimetex'] = 'Path of <i>mimetex</i> binary';
 $string['pathmimetexdesc'] = 'Moodle will use its own mimetex binary unless another valid path is specified.';
index a79a33e..d626a9f 100644 (file)
@@ -78,7 +78,7 @@
         /**
          * Render TeX string into gif/png
          * @param string $formula TeX formula
-         * @param string $filename base of filename for output (no extension)
+         * @param string $filename filename for output (including extension)
          * @param int $fontsize font size
          * @param int $density density value for .ps to .gif/.png conversion
          * @param string $background background color (e.g, #FFFFFF).
             $doc = $this->construct_latex_document( $formula, $fontsize );
 
             // construct some file paths
+            $convertformat = get_config('filter_tex', 'convertformat');
+            if (!strpos($filename, ".{$convertformat}")) {
+                $convertformat = 'png';
+            }
+            $filename = str_replace(".{$convertformat}", '', $filename);
             $tex = "{$this->temp_dir}/$filename.tex";
             $dvi = "{$this->temp_dir}/$filename.dvi";
             $ps  = "{$this->temp_dir}/$filename.ps";
-            $convertformat = get_config('filter_tex', 'convertformat');
             $img = "{$this->temp_dir}/$filename.{$convertformat}";
 
             // turn the latex doc into a .tex file in the temp area
                 return false;
             }
 
-            // run convert on document (.ps to .gif/.png)
+            // Run convert on document (.ps to .gif/.png) or run dvisvgm (.ps to .svg).
             if ($background) {
                 $bg_opt = "-transparent \"$background\""; // Makes transparent background
             } else {
                 $bg_opt = "";
             }
-            $pathconvert = get_config('filter_tex', 'pathconvert');
-            $command = "{$pathconvert} -density $density -trim $bg_opt $ps $img";
+            if ($convertformat == 'svg') {
+                $pathdvisvgm = 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";
+            }
             if ($this->execute($command, $log )) {
                 return false;
             }
index b77dfae..f48bca8 100644 (file)
@@ -127,13 +127,21 @@ function filter_tex_updatedcallback($name) {
 
     $pathdvips = get_config('filter_tex', 'pathdvips');
     $pathconvert = get_config('filter_tex', 'pathconvert');
+    $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
 
-    if (!(is_file($pathlatex) && is_executable($pathlatex) &&
-          is_file($pathdvips) && is_executable($pathdvips) &&
-          is_file($pathconvert) && is_executable($pathconvert))) {
-        // LaTeX, dvips or convert are not available, and mimetex can only produce GIFs so...
-        set_config('convertformat', 'gif', 'filter_tex');
+    $supportedformats = array('gif');
+    if ((is_file($pathlatex) && is_executable($pathlatex)) &&
+            (is_file($pathdvips) && is_executable($pathdvips))) {
+        if (is_file($pathconvert) && is_executable($pathconvert)) {
+             $supportedformats[] = 'png';
+        }
+        if (is_file($pathdvisvgm) && is_executable($pathdvisvgm)) {
+             $supportedformats[] = 'svg';
+        }
+    }
+    if (!in_array(get_config('filter_tex', 'convertformat'), $supportedformats)) {
+        set_config('convertformat', array_pop($supportedformats), 'filter_tex');
     }
-}
 
+}
 
index 43c9d1a..d66faf2 100644 (file)
@@ -33,6 +33,9 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
 
     if (!file_exists($pathname)) {
         $convertformat = get_config('filter_tex', 'convertformat');
+        if (strpos($image, '.png')) {
+            $convertformat = 'png';
+        }
         $md5 = str_replace(".{$convertformat}", '', $image);
         if ($texcache = $DB->get_record('cache_filters', array('filter'=>'tex', 'md5key'=>$md5))) {
             if (!file_exists($CFG->dataroot.'/filter/tex')) {
@@ -44,9 +47,9 @@ define('NO_MOODLE_COOKIES', true); // Because it interferes with caching
             $density = get_config('filter_tex', 'density');
             $background = get_config('filter_tex', 'latexbackground');
             $texexp = $texcache->rawtext; // the entities are now decoded before inserting to DB
-            $latex_path = $latex->render($texexp, $md5, 12, $density, $background);
-            if ($latex_path) {
-                copy($latex_path, $pathname);
+            $lateximage = $latex->render($texexp, $image, 12, $density, $background);
+            if ($lateximage) {
+                copy($lateximage, $pathname);
                 $latex->clean_up($md5);
 
             } else {
index 6c79a21..fb0fcdf 100644 (file)
@@ -36,15 +36,20 @@ if ($ADMIN->fulltree) {
     $items[] = new admin_setting_configtext('filter_tex/latexbackground', get_string('backgroundcolour', 'admin'), '', '#FFFFFF');
     $items[] = new admin_setting_configtext('filter_tex/density', get_string('density', 'admin'), '', '120', PARAM_INT);
 
+    $default_filter_tex_pathlatex   = '';
+    $default_filter_tex_pathdvips   = '';
+    $default_filter_tex_pathdvisvgm = '';
+    $default_filter_tex_pathconvert = '';
     if (PHP_OS=='Linux') {
         $default_filter_tex_pathlatex   = "/usr/bin/latex";
         $default_filter_tex_pathdvips   = "/usr/bin/dvips";
+        $default_filter_tex_pathdvisvgm = "/usr/bin/dvisvgm";
         $default_filter_tex_pathconvert = "/usr/bin/convert";
-
     } else if (PHP_OS=='Darwin') {
         // most likely needs a fink install (fink.sf.net)
         $default_filter_tex_pathlatex   = "/sw/bin/latex";
         $default_filter_tex_pathdvips   = "/sw/bin/dvips";
+        $default_filter_tex_pathdvisvgm = "/usr/bin/dvisvgm";
         $default_filter_tex_pathconvert = "/sw/bin/convert";
 
     } else if (PHP_OS=='WINNT' or PHP_OS=='WIN32' or PHP_OS=='Windows') {
@@ -53,22 +58,18 @@ if ($ADMIN->fulltree) {
         $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\" ";
-
-    } else {
-        $default_filter_tex_pathlatex   = '';
-        $default_filter_tex_pathdvips   = '';
-        $default_filter_tex_pathconvert = '';
     }
 
     $items[] = new admin_setting_configexecutable('filter_tex/pathlatex', get_string('pathlatex', 'filter_tex'), '', $default_filter_tex_pathlatex);
     $items[] = new admin_setting_configexecutable('filter_tex/pathdvips', get_string('pathdvips', 'filter_tex'), '', $default_filter_tex_pathdvips);
     $items[] = new admin_setting_configexecutable('filter_tex/pathconvert', get_string('pathconvert', 'filter_tex'), '', $default_filter_tex_pathconvert);
+    $items[] = new admin_setting_configexecutable('filter_tex/pathdvisvgm', get_string('pathdvisvgm', 'filter_tex'), '', $default_filter_tex_pathdvisvgm);
     $items[] = new admin_setting_configexecutable('filter_tex/pathmimetex', get_string('pathmimetex', 'filter_tex'), get_string('pathmimetexdesc', 'filter_tex'), '');
 
-    // Even if we offer GIF and PNG formats here, in the update callback we check whether
-    // all the paths actually point to executables. If they don't, we force the setting
+    // Even if we offer GIF, PNG and SVG formats here, in the update callback we check whether
+    // required paths actually point to executables. If they don't, we force the setting
     // to GIF, as that's the only format mimeTeX can produce.
-    $formats = array('gif' => 'GIF', 'png' => 'PNG');
+    $formats = array('gif' => 'GIF', 'png' => 'PNG', 'svg' => 'SVG');
     $items[] = new admin_setting_configselect('filter_tex/convertformat', get_string('convertformat', 'filter_tex'), get_string('configconvertformat', 'filter_tex'), 'gif', $formats);
 
     foreach ($items as $item) {
index 51588da..b82e97c 100644 (file)
 
         // first check if it is likely to work at all
         $output .= "<h3>Checking executables</h3>\n";
-        $executables_exist = true;
+        $executablesexist = true;
         $pathlatex = get_config('filter_tex', 'pathlatex');
         if (is_file($pathlatex)) {
             $output .= "latex executable ($pathlatex) is readable<br />\n";
-        }
-        else {
-            $executables_exist = false;
+        } else {
+            $executablesexist = false;
             $output .= "<b>Error:</b> latex executable ($pathlatex) is not readable<br />\n";
         }
         $pathdvips = get_config('filter_tex', 'pathdvips');
         if (is_file($pathdvips)) {
             $output .= "dvips executable ($pathdvips) is readable<br />\n";
-        }
-        else {
-            $executables_exist = false;
+        } else {
+            $executablesexist = false;
             $output .= "<b>Error:</b> dvips executable ($pathdvips) is not readable<br />\n";
         }
         $pathconvert = get_config('filter_tex', 'pathconvert');
         if (is_file($pathconvert)) {
             $output .= "convert executable ($pathconvert) is readable<br />\n";
-        }
-        else {
-            $executables_exist = false;
+        } else {
+            $executablesexist = false;
             $output .= "<b>Error:</b> convert executable ($pathconvert) is not readable<br />\n";
         }
+        $pathdvisvgm = get_config('filter_tex', 'pathdvisvgm');
+        if (is_file($pathdvisvgm)) {
+            $output .= "dvisvgm executable ($pathdvisvgm) is readable<br />\n";
+        } else {
+            $executablesexist = false;
+            $output .= "<b>Error:</b> dvisvgm executable ($pathdvisvgm) is not readable<br />\n";
+        }
 
         // knowing that it might work..
         $md5 = md5($expression);
         $cmd = "$pathdvips -E $dvi -o $ps";
         $output .= execute($cmd);
 
-        // step 3: convert command
-        $cmd = "$pathconvert -density 240 -trim $ps $img ";
+        // Step 3: Set convert or dvisvgm command.
+        if ($convertformat == 'svg') {
+            $cmd = "$pathdvisvgm -E $ps -o $img";
+        } else {
+            $cmd = "$pathconvert -density 240 -trim $ps $img ";
+        }
         $output .= execute($cmd);
 
         if (!$graphic) {
             echo $output;
-        } else if (file_exists($img)){
+        } else if (file_exists($img)) {
             send_file($img, "$md5.{$convertformat}");
         } else {
             echo "Error creating image, see command execution output for more details.";
@@ -338,7 +346,7 @@ searches the database cache_filters table to see if this TeX expression had been
 processed before. If not, it adds a DB entry for that expression.  It then
 replaces the TeX expression by an &lt;img src=&quot;.../filter/tex/pix.php...&quot;&gt;
 tag.  The filter/tex/pix.php script then searches the database to find an
-appropriate gif/png image file for that expression and to create one if it doesn't exist.
+appropriate gif/png/svg image file for that expression and to create one if it doesn't exist.
 It will then use either the LaTex/Ghostscript renderer (using external executables
 on your system) or the bundled Mimetex executable. The full Latex/Ghostscript
 renderer produces better results and is tried first.
@@ -349,7 +357,7 @@ you might try to fix them.</p>
 process this expression. Then the database entry for that expression contains
 a bad TeX expression in the rawtext field (usually blank). You can fix this
 by clicking on &quot;Delete DB Entry&quot;</li>
-<li>The TeX to gif/png image conversion process does not work.
+<li>The TeX to gif/png/svg image conversion process does not work.
 If paths are specified in the filter configuation screen for the three
 executables these will be tried first. Note that they still must be correctly
 installed and have the correct permissions. In particular make sure that you
index d6a74dc..bd3678e 100644 (file)
@@ -30,7 +30,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$string['admindirname'] = 'Ð\9aаÑ\82алог Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñ\81Ñ\82Ñ\80аÑ\82оÑ\80а';
+$string['admindirname'] = 'Ð\9aаÑ\82алог Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñ\81Ñ\82Ñ\80иÑ\80ованиÑ\8f';
 $string['availablelangs'] = 'Доступные языковые пакеты';
 $string['chooselanguagehead'] = 'Выберите язык';
 $string['chooselanguagesub'] = 'Сейчас необходимо выбрать язык ТОЛЬКО для сообщений во время установки. Язык сайта и пользовательских интерфейсов можно будет указать далее в процессе установки.';
@@ -45,7 +45,7 @@ $string['datarootpermission'] = 'Разрешения на каталоги да
 $string['dbprefix'] = 'Префикс имен таблиц';
 $string['dirroot'] = 'Каталог Moodle';
 $string['environmenthead'] = 'Проверка среды...';
-$string['environmentsub2'] = 'У каждой версии Moodle есть минимальные требования к версии PHP и набор обязательных расширений PHP.
+$string['environmentsub2'] = 'У каждой версии Moodle есть минимальные требования к версии PHP и набору обязательных расширений PHP.
 Полная проверка среды осуществляется перед каждой установкой и обновлением.
 Пожалуйста, свяжитесь с администратором сервера, если не знаете, как установить новую версию или включить расширения PHP.';
 $string['errorsinenvironment'] = 'Проверка окружения не выполнена!';
@@ -72,13 +72,12 @@ $string['pathshead'] = 'Подтвердите пути';
 $string['pathsrodataroot'] = 'Каталог данных недоступен для записи.';
 $string['pathsroparentdataroot'] = 'Родительский каталог ({$a->parent}) не доступен для записи. Программа установки не может создать каталог данных ({$a->dataroot}).';
 $string['pathssubadmindir'] = 'На небольшом числе веб-хостингов путь /admin используется для доступа к панели управления или чему-то еще. К сожалению, это противоречит стандартному расположению страниц управления Moodle. Это можно исправить путем переименования папки admin в каталоге Moodle и указания нового имени здесь. Например: <em>moodleadmin</em>. При этом все ссылки на панель управления Moodle исправятся автоматически.';
-$string['pathssubdataroot'] = 'Необходимо указать место, где Moodle будет хранить загружаемые файлы. Этот каталог должен быть доступен для чтения и ЗАПИСИ тому пользователю, от чьего имени запускается веб-сервер (обычно \'nobody\' или \'apache\'), но при этом не должен быть доступен напрямую через Интернет. Программа установки попробует создать этот каталог, если он не существует.';
-$string['pathssubdirroot'] = 'Полный путь к каталогу установки Moodle.';
-$string['pathssubwwwroot'] = 'Полный веб-адрес, по которому будет доступен Moodle.
-Использовать для доступа к Moodle несколько публичных адресов невозможно.
-Если у вашего сайта есть еще несколько публичных адресов, вам следует настроить постоянные перенаправления с этих адресов на указанный.
-Если ваш сайт доступен как из Интернета, так и из локальной сети, укажите здесь публичный адрес и настройте DNS таким образом, чтобы этот адрес был доступен и локальным пользователям.
-Если указанный здесь адрес неверный, измените URL в строке адреса браузера, чтобы перезапустить установку с другим значением.';
+$string['pathssubdataroot'] = '<p>Каталог, в котором Moodle будет хранить все файлы, размещаемые пользователями. </p><p>Этот каталог должен быть доступен для чтения и ЗАПИСИ тому пользователю, от чьего имени запускается веб-сервер (обычно \'www-data\', \'nobody\' или \'apache\'). </p><p>Этот каталог не должен быть доступен напрямую через Интернет. </p><p>Программа установки попробует создать этот каталог, если он не существует. </p>';
+$string['pathssubdirroot'] = '<p>Полный путь к каталогу установки Moodle.</p>';
+$string['pathssubwwwroot'] = '<p>Полный веб-адрес, по которому будет доступен Moodle, т.е. адрес, который пользователи будут вводить в адресной строке своего браузера для доступа к сайту Moodle.</p>
+<p>Использовать для доступа к Moodle несколько публичных адресов невозможно. Если у Вашего сайта есть еще несколько публичных адресов, то следует настроить постоянные перенаправления с этих адресов на указанный.</p>
+</p>Если Ваш сайт доступен как из Интернета, так и из локальной сети (иногда называют Интранет), укажите здесь публичный адрес.</p>
+</p>Если указанный здесь адрес неверный, измените URL в адресной строке браузера и перезапустите установку.</p>';
 $string['pathsunsecuredataroot'] = 'Расположение каталога данных не отвечает требованиям безопасности';
 $string['pathswrongadmindir'] = 'Каталог admin не существует';
 $string['phpextension'] = 'Расширение PHP «{$a}»';
@@ -91,7 +90,7 @@ $string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
 $string['welcomep20'] = 'Вы видите эту страницу, потому что успешно установили и запустили на своем компьютере набор программ <strong>{$a->packname} {$a->packversion}</strong>. Поздравляем!';
 $string['welcomep30'] = 'Эта версия набора программ <strong>{$a->installername}</strong> включает следующие программы, необходимые для создания среды, в которой будет работать <strong>Moodle</strong>:';
 $string['welcomep40'] = 'Также в этот набор входит <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>.';
-$string['welcomep50'] = 'Порядок использования приложений, входящих в этот набор, регламентируется соответствующими лицензиями. Набор программ <strong>{$a->installername}</strong> является полностью <a href="http://ru.wikipedia.org/wiki/Открытое_программное_обеспечение">открытым </a> и распространяется на условиях лицензии <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
+$string['welcomep50'] = 'Порядок использования приложений, входящих в этот набор, регламентируется соответствующими лицензиями. Набор программ <strong>{$a->installername}</strong> является <a href="http://www.opensource.org/docs/definition_plain.html">открытым программным обеспечением</a> и распространяется на условиях лицензии <a href="http://www.gnu.org/copyleft/gpl.html">GPL</a>.';
 $string['welcomep60'] = 'На следующих страницах Вы сможете за несколько простых шагов настроить и установить <strong>Moodle</strong> на свой компьютер. Вы сможете принять настройки по умолчанию или изменить их в зависимости от своих потребностей.';
 $string['welcomep70'] = 'Нажмите кнопку «Далее» чтобы продолжить процесс установки <strong>Moodle</strong>.';
 $string['wwwroot'] = 'Веб-адрес';
index 7a467c9..0885ceb 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['admindirname'] = 'అడ్మిన్ డైరెక్టరీ';
 $string['availablelangs'] = 'అందుబాటులో ఉన్న భాషల జాబితా';
 $string['chooselanguagehead'] = 'భాషను ఎంచుకోండి';
 $string['databasehost'] = 'డేటాబేసు హోస్టు';
 $string['databasename'] = 'డేటాబేసు పేరు';
+$string['databasetypehead'] = 'డేటాబేస్ డ్రైవర్ ఎంచుకోండి';
 $string['dataroot'] = 'డేటా డైరెక్టరీ';
+$string['datarootpermission'] = 'డేటా డైరెక్టరీల అనుమతి';
 $string['dbprefix'] = 'టేబుళ్ళ ఆదిపదం (ప్రిఫిక్స్)';
 $string['dirroot'] = 'Moodle డైరెక్టరీ';
 $string['environmenthead'] = 'మీ ఎన్విరాన్మెంటును పరిశీలిస్తున్నాం ...';
+$string['installation'] = 'సంస్థాపన';
+$string['paths'] = 'మార్గాలు';
+$string['pathswrongadmindir'] = 'అడ్మిన్ డైరెక్టరీ అసలు లేదు';
 $string['wwwroot'] = 'వెబ్ చిరునామా';
index 28de033..0d0180b 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
+$string['clianswerno'] = 'ні';
+$string['cliansweryes'] = 'так';
+$string['cliincorrectvalueerror'] = 'Помилка, некоректне значення "{$a->value}" для "{$a->option}"';
+$string['cliincorrectvalueretry'] = 'Некоректне значення, повторіть правильно';
+$string['clitypevalue'] = 'тип значення';
+$string['clitypevaluedefault'] = 'тип значення, натисніть Enter для використання типового значення ({$a})';
+$string['cliunknowoption'] = 'Невизначені опції: {$a}. Будь ласка, використайте опцію --help.';
+$string['cliyesnoprompt'] = 'натисніть y (означає так) або n (означає ні)';
 $string['environmentrequireinstall'] = 'повинен бути встановлений і включений';
 $string['environmentrequireversion'] = 'рекомендується версія {$a->needed}, використовується версія {$a->current}';
index c9f3c51..d07078d 100644 (file)
@@ -34,5 +34,11 @@ $string['clianswerno'] = 'n';
 $string['cliansweryes'] = 'y';
 $string['cliincorrectvalueerror'] = 'Lỗi, giá trị không đúng "{$a->value}" for "{$a->option}"';
 $string['cliincorrectvalueretry'] = 'Giá trị không đúng, vui lòng thử lại';
+$string['clitypevalue'] = 'nhập giá trị';
+$string['clitypevaluedefault'] = 'nhập giá trị, nhấn phím Enter để dùng giá trị mặc định ({$a})';
+$string['cliunknowoption'] = 'Lựa chọn chưa được nhận diện:
+  {$a}
+Vui lòng sử dụng lựa chọn --help.';
+$string['cliyesnoprompt'] = 'nhập y (có) hay n (không)';
 $string['environmentrequireinstall'] = 'cần phải được cài hay kích hoạt.';
 $string['environmentrequireversion'] = 'Cần phiên bản {$a->needed} trong khi bạn đang dùng {$a->current}';
index c74e820..61130db 100644 (file)
@@ -1513,12 +1513,9 @@ $string['resetstartdate'] = 'Reset start date';
 $string['resetstatus'] = 'Status';
 $string['resettask'] = 'Task';
 $string['resettodefaults'] = 'Reset to defaults';
-$string['resortsubcategoriesbyname'] = 'Sort subcategories by name';
-$string['resortsubcategoriesbyidnumber'] = 'Sort subcategories by idnumber';
+$string['resortsubcategoriesby'] = 'Sort subcategories by {$a} ascending';
+$string['resortsubcategoriesbyreverse'] = 'Sort subcategories by {$a} descending';
 $string['resortcourses'] = 'Sort courses';
-$string['resortbyshortname'] = 'By shortname';
-$string['resortbyfullname'] = 'By fullname';
-$string['resortbyidnumber'] = 'By idnumber';
 $string['resource'] = 'Resource';
 $string['resourcedisplayauto'] = 'Automatic';
 $string['resourcedisplaydownload'] = 'Force download';
@@ -1724,11 +1721,6 @@ $string['sort'] = 'Sort';
 $string['sortby'] = 'Sort by';
 $string['sortbyx'] = 'Sort by {$a} ascending';
 $string['sortbyxreverse'] = 'Sort by {$a} descending';
-$string['sortcategoriesbyname'] = 'Sort categories by name';
-$string['sortcategoriesbyidnumber'] = 'Sort categories by ID number';
-$string['sortcoursesbyfullname'] = 'Sort courses by full name';
-$string['sortcoursesbyshortname'] = 'Sort courses by short name';
-$string['sortcoursesbyidnumber'] = 'Sort courses by ID number';
 $string['sorting'] = 'Sorting';
 $string['sourcerole'] = 'Source role';
 $string['specifyname'] = 'You must specify a name.';
@@ -1822,6 +1814,7 @@ $string['therearecourses'] = 'There are {$a} courses';
 $string['thiscategory'] = 'This category';
 $string['thiscategorycontains'] = 'This category contains';
 $string['time'] = 'Time';
+$string['timecreatedcourse'] = 'Course time created';
 $string['timezone'] = 'Timezone';
 $string['to'] = 'To';
 $string['tocreatenewaccount'] = 'Skip to create new account';
index ad779f2..398ff67 100644 (file)
@@ -284,6 +284,46 @@ class core_collator {
         return $result;
     }
 
+    /**
+     * Locale aware sort of array of arrays.
+     *
+     * Given an array like:
+     * $array = array(
+     *     array('name' => 'bravo'),
+     *     array('name' => 'charlie'),
+     *     array('name' => 'alpha')
+     * );
+     *
+     * If you call:
+     * core_collator::asort_array_of_arrays_by_key($array, 'name')
+     *
+     * You will be returned $array sorted by the name key of the subarrays. e.g.
+     * $array = array(
+     *     array('name' => 'alpha'),
+     *     array('name' => 'bravo'),
+     *     array('name' => 'charlie')
+     * );
+     *
+     * @param array $array An array of objects to sort (handled by reference)
+     * @param string $key The key to use for comparison
+     * @param int $sortflag One of
+     *          core_collator::SORT_NUMERIC,
+     *          core_collator::SORT_STRING,
+     *          core_collator::SORT_NATURAL,
+     *          core_collator::SORT_REGULAR
+     *      optionally "|" core_collator::CASE_SENSITIVE
+     * @return bool True on success
+     */
+    public static function asort_array_of_arrays_by_key(array &$array, $key, $sortflag = core_collator::SORT_STRING) {
+        $original = $array;
+        foreach ($array as $initkey => $item) {
+            $array[$initkey] = $item[$key];
+        }
+        $result = self::asort($array, $sortflag);
+        self::restore_array($array, $original);
+        return $result;
+    }
+
     /**
      * Locale aware sorting, the key associations are kept, keys are sorted alphabetically.
      *
index 96488db..55eed55 100644 (file)
@@ -308,7 +308,7 @@ class core_grades_external extends external_api {
                                         'str_long_grade' => new external_value(
                                             PARAM_RAW, 'A nicely formatted string representation of the grade'),
                                         'str_feedback' => new external_value(
-                                            PARAM_TEXT, 'A string representation of the feedback from the grader'),
+                                            PARAM_RAW, 'A formatted string representation of the feedback from the grader'),
                                     )
                                 )
                             ),
@@ -345,7 +345,7 @@ class core_grades_external extends external_api {
                                         'str_grade' => new external_value(
                                             PARAM_RAW, 'A string representation of the grade'),
                                         'str_feedback' => new external_value(
-                                            PARAM_TEXT, 'A string representation of the feedback from the grader'),
+                                            PARAM_RAW, 'A formatted string representation of the feedback from the grader'),
                                     )
                                 )
                             ),
index e2ec497..d4e09f9 100644 (file)
@@ -456,6 +456,8 @@ class manager {
         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
         $records = $DB->get_records_select('task_scheduled', $where, $params);
 
+        $pluginmanager = \core_plugin_manager::instance();
+
         foreach ($records as $record) {
 
             if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
@@ -463,6 +465,17 @@ class manager {
                 $task = self::scheduled_task_from_record($record);
 
                 $task->set_lock($lock);
+
+                // See if the component is disabled.
+                $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
+
+                if ($plugininfo) {
+                    if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
+                        $lock->release();
+                        continue;
+                    }
+                }
+
                 if (!$task->is_blocking()) {
                     $cronlock->release();
                 } else {
index 47e93c7..ed2fc03 100644 (file)
@@ -183,6 +183,15 @@ abstract class scheduled_task extends task_base {
         return $this->disabled;
     }
 
+    /**
+     * Override this function if you want this scheduled task to run, even if the component is disabled.
+     *
+     * @return bool
+     */
+    public function get_run_if_component_disabled() {
+        return false;
+    }
+
     /**
      * Take a cron field definition and return an array of valid numbers with the range min-max.
      *
index d96f582..0bf08f6 100644 (file)
@@ -258,7 +258,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             return $coursecat;
         } else {
             if ($strictness == MUST_EXIST) {
-                throw new moodle_exception('unknowcategory');
+                throw new moodle_exception('unknowncategory');
             }
         }
         return null;
@@ -2441,18 +2441,26 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
     /**
      * Resorts the sub categories of this category by the given field.
      *
-     * @param string $field
+     * @param string $field One of name, idnumber or descending values of each (appended desc)
      * @param bool $cleanup If true cleanup will be done, if false you will need to do it manually later.
      * @return bool True on success.
      * @throws coding_exception
      */
     public function resort_subcategories($field, $cleanup = true) {
         global $DB;
+        $desc = false;
+        if (substr($field, -4) === "desc") {
+            $desc = true;
+            $field = substr($field, 0, -4);  // Remove "desc" from field name.
+        }
         if ($field !== 'name' && $field !== 'idnumber') {
             throw new coding_exception('Invalid field requested');
         }
         $children = $this->get_children();
         core_collator::asort_objects_by_property($children, $field, core_collator::SORT_NATURAL);
+        if (!empty($desc)) {
+            $children = array_reverse($children);
+        }
         $i = 1;
         foreach ($children as $cat) {
             $i++;
@@ -2481,14 +2489,19 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
     /**
      * Resort the courses within this category by the given field.
      *
-     * @param string $field One of fullname, shortname or idnumber
+     * @param string $field One of fullname, shortname, idnumber or descending values of each (appended desc)
      * @param bool $cleanup
      * @return bool True for success.
      * @throws coding_exception
      */
     public function resort_courses($field, $cleanup = true) {
         global $DB;
-        if ($field !== 'fullname' && $field !== 'shortname' && $field !== 'idnumber') {
+        $desc = false;
+        if (substr($field, -4) === "desc") {
+            $desc = true;
+            $field = substr($field, 0, -4);  // Remove "desc" from field name.
+        }
+        if ($field !== 'fullname' && $field !== 'shortname' && $field !== 'idnumber' && $field !== 'timecreated') {
             // This is ultra important as we use $field in an SQL statement below this.
             throw new coding_exception('Invalid field requested');
         }
@@ -2497,8 +2510,7 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
                   FROM {course} c
              LEFT JOIN {context} ctx ON ctx.instanceid = c.id
                  WHERE ctx.contextlevel = :ctxlevel AND
-                       c.category = :categoryid
-              ORDER BY c.{$field}, c.sortorder";
+                       c.category = :categoryid";
         $params = array(
             'ctxlevel' => CONTEXT_COURSE,
             'categoryid' => $this->id
@@ -2527,6 +2539,9 @@ class coursecat implements renderable, cacheable_object, IteratorAggregate {
             }
             // Sort the courses.
             core_collator::asort_objects_by_property($courses, 'sortby', core_collator::SORT_NATURAL);
+            if (!empty($desc)) {
+                $courses = array_reverse($courses);
+            }
             $i = 1;
             foreach ($courses as $course) {
                 $DB->set_field('course', 'sortorder', $this->sortorder + $i, array('id' => $course->id));
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 b65a36e..580896c 100644 (file)
@@ -1540,11 +1540,25 @@ abstract class enrol_plugin {
     /**
      * Is it possible to delete enrol instance via standard UI?
      *
+     * @deprecated since Moodle 2.8 MDL-35864 - please use can_delete_instance() instead.
+     * @todo MDL-46479 This will be deleted in Moodle 3.0.
+     * @see class_name::can_delete_instance()
      * @param object $instance
      * @return bool
      */
     public function instance_deleteable($instance) {
-        return true;
+        debugging('Function enrol_plugin::instance_deleteable() is deprecated', DEBUG_DEVELOPER);
+        return $this->can_delete_instance($instance);
+    }
+
+    /**
+     * Is it possible to delete enrol instance via standard UI?
+     *
+     * @param object $instance
+     * @return bool
+     */
+    public function can_delete_instance($instance) {
+        return false;
     }
 
     /**
index f6a3316..9450aba 100644 (file)
@@ -1166,7 +1166,8 @@ function format_postdata_for_curlcall($postdata) {
  * @param string $tofile store the downloaded content to file instead of returning it.
  * @param bool $calctimeout false by default, true enables an extra head request to try and determine
  *   filesize and appropriately larger timeout based on $CFG->curltimeoutkbitrate
- * @return mixed false if request failed or content of the file as string if ok. True if file downloaded into $tofile successfully.
+ * @return stdClass|string|bool stdClass object if $fullresponse is true, false if request failed, true
+ *   if file downloaded into $tofile successfully or the file content as a string.
  */
 function download_file_content($url, $headers=null, $postdata=null, $fullresponse=false, $timeout=300, $connecttimeout=20, $skipcertverify=false, $tofile=NULL, $calctimeout=false) {
     global $CFG;
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 7abfb70..6018d1a 100644 (file)
@@ -1329,6 +1329,8 @@ function html_is_blank($string) {
  *
  * A NULL value will delete the entry.
  *
+ * NOTE: this function is called from lib/db/upgrade.php
+ *
  * @param string $name the key to set
  * @param string $value the value to set (without magic quotes)
  * @param string $plugin (optional) the plugin scope, default null
@@ -1399,6 +1401,8 @@ function set_config($name, $value, $plugin=null) {
  * If called with 2 parameters it will return a string single
  * value or false if the value is not found.
  *
+ * NOTE: this function is called from lib/db/upgrade.php
+ *
  * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
  *     that we need only fetch it once per request.
  * @param string $plugin full component name
@@ -1485,6 +1489,8 @@ function get_config($plugin, $name = null) {
 /**
  * Removes a key from global configuration.
  *
+ * NOTE: this function is called from lib/db/upgrade.php
+ *
  * @param string $name the key to set
  * @param string $plugin (optional) the plugin scope
  * @return boolean whether the operation succeeded.
index 76b0c83..9bf8408 100644 (file)
@@ -174,6 +174,11 @@ class user_picture implements renderable {
      */
     public $class = 'userpicture';
 
+    /**
+     * @var bool Whether to be visible to screen readers.
+     */
+    public $visibletoscreenreaders = true;
+
     /**
      * User picture constructor.
      *
index 9da96cb..bbc616f 100644 (file)
@@ -827,6 +827,10 @@ class core_renderer extends renderer_base {
             $this->page->add_body_class('userloggedinas');
         }
 
+        if (is_role_switched($this->page->course->id)) {
+            $this->page->add_body_class('userswitchedrole');
+        }
+
         // Give themes a chance to init/alter the page object.
         $this->page->theme->init_page($this->page);
 
@@ -2288,6 +2292,7 @@ class core_renderer extends renderer_base {
      *     - popup=false (open in popup)
      *     - alttext=true (add image alt attribute)
      *     - class = image class attribute (default 'userpicture')
+     *     - visibletoscreenreaders=true (whether to be visible to screen readers)
      * @return string HTML fragment
      */
     public function user_picture(stdClass $user, array $options = null) {
@@ -2338,6 +2343,9 @@ class core_renderer extends renderer_base {
         $src = $userpicture->get_url($this->page, $this);
 
         $attributes = array('src'=>$src, 'alt'=>$alt, 'title'=>$alt, 'class'=>$class, 'width'=>$size, 'height'=>$size);
+        if (!$userpicture->visibletoscreenreaders) {
+            $attributes['role'] = 'presentation';
+        }
 
         // get the image html output fisrt
         $output = html_writer::empty_tag('img', $attributes);
@@ -2360,6 +2368,11 @@ class core_renderer extends renderer_base {
         }
 
         $attributes = array('href'=>$url);
+        if (!$userpicture->visibletoscreenreaders) {
+            $attributes['role'] = 'presentation';
+            $attributes['tabindex'] = '-1';
+            $attributes['aria-hidden'] = 'true';
+        }
 
         if ($userpicture->popup) {
             $id = html_writer::random_id('userpicture');
index 5711157..f962480 100644 (file)
@@ -198,6 +198,48 @@ class core_collator_testcase extends advanced_testcase {
         $this->assertTrue($result);
     }
 
+    /**
+     * Tests the sorting of an array of arrays by key.
+     */
+    public function test_asort_array_of_arrays_by_key() {
+        $array = array(
+            'a' => array('name' => 'bravo'),
+            'b' => array('name' => 'charlie'),
+            'c' => array('name' => 'alpha')
+        );
+        $this->assertSame(array('a', 'b', 'c'), array_keys($array));
+        $this->assertTrue(core_collator::asort_array_of_arrays_by_key($array, 'name'));
+        $this->assertSame(array('c', 'a', 'b'), array_keys($array));
+
+        $array = array(
+            'a' => array('name' => 'b'),
+            'b' => array('name' => 1),
+            'c' => array('name' => 0)
+        );
+        $this->assertSame(array('a', 'b', 'c'), array_keys($array));
+        $this->assertTrue(core_collator::asort_array_of_arrays_by_key($array, 'name'));
+        $this->assertSame(array('c', 'b', 'a'), array_keys($array));
+
+        $array = array(
+            'a' => array('name' => 'áb'),
+            'b' => array('name' => 'ab'),
+            1   => array('name' => 'aa'),
+            'd' => array('name' => 'cc'),
+            0   => array('name' => 'Áb')
+        );
+        $this->assertSame(array('a', 'b', 1, 'd', 0), array_keys($array));
+        $this->assertTrue(core_collator::asort_array_of_arrays_by_key($array, 'name'));
+        $this->assertSame(array(1, 'b', 'a', 0, 'd'), array_keys($array));
+        $this->assertSame(array(
+            1   => array('name' => 'aa'),
+            'b' => array('name' => 'ab'),
+            'a' => array('name' => 'áb'),
+            0   => array('name' => 'Áb'),
+            'd' => array('name' => 'cc')
+        ), $array);
+
+    }
+
     /**
      * Returns an array of sorted names.
      * @param array $objects
index b4b8180..0aa637b 100644 (file)
@@ -395,25 +395,29 @@ class core_coursecatlib_testcase extends advanced_testcase {
             'category' => $category->id,
             'idnumber' => '006-01',
             'shortname' => 'Biome Study',
-            'fullname' => '<span lang="ar" class="multilang">'.'دراسة منطقة إحيائية'.'</span><span lang="en" class="multilang">Biome Study</span>'
+            'fullname' => '<span lang="ar" class="multilang">'.'دراسة منطقة إحيائية'.'</span><span lang="en" class="multilang">Biome Study</span>',
+            'timecreated' => '10000000001'
         ));
         $course2 = $generator->create_course(array(
             'category' => $category->id,
             'idnumber' => '007-02',
             'shortname' => 'Chemistry Revision',
-            'fullname' => 'Chemistry Revision'
+            'fullname' => 'Chemistry Revision',
+            'timecreated' => '10000000002'
         ));
         $course3 = $generator->create_course(array(
             'category' => $category->id,
             'idnumber' => '007-03',
             'shortname' => 'Swiss Rolls and Sunflowers',
-            'fullname' => 'Aarkvarks guide to Swiss Rolls and Sunflowers'
+            'fullname' => 'Aarkvarks guide to Swiss Rolls and Sunflowers',
+            'timecreated' => '10000000003'
         ));
         $course4 = $generator->create_course(array(
             'category' => $category->id,
             'idnumber' => '006-04',
             'shortname' => 'Scratch',
-            'fullname' => '<a href="test.php">Basic Scratch</a>'
+            'fullname' => '<a href="test.php">Basic Scratch</a>',
+            'timecreated' => '10000000004'
         ));
         $c1 = (int)$course1->id;
         $c2 = (int)$course2->id;
@@ -427,6 +431,8 @@ class core_coursecatlib_testcase extends advanced_testcase {
         $this->assertTrue($coursecat->resort_courses('shortname'));
         $this->assertSame(array($c1, $c2, $c4, $c3), array_keys($coursecat->get_courses()));
 
+        $this->assertTrue($coursecat->resort_courses('timecreated'));
+        $this->assertSame(array($c1, $c2, $c3, $c4), array_keys($coursecat->get_courses()));
 
         try {
             // Enable the multilang filter and set it to apply to headings and content.
index a759287..0d80195 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-debug.js differ
index 9ee43d1..65fff2d 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock-min.js differ
index 528e73b..90dc406 100644 (file)
Binary files a/lib/yui/build/moodle-core-dock/moodle-core-dock.js and b/lib/yui/build/moodle-core-dock/moodle-core-dock.js differ
index 1a7edfc..50ad9a9 100644 (file)
@@ -965,7 +965,7 @@ DOCK.prototype = {
         }
 
         if (this.get('position') === 'right') {
-            panel.get('node').setStyle('left', -panel.get('offsetWidth')+'px');
+            panel.get('node').setStyle('left', '-' + panel.get('node').get('offsetWidth') + 'px');
 
         } else if (this.get('position') === 'top') {
             dockx = this.get('dockNode').getX();
index bae2d7c..7f212c3 100644 (file)
@@ -43,7 +43,6 @@ if (empty($CFG->authloginviaemail)) {
             <div class="form-label"><label for="password"><?php print_string("password") ?></label></div>
             <div class="form-input">
               <input type="password" name="password" id="password" size="15" value="" <?php echo $autocomplete; ?> />
-              <input type="submit" id="loginbtn" value="<?php print_string("login") ?>" />
             </div>
           </div>
             <div class="clearer"><!-- --></div>
@@ -54,6 +53,7 @@ if (empty($CFG->authloginviaemail)) {
               </div>
               <?php } ?>
           <div class="clearer"><!-- --></div>
+          <input type="submit" id="loginbtn" value="<?php print_string("login") ?>" />
           <div class="forgetpass"><a href="forgot_password.php"><?php print_string("forgotten") ?></a></div>
         </form>
         <div class="desc">
index 3d07191..2ccb032 100644 (file)
@@ -140,6 +140,7 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
 
         // Define id annotations.
         $userflag->annotate_ids('user', 'userid');
+        $userflag->annotate_ids('user', 'allocatedmarker');
         $submission->annotate_ids('user', 'userid');
         $submission->annotate_ids('group', 'groupid');
         $grade->annotate_ids('user', 'userid');
index 63fd2e5..4096629 100644 (file)
@@ -54,6 +54,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
             $grade = new restore_path_element('assign_grade', '/activity/assign/grades/grade');
             $paths[] = $grade;
             $this->add_subplugin_structure('assignfeedback', $grade);
+            $userflag = new restore_path_element('assign_userflag',
+                                                   '/activity/assign/userflags/userflag');
+            $paths[] = $userflag;
         }
         $paths[] = new restore_path_element('assign_plugin_config',
                                             '/activity/assign/plugin_configs/plugin_config');
@@ -145,7 +148,7 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
      * @param object $data The data in object form
      * @return void
      */
-    protected function process_assign_userflags($data) {
+    protected function process_assign_userflag($data) {
         global $DB;
 
         $data = (object)$data;
@@ -154,6 +157,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
         $data->assignment = $this->get_new_parentid('assign');
 
         $data->userid = $this->get_mappingid('user', $data->userid);
+        if (!empty($data->allocatedmarker)) {
+            $data->allocatedmarker = $this->get_mappingid('user', $data->allocatedmarker);
+        }
         if (!empty($data->extensionduedate)) {
             $data->extensionduedate = $this->apply_date_offset($data->extensionduedate);
         } else {
@@ -182,19 +188,24 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
         $data->userid = $this->get_mappingid('user', $data->userid);
         $data->grader = $this->get_mappingid('user', $data->grader);
 
-        // Handle flags restore to a different table.
-        $flags = new stdClass();
-        $flags->assignment = $this->get_new_parentid('assign');
-        if (!empty($data->extensionduedate)) {
-            $flags->extensionduedate = $this->apply_date_offset($data->extensionduedate);
-        }
-        if (!empty($data->mailed)) {
-            $flags->mailed = $data->mailed;
-        }
-        if (!empty($data->locked)) {
-            $flags->locked = $data->locked;
+        // Handle flags restore to a different table (for upgrade from old backups).
+        if (!empty($data->extensionduedate) ||
+                !empty($data->mailed) ||
+                !empty($data->locked)) {
+            $flags = new stdClass();
+            $flags->assignment = $this->get_new_parentid('assign');
+            if (!empty($data->extensionduedate)) {
+                $flags->extensionduedate = $this->apply_date_offset($data->extensionduedate);
+            }
+            if (!empty($data->mailed)) {
+                $flags->mailed = $data->mailed;
+            }
+            if (!empty($data->locked)) {
+                $flags->locked = $data->locked;
+            }
+            $flags->userid = $this->get_mappingid('user', $data->userid);
+            $DB->insert_record('assign_user_flags', $flags);
         }
-        $DB->insert_record('assign_user_flags', $flags);
 
         $newitemid = $DB->insert_record('assign_grades', $data);
 
index f61579c..e58da85 100644 (file)
@@ -169,5 +169,14 @@ $capabilities = array(
         )
     ),
 
+    'mod/assign:receivegradernotifications' => array(
+        'captype' => 'read',
+        'contextlevel' => CONTEXT_MODULE,
+        'archetypes' => array(
+            'teacher' => CAP_ALLOW,
+            'editingteacher' => CAP_ALLOW,
+            'manager' => CAP_ALLOW
+        )
+    ),
 );
 
index a911fc8..4fc211f 100644 (file)
@@ -47,6 +47,7 @@ $string['assign:grade'] = 'Grade assignment';
 $string['assign:grantextension'] = 'Grant extension';
 $string['assign:manageallocations'] = 'Manage markers allocated to submissions';
 $string['assign:managegrades'] = 'Review and release grades';
+$string['assign:receivegradernotifications'] = 'Receive grader submission notifications';
 $string['assign:releasegrades'] = 'Release grades';
 $string['assign:revealidentities'] = 'Reveal student identities';
 $string['assign:reviewgrades'] = 'Review grades';
@@ -313,6 +314,7 @@ $string['quickgrading'] = 'Quick grading';
 $string['quickgradingresult'] = 'Quick grading';
 $string['quickgradingchangessaved'] = 'The grade changes were saved';
 $string['quickgrading_help'] = 'Quick grading allows you to assign grades (and outcomes) directly in the submissions table. Quick grading is not compatible with advanced grading and is not recommended when there are multiple markers.';
+$string['reopenuntilpassincompatiblewithblindmarking'] = 'Reopen until pass option is incompatible with blind marking, because the grades are not released to the gradebook until the student identities are revealed.';
 $string['requiresubmissionstatement'] = 'Require that students accept the submission statement';
 $string['requiresubmissionstatement_help'] = 'Require that students accept the submission statement for all submissions to this assignment.';
 $string['requireallteammemberssubmit'] = 'Require all group members submit';
index 53413e9..46404bc 100644 (file)
@@ -865,10 +865,17 @@ class assign {
             // We need to remove the links to files as the calendar is not ready
             // to support module events with file areas.
             $intro = strip_pluginfile_content($intro);
-            $event->description = array(
-                'text' => $intro,
-                'format' => $instance->introformat
-            );
+            if ($this->show_intro()) {
+                $event->description = array(
+                    'text' => $intro,
+                    'format' => $instance->introformat
+                );
+            } else {
+                $event->description = array(
+                    'text' => '',
+                    'format' => $instance->introformat
+                );
+            }
 
             if ($event->id) {
                 $calendarevent = calendar_event::load($event->id);
@@ -1588,6 +1595,7 @@ class assign {
         // Only ever send a max of one days worth of updates.
         $yesterday = time() - (24 * 3600);
         $timenow   = time();
+        $lastcron = $DB->get_field('modules', 'lastcron', array('name'=>'mod_assign'));
 
         // Collect all submissions from the past 24 hours that require mailing.
         // Submissions are excluded if the assignment is hidden in the gradebook.
@@ -1606,143 +1614,159 @@ class assign {
         $params = array('yesterday' => $yesterday, 'today' => $timenow);
         $submissions = $DB->get_records_sql($sql, $params);
 
-        if (empty($submissions)) {
-            return true;
-        }
+        if (!empty($submissions)) {
 
-        mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
+            mtrace('Processing ' . count($submissions) . ' assignment submissions ...');
 
-        // Preload courses we are going to need those.
-        $courseids = array();
-        foreach ($submissions as $submission) {
-            $courseids[] = $submission->course;
-        }
+            // Preload courses we are going to need those.
+            $courseids = array();
+            foreach ($submissions as $submission) {
+                $courseids[] = $submission->course;
+            }
 
-        // Filter out duplicates.
-        $courseids = array_unique($courseids);
-        $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
-        list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
-        $sql = 'SELECT c.*, ' . $ctxselect .
-                  ' FROM {course} c
-             LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
-                 WHERE c.id ' . $courseidsql;
+            // Filter out duplicates.
+            $courseids = array_unique($courseids);
+            $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+            list($courseidsql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
+            $sql = 'SELECT c.*, ' . $ctxselect .
+                      ' FROM {course} c
+                 LEFT JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel
+                     WHERE c.id ' . $courseidsql;
 
-        $params['contextlevel'] = CONTEXT_COURSE;
-        $courses = $DB->get_records_sql($sql, $params);
+            $params['contextlevel'] = CONTEXT_COURSE;
+            $courses = $DB->get_records_sql($sql, $params);
 
-        // Clean up... this could go on for a while.
-        unset($courseids);
-        unset($ctxselect);
-        unset($courseidsql);
-        unset($params);
+            // Clean up... this could go on for a while.
+            unset($courseids);
+            unset($ctxselect);
+            unset($courseidsql);
+            unset($params);
 
-        // Simple array we'll use for caching modules.
-        $modcache = array();
+            // Simple array we'll use for caching modules.
+            $modcache = array();
 
-        // Message students about new feedback.
-        foreach ($submissions as $submission) {
+            // Message students about new feedback.
+            foreach ($submissions as $submission) {
 
-            mtrace("Processing assignment submission $submission->id ...");
+                mtrace("Processing assignment submission $submission->id ...");
 
-            // Do not cache user lookups - could be too many.
-            if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
-                mtrace('Could not find user ' . $submission->userid);
-                continue;
-            }
+                // Do not cache user lookups - could be too many.
+                if (!$user = $DB->get_record('user', array('id'=>$submission->userid))) {
+                    mtrace('Could not find user ' . $submission->userid);
+                    continue;
+                }
 
-            // Use a cache to prevent the same DB queries happening over and over.
-            if (!array_key_exists($submission->course, $courses)) {
-                mtrace('Could not find course ' . $submission->course);
-                continue;
-            }
-            $course = $courses[$submission->course];
-            if (isset($course->ctxid)) {
-                // Context has not yet been preloaded. Do so now.
-                context_helper::preload_from_record($course);
-            }
+                // Use a cache to prevent the same DB queries happening over and over.
+                if (!array_key_exists($submission->course, $courses)) {
+                    mtrace('Could not find course ' . $submission->course);
+                    continue;
+                }
+                $course = $courses[$submission->course];
+                if (isset($course->ctxid)) {
+                    // Context has not yet been preloaded. Do so now.
+                    context_helper::preload_from_record($course);
+                }
 
-            // Override the language and timezone of the "current" user, so that
-            // mail is customised for the receiver.
-            cron_setup_user($user, $course);
+                // Override the language and timezone of the "current" user, so that
+                // mail is customised for the receiver.
+                cron_setup_user($user, $course);
+
+                // Context lookups are already cached.
+                $coursecontext = context_course::instance($course->id);
+                if (!is_enrolled($coursecontext, $user->id)) {
+                    $courseshortname = format_string($course->shortname,
+                                                     true,
+                                                     array('context' => $coursecontext));
+                    mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
+                    continue;
+                }
 
-            // Context lookups are already cached.
-            $coursecontext = context_course::instance($course->id);
-            if (!is_enrolled($coursecontext, $user->id)) {
-                $courseshortname = format_string($course->shortname,
-                                                 true,
-                                                 array('context' => $coursecontext));
-                mtrace(fullname($user) . ' not an active participant in ' . $courseshortname);
-                continue;
-            }
+                if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
+                    mtrace('Could not find grader ' . $submission->grader);
+                    continue;
+                }
 
-            if (!$grader = $DB->get_record('user', array('id'=>$submission->grader))) {
-                mtrace('Could not find grader ' . $submission->grader);
-                continue;
-            }
+                if (!array_key_exists($submission->assignment, $modcache)) {
+                    $mod = get_coursemodule_from_instance('assign', $submission->assignment, $course->id);
+                    if (empty($mod)) {
+                        mtrace('Could not find course module for assignment id ' . $submission->assignment);
+                        continue;
+                    }
+                    $modcache[$submission->assignment] = $mod;
+                } else {
+                    $mod = $modcache[$submission->assignment];
+                }
+                // Context lookups are already cached.
+                $contextmodule = context_module::instance($mod->id);
 
-            if (!array_key_exists($submission->assignment, $modcache)) {
-                $mod = get_coursemodule_from_instance('assign', $submission->assignment, $course->id);
-                if (empty($mod)) {
-                    mtrace('Could not find course module for assignment id ' . $submission->assignment);
+                if (!$mod->visible) {
+                    // Hold mail notification for hidden assignments until later.
                     continue;
                 }
-                $modcache[$submission->assignment] = $mod;
-            } else {
-                $mod = $modcache[$submission->assignment];
-            }
-            // Context lookups are already cached.
-            $contextmodule = context_module::instance($mod->id);
 
-            if (!$mod->visible) {
-                // Hold mail notification for hidden assignments until later.
-                continue;
-            }
+                // Need to send this to the student.
+                $messagetype = 'feedbackavailable';
+                $eventtype = 'assign_notification';
+                $updatetime = $submission->lastmodified;
+                $modulename = get_string('modulename', 'assign');
 
-            // Need to send this to the student.
-            $messagetype = 'feedbackavailable';
-            $eventtype = 'assign_notification';
-            $updatetime = $submission->lastmodified;
-            $modulename = get_string('modulename', 'assign');
-
-            $uniqueid = 0;
-            if ($submission->blindmarking && !$submission->revealidentities) {
-                $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
-            }
-            $showusers = $submission->blindmarking && !$submission->revealidentities;
-            self::send_assignment_notification($grader,
-                                               $user,
-                                               $messagetype,
-                                               $eventtype,
-                                               $updatetime,
-                                               $mod,
-                                               $contextmodule,
-                                               $course,
-                                               $modulename,
-                                               $submission->name,
-                                               $showusers,
-                                               $uniqueid);
-
-            $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
-            if ($flags) {
-                $flags->mailed = 1;
-                $DB->update_record('assign_user_flags', $flags);
-            } else {
-                $flags = new stdClass();
-                $flags->userid = $user->id;
-                $flags->assignment = $submission->assignment;
-                $flags->mailed = 1;
-                $DB->insert_record('assign_user_flags', $flags);
+                $uniqueid = 0;
+                if ($submission->blindmarking && !$submission->revealidentities) {
+                    $uniqueid = self::get_uniqueid_for_user_static($submission->assignment, $user->id);
+                }
+                $showusers = $submission->blindmarking && !$submission->revealidentities;
+                self::send_assignment_notification($grader,
+                                                   $user,
+                                                   $messagetype,
+                                                   $eventtype,
+                                                   $updatetime,
+                                                   $mod,
+                                                   $contextmodule,
+                                                   $course,
+                                                   $modulename,
+                                                   $submission->name,
+                                                   $showusers,
+                                                   $uniqueid);
+
+                $flags = $DB->get_record('assign_user_flags', array('userid'=>$user->id, 'assignment'=>$submission->assignment));
+                if ($flags) {
+                    $flags->mailed = 1;
+                    $DB->update_record('assign_user_flags', $flags);
+                } else {
+                    $flags = new stdClass();
+                    $flags->userid = $user->id;
+                    $flags->assignment = $submission->assignment;
+                    $flags->mailed = 1;
+                    $DB->insert_record('assign_user_flags', $flags);
+                }
+
+                mtrace('Done');
             }
+            mtrace('Done processing ' . count($submissions) . ' assignment submissions');
+
+            cron_setup_user();
 
-            mtrace('Done');
+            // Free up memory just to be sure.
+            unset($courses);
+            unset($modcache);
         }
-        mtrace('Done processing ' . count($submissions) . ' assignment submissions');
 
-        cron_setup_user();
+        // Update calendar events to provide a description.
+        $sql = 'SELECT id
+                    FROM {assign}
+                    WHERE
+                        allowsubmissionsfromdate >= :lastcron AND
+                        allowsubmissionsfromdate <= :timenow AND
+                        alwaysshowdescription = 0';
+        $params = array('lastcron' => $lastcron, 'timenow' => $timenow);
+        $newlyavailable = $DB->get_records_sql($sql, $params);
+        foreach ($newlyavailable as $record) {
+            $cm = get_coursemodule_from_instance('assign', $record->id, 0, false, MUST_EXIST);
+            $context = context_module::instance($cm->id);
 
-        // Free up memory just to be sure.
-        unset($courses);
-        unset($modcache);
+            $assignment = new assign($context, null, null);
+            $assignment->update_calendar($cm->id);
+        }
 
         return true;
     }
@@ -1864,12 +1888,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 {
@@ -2303,9 +2327,9 @@ class assign {
     }
 
     /**
-     * Display a continue page.
+     * Display a continue page after grading.
      *
-     * @param string $message - The message to display
+     * @param string $message - The message to display.
      * @return string
      */
     protected function view_savegrading_result($message) {
@@ -2323,9 +2347,9 @@ class assign {
         return $o;
     }
     /**
-     * Display a grading error.
+     * Display a continue page after quickgrading.
      *
-     * @param string $message - The description of the result
+     * @param string $message - The message to display.
      * @return string
      */
     protected function view_quickgrading_result($message) {
@@ -4447,6 +4471,58 @@ class assign {
         return $graders;
     }
 
+    /**
+     * Returns a list of users that should receive notification about given submission.
+     *
+     * @param int $userid The submission to grade
+     * @return array
+     */
+    protected function get_notifiable_users($userid) {
+        // Potential users should be active users only.
+        $potentialusers = get_enrolled_users($this->context, "mod/assign:receivegradernotifications",
+                                             null, 'u.*', null, null, null, true);
+
+        $notifiableusers = array();
+        if (groups_get_activity_groupmode($this->get_course_module()) == SEPARATEGROUPS) {
+            if ($groups = groups_get_all_groups($this->get_course()->id, $userid, $this->get_course_module()->groupingid)) {
+                foreach ($groups as $group) {
+                    foreach ($potentialusers as $potentialuser) {
+                        if ($potentialuser->id == $userid) {
+                            // Do not send self.
+                            continue;
+                        }
+                        if (groups_is_member($group->id, $potentialuser->id)) {
+                            $notifiableusers[$potentialuser->id] = $potentialuser;
+                        }
+                    }
+                }
+            } else {
+                // User not in group, try to find graders without group.
+                foreach ($potentialusers as $potentialuser) {
+                    if ($potentialuser->id == $userid) {
+                        // Do not send self.
+                        continue;
+                    }
+                    if (!groups_has_membership($this->get_course_module(), $potentialuser->id)) {
+                        $notifiableusers[$potentialuser->id] = $potentialuser;
+                    }
+                }
+            }
+        } else {
+            foreach ($potentialusers as $potentialuser) {
+                if ($potentialuser->id == $userid) {
+                    // Do not send self.
+                    continue;
+                }
+                // Must be enrolled.
+                if (is_enrolled($this->get_course_context(), $potentialuser->id)) {
+                    $notifiableusers[$potentialuser->id] = $potentialuser;
+                }
+            }
+        }
+        return $notifiableusers;
+    }
+
     /**
      * Format a notification for plain text.
      *
@@ -4705,10 +4781,11 @@ class assign {
         } else {
             $user = $USER;
         }
-        if ($teachers = $this->get_graders($user->id)) {
-            foreach ($teachers as $teacher) {
+
+        if ($notifyusers = $this->get_notifiable_users($user->id)) {
+            foreach ($notifyusers as $notifyuser) {
                 $this->send_notification($user,
-                                         $teacher,
+                                         $notifyuser,
                                          'gradersubmissionupdated',
                                          'assign_notification',
                                          $submission->timemodified);
@@ -5340,8 +5417,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);
@@ -6451,31 +6532,12 @@ class assign {
         $shouldreopen = false;
         if ($instance->attemptreopenmethod == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
             // Check the gradetopass from the gradebook.
-            $gradinginfo = grade_get_grades($this->get_course()->id,
-                                            'mod',
-                                            'assign',
-                                            $instance->id,
-                                            $userid);
+            $gradeitem = $this->get_grade_item();
+            if ($gradeitem) {
+                $gradegrade = grade_grade::fetch(array('userid' => $userid, 'itemid' => $gradeitem->id));
 
-            // What do we do if the grade has not been added to the gradebook (e.g. blind marking)?
-            $gradingitem = null;
-            $gradebookgrade = null;
-            if (isset($gradinginfo->items[0])) {
-                $gradingitem = $gradinginfo->items[0];
-                $gradebookgrade = $gradingitem->grades[$userid];
-            }
-
-            if ($gradebookgrade) {
-                // TODO: This code should call grade_grade->is_passed().
-                $shouldreopen = true;
-                if (is_null($gradebookgrade->grade)) {
-                    $shouldreopen = false;
-                }
-                if (empty($gradingitem->gradepass) || $gradingitem->gradepass == $gradingitem->grademin) {
-                    $shouldreopen = false;
-                }
-                if ($gradebookgrade->grade >= $gradingitem->gradepass) {
-                    $shouldreopen = false;
+                if ($gradegrade && !$gradegrade->is_passed()) {
+                    $shouldreopen = true;
                 }
             }
         }
index eead2ae..5fd4ccd 100644 (file)
@@ -241,6 +241,9 @@ class mod_assign_mod_form extends moodleform_mod {
                 $errors['cutoffdate'] = get_string('cutoffdatefromdatevalidation', 'assign');
             }
         }
+        if ($data['blindmarking'] && $data['attemptreopenmethod'] == ASSIGN_ATTEMPT_REOPEN_METHOD_UNTILPASS) {
+            $errors['attemptreopenmethod'] = get_string('reopenuntilpassincompatiblewithblindmarking', 'assign');
+        }
 
         return $errors;
     }
index c4e2598..3758f30 100644 (file)
@@ -65,17 +65,21 @@ class assign_gradingmessage implements renderable {
     public $message = '';
     /** @var int $coursemoduleid */
     public $coursemoduleid = 0;
+    /** @var int $gradingerror should be set true if there was a problem grading */
+    public $gradingerror = null;
 
     /**
      * Constructor
      * @param string $heading This is the heading to display
      * @param string $message This is the message to display
+     * @param bool $gradingerror Set to true to display the message as an error.
      * @param int $coursemoduleid
      */
-    public function __construct($heading, $message, $coursemoduleid) {
+    public function __construct($heading, $message, $coursemoduleid, $gradingerror = false) {
         $this->heading = $heading;
         $this->message = $message;
         $this->coursemoduleid = $coursemoduleid;
+        $this->gradingerror = $gradingerror;
     }
 
 }
index d371eb0..f6037a4 100644 (file)
@@ -92,10 +92,11 @@ class mod_assign_renderer extends plugin_renderer_base {
     public function render_assign_gradingmessage(assign_gradingmessage $result) {
         $urlparams = array('id' => $result->coursemoduleid, 'action'=>'grading');
         $url = new moodle_url('/mod/assign/view.php', $urlparams);
+        $classes = $result->gradingerror ? 'notifyproblem' : 'notifysuccess';
 
         $o = '';
         $o .= $this->output->heading($result->heading, 4);
-        $o .= $this->output->notification($result->message);
+        $o .= $this->output->notification($result->message, $classes);
         $o .= $this->output->continue_button($url);
         return $o;
     }
index 6e65b49..fb3d213 100644 (file)
@@ -305,6 +305,10 @@ class testable_assign extends assign {
         return parent::get_graders($userid);
     }
 
+    public function testable_get_notifiable_users($userid) {
+        return parent::get_notifiable_users($userid);
+    }
+
     public function testable_view_batch_set_workflow_state($selectedusers) {
         $mform = $this->testable_grading_batch_operations_form('setmarkingworkflowstate', $selectedusers);
         return parent::view_batch_set_workflow_state($mform);
index d1cbbbc..1a203d2 100644 (file)
@@ -502,6 +502,25 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $event = $DB->get_record('event', $params);
         $this->assertNotEmpty($event);
         $this->assertSame('new', $event->description);     // The pluginfile links are removed.
+
+        // Create an assignment with a description that should be hidden.
+        $assign = $this->create_instance(array('duedate'=>$now + 160,
+                                               'alwaysshowdescription'=>false,
+                                               'allowsubmissionsfromdate'=>$now+3,
+                                               'intro'=>'Some text'));
+
+        // Get the event from the calendar.
+        $params = array('modulename'=>'assign', 'instance'=>$assign->get_instance()->id);
+        $event = $DB->get_record('event', $params);
+
+        $this->assertEmpty($event->description);
+        sleep(6);
+        // Run cron to update the event in the calendar.
+        assign::cron();
+        $event = $DB->get_record('event', $params);
+
+        $this->assertContains('Some text', $event->description);
+
     }
 
     public function test_update_instance() {
@@ -587,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]);
@@ -1065,6 +1116,58 @@ class mod_assign_locallib_testcase extends mod_assign_base_testcase {
         $this->assertCount(10, $assign->testable_get_graders($this->students[1]->id));
     }
 
+    public function test_get_notified_users() {
+        global $CFG, $DB;
+
+        $capability = 'mod/assign:receivegradernotifications';
+        $coursecontext = context_course::instance($this->course->id);
+        $role = $DB->get_record('role', array('shortname' => 'teacher'));
+
+        $this->create_extra_users();
+        $this->setUser($this->editingteachers[0]);
+
+        // Create an assignment with no groups.
+        $assign = $this->create_instance();
+
+        $this->assertCount(self::DEFAULT_TEACHER_COUNT +
+                           self::DEFAULT_EDITING_TEACHER_COUNT +
+                           self::EXTRA_TEACHER_COUNT +
+                           self::EXTRA_EDITING_TEACHER_COUNT,
+                           $assign->testable_get_notifiable_users($this->students[0]->id));
+
+        // Change nonediting teachers role to not receive grader notifications.
+        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
+
+        $this->assertCount(self::DEFAULT_EDITING_TEACHER_COUNT +
+                           self::EXTRA_EDITING_TEACHER_COUNT,
+                           $assign->testable_get_notifiable_users($this->students[0]->id));
+
+        // Reset nonediting teachers role to default.
+        unassign_capability($capability, $role->id, $coursecontext);
+
+        // Force create an assignment with SEPARATEGROUPS.
+        $data = new stdClass();
+        $data->courseid = $this->course->id;
+        $data->name = 'Grouping';
+        $groupingid = groups_create_grouping($data);
+        groups_assign_grouping($groupingid, $this->groups[0]->id);
+        $assign = $this->create_instance(array('groupingid' => $groupingid, 'groupmode' => SEPARATEGROUPS));
+
+        $this->setUser($this->students[1]);
+        $this->assertCount(4, $assign->testable_get_notifiable_users($this->students[0]->id));
+        // Note the second student is in a group that is not in the grouping.
+        // This means that we get all graders that are not in a group in the grouping.
+        $this->assertCount(10, $assign->testable_get_notifiable_users($this->students[1]->id));
+
+        // Change nonediting teachers role to not receive grader notifications.
+        assign_capability($capability, CAP_PROHIBIT, $role->id, $coursecontext);
+
+        $this->assertCount(2, $assign->testable_get_notifiable_users($this->students[0]->id));
+        // Note the second student is in a group that is not in the grouping.
+        // This means that we get all graders that are not in a group in the grouping.
+        $this->assertCount(5, $assign->testable_get_notifiable_users($this->students[1]->id));
+    }
+
     public function test_group_members_only() {
         global $CFG;
 
index 447e4b0..b3ae6a5 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
-$plugin->version  = 2014051204;    // The current module version (Date: YYYYMMDDXX).
+$plugin->version  = 2014072200;    // The current module version (Date: YYYYMMDDXX).
 $plugin->requires = 2014050800;    // Requires this Moodle version.
 $plugin->cron     = 60;
 
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) {
@@