Merge branch 'MDL-57550-master' of git://github.com/danpoltawski/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 24 Jan 2017 06:57:53 +0000 (14:57 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 24 Jan 2017 06:57:53 +0000 (14:57 +0800)
31 files changed:
admin/tool/mobile/classes/api.php
admin/tool/mobile/lang/en/tool_mobile.php
admin/tool/mobile/settings.php
admin/tool/mobile/tests/externallib_test.php
admin/tool/mobile/version.php
auth/cas/config.html
auth/ldap/auth.php
auth/ldap/config.html
auth/ldap/lang/en/auth_ldap.php
composer.json
composer.lock
lib/enrollib.php
lib/ldaplib.php
mod/assign/feedback/offline/locallib.php
mod/forum/lib.php
mod/forum/tests/lib_test.php
mod/glossary/classes/external.php
mod/glossary/tests/external_test.php
mod/quiz/report/attemptsreport_table.php
mod/quiz/report/overview/tests/report_test.php
mod/resource/classes/external.php
mod/resource/db/services.php
mod/resource/tests/externallib_test.php
mod/resource/version.php
mod/scorm/locallib.php
mod/scorm/tests/behat/completion_condition_require_status.feature
mod/scorm/view.php
theme/boost/classes/output/core_renderer.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/modules.scss
user/index.php

index 4366823..c04bba3 100644 (file)
@@ -47,6 +47,8 @@ class api {
     const LOGIN_VIA_EMBEDDED_BROWSER = 3;
     /** @var int seconds an auto-login key will expire. */
     const LOGIN_KEY_TTL = 60;
+    /** @var str link to the custom strings documentation for the app */
+    const CUSTOM_STRINGS_DOC_URL = 'https://docs.moodle.org/en/Moodle_Mobile_language_strings_customisation';
 
     /**
      * Returns a list of Moodle plugins supporting the mobile app.
@@ -204,6 +206,7 @@ class api {
 
         if (empty($section) or $section == 'mobileapp') {
             $settings->tool_mobile_forcelogout = get_config('tool_mobile', 'forcelogout');
+            $settings->tool_mobile_customlangstrings = get_config('tool_mobile', 'customlangstrings');
         }
 
         return $settings;
index 5493521..2979bad 100644 (file)
@@ -26,6 +26,13 @@ $string['autologinkeygenerationlockout'] = 'Auto-login key generation is blocked
 $string['autologinnotallowedtoadmins'] = 'Auto-login is not allowed for site admins.';
 $string['clickheretolaunchtheapp'] = 'Click here if the app does not open automatically.';
 $string['configmobilecssurl'] = 'A CSS file to customise your mobile app interface.';
+$string['customlangstrings'] = 'Custom language strings';
+$string['customlangstrings_desc'] = 'Words and phrases displayed in the app can be customised here. Enter each custom language string on a new line with format: string identifier, custom language string and language code, separated by pipe characters. For example:
+<pre>
+mm.user.student|Learner|en
+mm.user.student|Aprendiz|es
+</pre>
+For a complete list of string identifiers and more information, see the <a href="{$a}">documentation page</a>.';
 $string['enablesmartappbanners'] = 'Enable Smart App Banners';
 $string['enablesmartappbanners_desc'] = 'This will display a banner promoting the Moodle Mobile app when visiting the site in Mobile Safari.';
 $string['forcedurlscheme'] = 'If you want to allow only your custom branded app to be opened via a browser window, then specify its URL scheme here; otherwise leave the field empty.';
index 45b4799..d129e5a 100644 (file)
@@ -92,6 +92,14 @@ if ($hassiteconfig) {
                     new lang_string('forcelogout', 'tool_mobile'),
                     new lang_string('forcelogout_desc', 'tool_mobile'), 0));
 
+        $temp->add(new admin_setting_heading('tool_mobile/language',
+                    new lang_string('language'), ''));
+
+        $temp->add(new admin_setting_configtextarea('tool_mobile/customlangstrings',
+                    new lang_string('customlangstrings', 'tool_mobile'),
+                    new lang_string('customlangstrings_desc', 'tool_mobile', tool_mobile\api::CUSTOM_STRINGS_DOC_URL),
+                    '', PARAM_RAW, '50', '10'));
+
         $ADMIN->add('mobileapp', $temp);
     }
 }
index bed4eb0..9b3c49d 100644 (file)
@@ -145,6 +145,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
             array('name' => 'disableuserimages', 'value' => $CFG->disableuserimages),
             array('name' => 'mygradesurl', 'value' => user_mygrades_url()->out(false)),
             array('name' => 'tool_mobile_forcelogout', 'value' => 0),
+            array('name' => 'tool_mobile_customlangstrings', 'value' => ''),
         );
         $this->assertCount(0, $result['warnings']);
         $this->assertEquals($expected, $result['settings']);
index b6a3b7b..a0ea73c 100644 (file)
@@ -23,7 +23,7 @@
  */
 
 defined('MOODLE_INTERNAL') || die();
-$plugin->version   = 2016120501; // The current plugin version (Date: YYYYMMDDXX).
+$plugin->version   = 2016120502; // The current plugin version (Date: YYYYMMDDXX).
 $plugin->requires  = 2016112900; // Requires this Moodle version.
 $plugin->component = 'tool_mobile'; // Full name of the plugin (used for diagnostics).
 $plugin->dependencies = array(
index c0ea85b..6279950 100644 (file)
@@ -99,9 +99,31 @@ if (!isset($config->removeuser)) {
 $yesno = array( get_string('no'), get_string('yes') );
 
 $disabled = '';
-if (!ldap_paged_results_supported($config->ldap_version)) {
+$pagedresultssupported = false;
+if ($config->host_url !== '') {
+    /**
+     * We try to connect each and every time we open the config, because we want to set the Page
+     * Size setting as enabled or disabled depending on the configured LDAP server supporting
+     * pagination or not, and to notify the user about it. If the user changed the LDAP server (or
+     * the LDAP protocol version) last time, it might happen that paged results are no longer
+     * available and we want to show that to the user the next time she goes to the settings page.
+     */
+    try {
+        $ldapconn = $this->ldap_connect();
+        $pagedresultssupported = ldap_paged_results_supported($config->ldap_version, $ldapconn);
+    } catch (Exception $e) {
+        // If we couldn't connect and get the supported options, we can only assume we don't support paged results.
+        $pagedresultssupported = false;
+    }
+}
+/* Make sure we only disable the paged result size setting and show the notification about it if
+ * there is a configured server that we tried to contact.  Othersiwe, if someone's LDAP server does
+ * support paged results, they won't be able to turn it on the first time they set it up (because
+ * the field will be disabled).
+ */
+if (($config->host_url !== '') && (!$pagedresultssupported)) {
     $disabled = ' disabled="disabled"';
-    echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'));
+    echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
 }
 
 ?>
index f80d6b3..b23c9ec 100644 (file)
@@ -695,7 +695,7 @@ class auth_plugin_ldap extends auth_plugin_base {
             array_push($contexts, $this->config->create_context);
         }
 
-        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version);
+        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection);
         $ldap_cookie = '';
         foreach ($contexts as $context) {
             $context = trim($context);
@@ -1540,7 +1540,7 @@ class auth_plugin_ldap extends auth_plugin_base {
         }
 
         $ldap_cookie = '';
-        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version);
+        $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection);
         foreach ($contexts as $context) {
             $context = trim($context);
             if (empty($context)) {
index 422f474..883eb9d 100644 (file)
@@ -117,9 +117,31 @@ $fastpathoptions = array(AUTH_NTLM_FASTPATH_YESFORM => get_string('auth_ntlmsso_
                          AUTH_NTLM_FASTPATH_ATTEMPT => get_string('auth_ntlmsso_ie_fastpath_attempt', 'auth_ldap'));
 
 $disabled = '';
-if (!ldap_paged_results_supported($config->ldap_version)) {
+$pagedresultssupported = false;
+if ($config->host_url !== '') {
+    /**
+     * We try to connect each and every time we open the config, because we want to set the Page
+     * Size setting as enabled or disabled depending on the configured LDAP server supporting
+     * pagination or not, and to notify the user about it. If the user changed the LDAP server (or
+     * the LDAP protocol version) last time, it might happen that paged results are no longer
+     * available and we want to show that to the user the next time she goes to the settings page.
+     */
+    try {
+        $ldapconn = $this->ldap_connect();
+        $pagedresultssupported = ldap_paged_results_supported($config->ldap_version, $ldapconn);
+    } catch (Exception $e) {
+        // If we couldn't connect and get the supported options, we can only assume we don't support paged results.
+        $pagedresultssupported = false;
+    }
+}
+/* Make sure we only disable the paged result size setting and show the notification about it if
+ * there is a configured server that we tried to contact.  Othersiwe, if someone's LDAP server does
+ * support paged results, they won't be able to turn it on the first time they set it up (because
+ * the field will be disabled).
+ */
+if (($config->host_url !== '') && (!$pagedresultssupported)) {
     $disabled = ' disabled="disabled"';
-    echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'));
+    echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
 }
 
 ?>
index 6f23523..a5285dd 100644 (file)
@@ -132,7 +132,7 @@ $string['ntlmsso_attempting'] = 'Attempting Single Sign On via NTLM...';
 $string['ntlmsso_failed'] = 'Auto-login failed, try the normal login page...';
 $string['ntlmsso_isdisabled'] = 'NTLM SSO is disabled.';
 $string['ntlmsso_unknowntype'] = 'Unknown ntlmsso type!';
-$string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support or you have configured Moodle to use LDAP protocol version 2)';
+$string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support, you have configured Moodle to use LDAP protocol version 2 or Moodle cannot contact your LDAP server to see if paged support is available.)';
 $string['pagesize'] = 'Make sure this value is smaller than your LDAP server result set size limit (the maximum number of entries that can be returned in a single query)';
 $string['pagesize_key'] = 'Page size';
 $string['pluginname'] = 'LDAP server';
index 68ea7c3..9c795b6 100644 (file)
@@ -7,6 +7,6 @@
     "require-dev": {
         "phpunit/phpunit": "5.5.*",
         "phpunit/dbUnit": "1.4.*",
-        "moodlehq/behat-extension": "3.33.0"
+        "moodlehq/behat-extension": "3.33.1"
     }
 }
index f389eb3..c757473 100644 (file)
@@ -4,27 +4,27 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "hash": "e92c5d907a1058736ad80ca004530e8a",
-    "content-hash": "5131188e8dcb035bbea39a1fbe84ad42",
+    "content-hash": "f4cfcd74744fbbced495458ea82fd314",
     "packages": [],
     "packages-dev": [
         {
             "name": "behat/behat",
-            "version": "v3.2.2",
+            "version": "v3.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/Behat.git",
-                "reference": "30aa3836825416f28581ee55fcfe6a5b0cdeeb85"
+                "reference": "15a3a1857457eaa29cdf41564a5e421effb09526"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/Behat/zipball/30aa3836825416f28581ee55fcfe6a5b0cdeeb85",
-                "reference": "30aa3836825416f28581ee55fcfe6a5b0cdeeb85",
+                "url": "https://api.github.com/repos/Behat/Behat/zipball/15a3a1857457eaa29cdf41564a5e421effb09526",
+                "reference": "15a3a1857457eaa29cdf41564a5e421effb09526",
                 "shasum": ""
             },
             "require": {
                 "behat/gherkin": "^4.4.4",
                 "behat/transliterator": "~1.0",
+                "container-interop/container-interop": "^1.1",
                 "ext-mbstring": "*",
                 "php": ">=5.3.3",
                 "symfony/class-loader": "~2.1||~3.0",
@@ -87,7 +87,7 @@
                 "symfony",
                 "testing"
             ],
-            "time": "2016-11-05 17:13:53"
+            "time": "2016-12-25T13:43:52+00:00"
         },
         {
             "name": "behat/gherkin",
                 "gherkin",
                 "parser"
             ],
-            "time": "2016-10-30 11:50:56"
+            "time": "2016-10-30T11:50:56+00:00"
         },
         {
             "name": "behat/mink",
                 "testing",
                 "web"
             ],
-            "time": "2016-03-05 08:26:18"
+            "time": "2016-03-05T08:26:18+00:00"
         },
         {
             "name": "behat/mink-browserkit-driver",
                 "browser",
                 "testing"
             ],
-            "time": "2016-03-05 08:59:47"
+            "time": "2016-03-05T08:59:47+00:00"
         },
         {
             "name": "behat/mink-extension",
                 "test",
                 "web"
             ],
-            "time": "2016-02-15 07:55:18"
+            "time": "2016-02-15T07:55:18+00:00"
         },
         {
             "name": "behat/mink-goutte-driver",
                 "headless",
                 "testing"
             ],
-            "time": "2016-03-05 09:04:22"
+            "time": "2016-03-05T09:04:22+00:00"
         },
         {
             "name": "behat/mink-selenium2-driver",
                 "testing",
                 "webdriver"
             ],
-            "time": "2016-03-05 09:10:18"
+            "time": "2016-03-05T09:10:18+00:00"
         },
         {
             "name": "behat/transliterator",
                 "slug",
                 "transliterator"
             ],
-            "time": "2015-09-28 16:26:35"
+            "time": "2015-09-28T16:26:35+00:00"
+        },
+        {
+            "name": "container-interop/container-interop",
+            "version": "1.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/container-interop/container-interop.git",
+                "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e",
+                "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Interop\\Container\\": "src/Interop/Container/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
+            "time": "2014-12-30T15:22:37+00:00"
         },
         {
             "name": "doctrine/instantiator",
                 "constructor",
                 "instantiate"
             ],
-            "time": "2015-06-14 21:17:01"
+            "time": "2015-06-14T21:17:01+00:00"
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.0",
+            "version": "v3.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "8cc89de5e71daf84051859616891d3320d88a9e8"
+                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/8cc89de5e71daf84051859616891d3320d88a9e8",
-                "reference": "8cc89de5e71daf84051859616891d3320d88a9e8",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638",
+                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638",
                 "shasum": ""
             },
             "require": {
             "keywords": [
                 "scraper"
             ],
-            "time": "2016-11-15 16:27:29"
+            "time": "2017-01-03T13:21:43+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
                 "rest",
                 "web service"
             ],
-            "time": "2016-10-08 15:01:37"
+            "time": "2016-10-08T15:01:37+00:00"
         },
         {
             "name": "guzzlehttp/promises",
-            "version": "1.3.0",
+            "version": "v1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "2693c101803ca78b27972d84081d027fca790a1e"
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/2693c101803ca78b27972d84081d027fca790a1e",
-                "reference": "2693c101803ca78b27972d84081d027fca790a1e",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+                "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.5.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "~4.0"
+                "phpunit/phpunit": "^4.0"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "1.0-dev"
+                    "dev-master": "1.4-dev"
                 }
             },
             "autoload": {
             "keywords": [
                 "promise"
             ],
-            "time": "2016-11-18 17:47:58"
+            "time": "2016-12-20T10:07:11+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
                 "stream",
                 "uri"
             ],
-            "time": "2016-06-24 23:00:38"
+            "time": "2016-06-24T23:00:38+00:00"
         },
         {
             "name": "instaclick/php-webdriver",
                 "webdriver",
                 "webtest"
             ],
-            "time": "2015-06-15 20:19:33"
+            "time": "2015-06-15T20:19:33+00:00"
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.33.0",
+            "version": "v3.33.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
-                "reference": "d363b92f62770acdd8cd878810777f3a61eada4d"
+                "reference": "a1f956fb13ef4c430ceb37c6c1ffcd355d956a22"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/d363b92f62770acdd8cd878810777f3a61eada4d",
-                "reference": "d363b92f62770acdd8cd878810777f3a61eada4d",
+                "url": "https://api.github.com/repos/moodlehq/moodle-behat-extension/zipball/a1f956fb13ef4c430ceb37c6c1ffcd355d956a22",
+                "reference": "a1f956fb13ef4c430ceb37c6c1ffcd355d956a22",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "3.2.*",
+                "behat/behat": "3.3.*",
                 "behat/mink": "~1.7",
                 "behat/mink-extension": "~2.2",
                 "behat/mink-goutte-driver": "~1.2",
                 "Behat",
                 "moodle"
             ],
-            "time": "2016-11-10 23:36:48"
+            "time": "2017-01-20T02:48:22+00:00"
         },
         {
             "name": "myclabs/deep-copy",
                 "object",
                 "object graph"
             ],
-            "time": "2016-10-31 17:19:45"
+            "time": "2016-10-31T17:19:45+00:00"
         },
         {
             "name": "phpdocumentor/reflection-common",
                 "reflection",
                 "static analysis"
             ],
-            "time": "2015-12-27 11:43:31"
+            "time": "2015-12-27T11:43:31+00:00"
         },
         {
             "name": "phpdocumentor/reflection-docblock",
                 }
             ],
             "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
-            "time": "2016-09-30 07:12:33"
+            "time": "2016-09-30T07:12:33+00:00"
         },
         {
             "name": "phpdocumentor/type-resolver",
                     "email": "me@mikevanriel.com"
                 }
             ],
-            "time": "2016-11-25 06:54:22"
+            "time": "2016-11-25T06:54:22+00:00"
         },
         {
             "name": "phpspec/prophecy",
                 "spy",
                 "stub"
             ],
-            "time": "2016-11-21 14:58:47"
+            "time": "2016-11-21T14:58:47+00:00"
         },
         {
             "name": "phpunit/dbunit",
                 "testing",
                 "xunit"
             ],
-            "time": "2015-08-07 04:57:38"
+            "time": "2015-08-07T04:57:38+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "4.0.3",
+            "version": "4.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929"
+                "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/903fd6318d0a90b4770a009ff73e4a4e9c437929",
-                "reference": "903fd6318d0a90b4770a009ff73e4a4e9c437929",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c14196e64a78570034afd0b7a9f3757ba71c2a0a",
+                "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2016-11-28 16:00:31"
+            "time": "2016-12-20T15:22:42+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
                 "filesystem",
                 "iterator"
             ],
-            "time": "2016-10-03 07:40:28"
+            "time": "2016-10-03T07:40:28+00:00"
         },
         {
             "name": "phpunit/php-text-template",
             "keywords": [
                 "template"
             ],
-            "time": "2015-06-21 13:50:34"
+            "time": "2015-06-21T13:50:34+00:00"
         },
         {
             "name": "phpunit/php-timer",
             "keywords": [
                 "timer"
             ],
-            "time": "2016-05-12 18:03:57"
+            "time": "2016-05-12T18:03:57+00:00"
         },
         {
             "name": "phpunit/php-token-stream",
             "keywords": [
                 "tokenizer"
             ],
-            "time": "2016-11-15 14:06:22"
+            "time": "2016-11-15T14:06:22+00:00"
         },
         {
             "name": "phpunit/phpunit",
                 "testing",
                 "xunit"
             ],
-            "time": "2016-10-03 13:04:15"
+            "time": "2016-10-03T13:04:15+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
                 "mock",
                 "xunit"
             ],
-            "time": "2016-12-08 20:27:08"
+            "time": "2016-12-08T20:27:08+00:00"
         },
         {
             "name": "psr/http-message",
                 "request",
                 "response"
             ],
-            "time": "2016-08-06 14:39:51"
+            "time": "2016-08-06T14:39:51+00:00"
         },
         {
             "name": "psr/log",
                 "psr",
                 "psr-3"
             ],
-            "time": "2016-10-10 12:19:37"
+            "time": "2016-10-10T12:19:37+00:00"
         },
         {
             "name": "sebastian/code-unit-reverse-lookup",
             ],
             "description": "Looks up which function or method a line of code belongs to",
             "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
-            "time": "2016-02-13 06:45:14"
+            "time": "2016-02-13T06:45:14+00:00"
         },
         {
             "name": "sebastian/comparator",
                 "compare",
                 "equality"
             ],
-            "time": "2016-11-19 09:18:40"
+            "time": "2016-11-19T09:18:40+00:00"
         },
         {
             "name": "sebastian/diff",
             "keywords": [
                 "diff"
             ],
-            "time": "2015-12-08 07:14:41"
+            "time": "2015-12-08T07:14:41+00:00"
         },
         {
             "name": "sebastian/environment",
                 "environment",
                 "hhvm"
             ],
-            "time": "2016-11-26 07:53:53"
+            "time": "2016-11-26T07:53:53+00:00"
         },
         {
             "name": "sebastian/exporter",
                 "export",
                 "exporter"
             ],
-            "time": "2016-06-17 09:04:28"
+            "time": "2016-06-17T09:04:28+00:00"
         },
         {
             "name": "sebastian/global-state",
             "keywords": [
                 "global state"
             ],
-            "time": "2015-10-12 03:26:01"
+            "time": "2015-10-12T03:26:01+00:00"
         },
         {
             "name": "sebastian/object-enumerator",
             ],
             "description": "Traverses array structures and object graphs to enumerate all referenced objects",
             "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
-            "time": "2016-01-28 13:25:10"
+            "time": "2016-01-28T13:25:10+00:00"
         },
         {
             "name": "sebastian/recursion-context",
             ],
             "description": "Provides functionality to recursively process PHP variables",
             "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
-            "time": "2015-11-11 19:50:13"
+            "time": "2015-11-11T19:50:13+00:00"
         },
         {
             "name": "sebastian/resource-operations",
             ],
             "description": "Provides a list of PHP built-in functions that operate on resources",
             "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
-            "time": "2015-07-28 20:34:47"
+            "time": "2015-07-28T20:34:47+00:00"
         },
         {
             "name": "sebastian/version",
             ],
             "description": "Library that helps with managing the version number of Git-hosted PHP projects",
             "homepage": "https://github.com/sebastianbergmann/version",
-            "time": "2016-10-03 07:35:21"
+            "time": "2016-10-03T07:35:21+00:00"
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "34348c2691ce6254e8e008026f4c5e72c22bb318"
+                "reference": "548f8230bad9f77463b20b15993a008f03e96db5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/34348c2691ce6254e8e008026f4c5e72c22bb318",
-                "reference": "34348c2691ce6254e8e008026f4c5e72c22bb318",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/548f8230bad9f77463b20b15993a008f03e96db5",
+                "reference": "548f8230bad9f77463b20b15993a008f03e96db5",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2016-10-13 13:35:11"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "87cd4e69435d98de01d0162c5f9c0ac017075c63"
+                "reference": "0152f7a47acd564ca62c652975c2b32ac6d613a6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/87cd4e69435d98de01d0162c5f9c0ac017075c63",
-                "reference": "87cd4e69435d98de01d0162c5f9c0ac017075c63",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/0152f7a47acd564ca62c652975c2b32ac6d613a6",
+                "reference": "0152f7a47acd564ca62c652975c2b32ac6d613a6",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-29 08:26:13"
+            "time": "2017-01-10T14:14:38+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "4a68f8953180bf77ea65f585020f4db0b18600b4"
+                "reference": "c5ea878b5a7f6a01b9a2f182f905831711b9ff3f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/4a68f8953180bf77ea65f585020f4db0b18600b4",
-                "reference": "4a68f8953180bf77ea65f585020f4db0b18600b4",
+                "url": "https://api.github.com/repos/symfony/config/zipball/c5ea878b5a7f6a01b9a2f182f905831711b9ff3f",
+                "reference": "c5ea878b5a7f6a01b9a2f182f905831711b9ff3f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-29 11:12:32"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "09d0fd33560e3573185a2ea17614e37ba38716c5"
+                "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/09d0fd33560e3573185a2ea17614e37ba38716c5",
-                "reference": "09d0fd33560e3573185a2ea17614e37ba38716c5",
+                "url": "https://api.github.com/repos/symfony/console/zipball/4f9e449e76996adf310498a8ca955c6deebe29dd",
+                "reference": "4f9e449e76996adf310498a8ca955c6deebe29dd",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-16 22:18:16"
+            "time": "2017-01-08T20:47:33+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "e1241f275814827c411d922ba8e64cf2a00b2994"
+                "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/e1241f275814827c411d922ba8e64cf2a00b2994",
-                "reference": "e1241f275814827c411d922ba8e64cf2a00b2994",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/f0e628f04fc055c934b3211cfabdb1c59eefbfaa",
+                "reference": "f0e628f04fc055c934b3211cfabdb1c59eefbfaa",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-03 08:11:03"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231"
+                "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/9f923e68d524a3095c5a2ae5fc7220c7cbc12231",
-                "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/810ba5c1c5352a4ddb15d4719e8936751dff0b05",
+                "reference": "810ba5c1c5352a4ddb15d4719e8936751dff0b05",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-16 22:18:16"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "f5419adad083c90e0dfd8588ef83683d7dbcc20d"
+                "reference": "22b2c97cffc6a612db82084f9e7823b095958751"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f5419adad083c90e0dfd8588ef83683d7dbcc20d",
-                "reference": "f5419adad083c90e0dfd8588ef83683d7dbcc20d",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/22b2c97cffc6a612db82084f9e7823b095958751",
+                "reference": "22b2c97cffc6a612db82084f9e7823b095958751",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-25 12:32:42"
+            "time": "2017-01-10T14:21:25+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "c6b6111f5aae7c58698cdc10220785627ac44a2c"
+                "reference": "27d9790840a4efd3b7bb8f5f4f9efc27b36b7024"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c6b6111f5aae7c58698cdc10220785627ac44a2c",
-                "reference": "c6b6111f5aae7c58698cdc10220785627ac44a2c",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/27d9790840a4efd3b7bb8f5f4f9efc27b36b7024",
+                "reference": "27d9790840a4efd3b7bb8f5f4f9efc27b36b7024",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-25 12:32:42"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283"
+                "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e8f47a327c2f0fd5aa04fa60af2b693006ed7283",
-                "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9137eb3a3328e413212826d63eeeb0217836e2b6",
+                "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2016-10-13 06:29:04"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4"
+                "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4",
-                "reference": "8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/a0c6ef2dc78d33b58d91d3a49f49797a184d06f4",
+                "reference": "a0c6ef2dc78d33b58d91d3a49f49797a184d06f4",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-24 00:46:43"
+            "time": "2017-01-08T20:47:33+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
                 "portable",
                 "shim"
             ],
-            "time": "2016-11-14 01:06:16"
+            "time": "2016-11-14T01:06:16+00:00"
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.14",
+            "version": "v2.8.16",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f"
+                "reference": "ebb3c2abe0940a703f08e0cbe373f62d97d40231"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f",
-                "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f",
+                "url": "https://api.github.com/repos/symfony/process/zipball/ebb3c2abe0940a703f08e0cbe373f62d97d40231",
+                "reference": "ebb3c2abe0940a703f08e0cbe373f62d97d40231",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2016-09-29 14:03:54"
+            "time": "2017-01-02T20:30:24+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "64ab6fc0b42e5386631f408e6adcaaff8eee5ba1"
+                "reference": "6520f3d4cce604d9dd1e86cac7af954984dd9bda"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/64ab6fc0b42e5386631f408e6adcaaff8eee5ba1",
-                "reference": "64ab6fc0b42e5386631f408e6adcaaff8eee5ba1",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/6520f3d4cce604d9dd1e86cac7af954984dd9bda",
+                "reference": "6520f3d4cce604d9dd1e86cac7af954984dd9bda",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-18 21:17:59"
+            "time": "2017-01-02T20:32:22+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.2.0",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "f2300ba8fbb002c028710b92e1906e7457410693"
+                "reference": "50eadbd7926e31842893c957eca362b21592a97d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/f2300ba8fbb002c028710b92e1906e7457410693",
-                "reference": "f2300ba8fbb002c028710b92e1906e7457410693",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/50eadbd7926e31842893c957eca362b21592a97d",
+                "reference": "50eadbd7926e31842893c957eca362b21592a97d",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2016-11-18 21:17:59"
+            "time": "2017-01-03T13:51:32+00:00"
         },
         {
             "name": "webmozart/assert",
                 "check",
                 "validate"
             ],
-            "time": "2016-11-23 20:04:58"
+            "time": "2016-11-23T20:04:58+00:00"
         }
     ],
     "aliases": [],
index 452a899..8bb17af 100644 (file)
@@ -1179,6 +1179,10 @@ function is_enrolled(context $context, $user = null, $withcapability = '', $only
  * Returns an array of joins, wheres and params that will limit the group of
  * users to only those enrolled and with given capability (if specified).
  *
+ * Note this join will return duplicate rows for users who have been enrolled
+ * several times (e.g. as manual enrolment, and as self enrolment). You may
+ * need to use a SELECT DISTINCT in your query (see get_enrolled_sql for example).
+ *
  * @param context $context
  * @param string $prefix optional, a prefix to the user id column
  * @param string|array $capability optional, may include a capability name, or array of names.
index ec18f6f..8a5c965 100644 (file)
@@ -22,6 +22,11 @@ if (!defined('ROOTDSE')) {
     define ('ROOTDSE', '');
 }
 
+// Paged results control OID value.
+if (!defined('LDAP_PAGED_RESULTS_CONTROL')) {
+    define ('LDAP_PAGED_RESULTS_CONTROL', '1.2.840.113556.1.4.319');
+}
+
 // Default page size when using LDAP paged results
 if (!defined('LDAP_DEFAULT_PAGESIZE')) {
     define('LDAP_DEFAULT_PAGESIZE', 250);
@@ -452,14 +457,39 @@ function ldap_stripslashes($text) {
 
 
 /**
- * Check if we use LDAP version 3, otherwise the server cannot use them.
+ * Check if we can use paged results (see RFC 2696). We need to use
+ * LDAP version 3 (or later), otherwise the server cannot use them. If
+ * we also pass in a valid LDAP connection handle, we also check
+ * whether the server actually supports them.
  *
  * @param ldapversion integer The LDAP protocol version we use.
+ * @param ldapconnection resource An existing LDAP connection (optional).
  *
  * @return boolean true is paged results can be used, false otherwise.
  */
-function ldap_paged_results_supported($ldapversion) {
-    if ((int)$ldapversion === 3) {
+function ldap_paged_results_supported($ldapversion, $ldapconnection = null) {
+    if ((int)$ldapversion < 3) {
+        // Minimun required version: LDAP v3.
+        return false;
+    }
+
+    if ($ldapconnection === null) {
+        // Can't verify it, so assume it isn't supported.
+        return false;
+    }
+
+    // Connect to the rootDSE and get the supported controls.
+    $sr = ldap_read($ldapconnection, ROOTDSE, '(objectClass=*)', array('supportedControl'));
+    if (!$sr) {
+        return false;
+    }
+
+    $entries = ldap_get_entries_moodle($ldapconnection, $sr);
+    if (empty($entries)) {
+        return false;
+    }
+    $info = array_change_key_case($entries[0], CASE_LOWER);
+    if (isset($info['supportedcontrol']) && in_array(LDAP_PAGED_RESULTS_CONTROL, $info['supportedcontrol'])) {
         return true;
     }
 
index f293ce1..5e37773 100644 (file)
@@ -288,7 +288,7 @@ class assign_feedback_offline extends assign_feedback_plugin {
         } else if ($confirm) {
             $importid = optional_param('importid', 0, PARAM_INT);
             $draftid = optional_param('draftid', 0, PARAM_INT);
-            $encoding = optional_param('encoding', 'utf-8', PARAM_ALPHAEXT);
+            $encoding = optional_param('encoding', 'utf-8', PARAM_ALPHANUMEXT);
             $separator = optional_param('separator', 'comma', PARAM_ALPHA);
             $ignoremodified = optional_param('ignoremodified', 0, PARAM_BOOL);
             $gradeimporter = new assignfeedback_offline_grade_importer($importid, $this->assignment, $encoding, $separator);
index c12e712..ef67d74 100644 (file)
@@ -4439,7 +4439,7 @@ function forum_update_post($newpost, $mform, $unused = null) {
 
     // Last post modified tracking.
     $discussion->timemodified = $post->modified;
-    $discussion->usermodified = $USER->id;
+    $discussion->usermodified = $post->userid;
 
     if (!$post->parent) {   // Post is a discussion starter - update discussion title and times too
         $discussion->name      = $post->subject;
index ef135e5..82c5ffb 100644 (file)
@@ -3260,4 +3260,58 @@ class mod_forum_lib_testcase extends advanced_testcase {
             ],
         ];
     }
+
+    /**
+     * Test that {@link forum_update_post()} keeps correct forum_discussions usermodified.
+     */
+    public function test_forum_update_post_keeps_discussions_usermodified() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        // Let there be light.
+        $teacher = self::getDataGenerator()->create_user();
+        $student = self::getDataGenerator()->create_user();
+        $course = self::getDataGenerator()->create_course();
+
+        $forum = self::getDataGenerator()->create_module('forum', (object)[
+            'course' => $course->id,
+        ]);
+
+        $generator = self::getDataGenerator()->get_plugin_generator('mod_forum');
+
+        // Let the teacher start a discussion.
+        $discussion = $generator->create_discussion((object)[
+            'course' => $course->id,
+            'userid' => $teacher->id,
+            'forum' => $forum->id,
+        ]);
+
+        // On this freshly created discussion, the teacher is the author of the last post.
+        $this->assertEquals($teacher->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
+
+        // Let the student reply to the teacher's post.
+        $reply = $generator->create_post((object)[
+            'course' => $course->id,
+            'userid' => $student->id,
+            'forum' => $forum->id,
+            'discussion' => $discussion->id,
+            'parent' => $discussion->firstpost,
+        ]);
+
+        // The student should now be the last post's author.
+        $this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
+
+        // Let the teacher edit the student's reply.
+        $this->setUser($teacher->id);
+        $newpost = (object)[
+            'id' => $reply->id,
+            'itemid' => 0,
+            'subject' => 'Amended subject',
+        ];
+        forum_update_post($newpost, null);
+
+        // The student should be still the last post's author.
+        $this->assertEquals($student->id, $DB->get_field('forum_discussions', 'usermodified', ['id' => $discussion->id]));
+    }
 }
index 41a3233..c8ea357 100644 (file)
@@ -1412,7 +1412,7 @@ class mod_glossary_external extends external_api {
         return new external_function_parameters(array(
             'glossaryid' => new external_value(PARAM_INT, 'Glossary id'),
             'concept' => new external_value(PARAM_TEXT, 'Glossary concept'),
-            'definition' => new external_value(PARAM_TEXT, 'Glossary concept definition'),
+            'definition' => new external_value(PARAM_RAW, 'Glossary concept definition'),
             'definitionformat' => new external_format_value('definition'),
             'options' => new external_multiple_structure (
                 new external_single_structure(
index 34b2f82..60cf1e5 100644 (file)
@@ -1121,7 +1121,7 @@ class mod_glossary_external_testcase extends externallib_advanced_testcase {
 
         $this->setAdminUser();
         $concept = 'A concept';
-        $definition = 'A definition';
+        $definition = '<p>A definition</p>';
         $return = mod_glossary_external::add_entry($glossary->id, $concept, $definition, FORMAT_HTML);
         $return = external_api::clean_returnvalue(mod_glossary_external::add_entry_returns(), $return);
 
index 87e31bc..539df5a 100644 (file)
@@ -389,7 +389,8 @@ abstract class quiz_attempts_report_table extends table_sql {
     public function base_sql(\core\dml\sql_join $allowedstudentsjoins) {
         global $DB;
 
-        $fields = $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
+        // Please note this uniqueid column is not the same as quiza.uniqueid.
+        $fields = 'DISTINCT ' . $DB->sql_concat('u.id', "'#'", 'COALESCE(quiza.attempt, 0)') . ' AS uniqueid,';
 
         if ($this->qmsubselect) {
             $fields .= "\n(CASE WHEN $this->qmsubselect THEN 1 ELSE 0 END) AS gradedattempt,";
index 46cdafc..79c1928 100644 (file)
@@ -90,6 +90,14 @@ class quiz_overview_report_testcase extends advanced_testcase {
             $DB->insert_record('quiz_attempts', $data);
         }
 
+        // This line is not really necessary for the test asserts below,
+        // but what it does is add an extra user row returned by
+        // get_enrolled_with_capabilities_join because of a second enrolment.
+        // The extra row returned used to make $table->query_db complain
+        // about duplicate records. So this is really a test that an extra
+        // student enrolment does not cause duplicate records in this query.
+        $generator->enrol_user($student2->id, $course->id, null, 'self');
+
         // Actually getting the SQL to run is quite hard. Do a minimal set up of
         // some objects.
         $context = context_module::instance($quiz->cmid);
index de55514..0c06abc 100644 (file)
@@ -104,4 +104,111 @@ class mod_resource_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_resources_by_courses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_resources_by_courses_parameters() {
+        return new external_function_parameters (
+            array(
+                'courseids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, array()
+                ),
+            )
+        );
+    }
+
+    /**
+     * Returns a list of files in a provided list of courses.
+     * If no list is provided all files that the user can view will be returned.
+     *
+     * @param array $courseids course ids
+     * @return array of warnings and files
+     * @since Moodle 3.3
+     */
+    public static function get_resources_by_courses($courseids = array()) {
+
+        $warnings = array();
+        $returnedresources = array();
+
+        $params = array(
+            'courseids' => $courseids,
+        );
+        $params = self::validate_parameters(self::get_resources_by_courses_parameters(), $params);
+
+        $mycourses = array();
+        if (empty($params['courseids'])) {
+            $mycourses = enrol_get_my_courses();
+            $params['courseids'] = array_keys($mycourses);
+        }
+
+        // Ensure there are courseids to loop through.
+        if (!empty($params['courseids'])) {
+
+            list($courses, $warnings) = external_util::validate_courses($params['courseids'], $mycourses);
+
+            // Get the resources in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $resources = get_all_instances_in_courses("resource", $courses);
+            foreach ($resources as $resource) {
+                $context = context_module::instance($resource->coursemodule);
+                // Entry to return.
+                $resource->name = external_format_string($resource->name, $context->id);
+
+                list($resource->intro, $resource->introformat) = external_format_text($resource->intro,
+                                                                $resource->introformat, $context->id, 'mod_resource', 'intro', null);
+                $resource->introfiles = external_util::get_area_files($context->id, 'mod_resource', 'intro', false, false);
+                $resource->contentfiles = external_util::get_area_files($context->id, 'mod_resource', 'content');
+
+                $returnedresources[] = $resource;
+            }
+        }
+
+        $result = array(
+            'resources' => $returnedresources,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_resources_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_resources_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'resources' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'id' => new external_value(PARAM_INT, 'Module id'),
+                            'course' => new external_value(PARAM_INT, 'Course id'),
+                            'name' => new external_value(PARAM_RAW, 'Page name'),
+                            'intro' => new external_value(PARAM_RAW, 'Summary'),
+                            'introformat' => new external_format_value('intro', 'Summary format'),
+                            'introfiles' => new external_files('Files in the introduction text'),
+                            'contentfiles' => new external_files('Files in the content'),
+                            'tobemigrated' => new external_value(PARAM_INT, 'Whether this resource was migrated'),
+                            'legacyfiles' => new external_value(PARAM_INT, 'Legacy files flag'),
+                            'legacyfileslast' => new external_value(PARAM_INT, 'Legacy files last control flag'),
+                            'display' => new external_value(PARAM_INT, 'How to display the resource'),
+                            'displayoptions' => new external_value(PARAM_RAW, 'Display options (width, height)'),
+                            'filterfiles' => new external_value(PARAM_INT, 'If filters should be applied to the resource content'),
+                            'revision' => new external_value(PARAM_INT, 'Incremented when after each file changes, to avoid cache'),
+                            'timemodified' => new external_value(PARAM_INT, 'Last time the resource was modified'),
+                            'section' => new external_value(PARAM_INT, 'Course section id'),
+                            'visible' => new external_value(PARAM_INT, 'Module visibility'),
+                            'groupmode' => new external_value(PARAM_INT, 'Group mode'),
+                            'groupingid' => new external_value(PARAM_INT, 'Grouping id'),
+                        )
+                    )
+                ),
+                'warnings' => new external_warnings(),
+            )
+        );
+    }
 }
index d5f5658..c85e002 100644 (file)
@@ -36,5 +36,13 @@ $functions = array(
         'capabilities'  => 'mod/resource:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
-
+    'mod_resource_get_resources_by_courses' => array(
+        'classname'     => 'mod_resource_external',
+        'methodname'    => 'get_resources_by_courses',
+        'description'   => 'Returns a list of files in a provided list of courses, if no list is provided all files that
+                            the user can view will be returned.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/resource:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
index 8ce41c4..c8a2a65 100644 (file)
@@ -111,4 +111,146 @@ class mod_resource_external_testcase extends externallib_advanced_testcase {
         }
 
     }
+
+    /**
+     * Test test_mod_resource_get_resources_by_courses
+     */
+    public function test_mod_resource_get_resources_by_courses() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course1 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course();
+
+        $student = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id);
+
+        self::setUser($student);
+
+        // First resource.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $resource1 = self::getDataGenerator()->create_module('resource', $record);
+
+        // Second resource.
+        $record = new stdClass();
+        $record->course = $course2->id;
+        $resource2 = self::getDataGenerator()->create_module('resource', $record);
+
+        // Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
+        $enrol = enrol_get_plugin('manual');
+        $enrolinstances = enrol_get_instances($course2->id, true);
+        foreach ($enrolinstances as $courseenrolinstance) {
+            if ($courseenrolinstance->enrol == "manual") {
+                $instance2 = $courseenrolinstance;
+                break;
+            }
+        }
+        $enrol->enrol_user($instance2, $student->id, $studentrole->id);
+
+        $returndescription = mod_resource_external::get_resources_by_courses_returns();
+
+        // Create what we expect to be returned when querying the two courses.
+        $expectedfields = array('id', 'course', 'name', 'intro', 'introformat', 'introfiles',
+                                'contentfiles', 'tobemigrated', 'legacyfiles', 'legacyfileslast', 'display', 'displayoptions',
+                                'filterfiles', 'revision', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid');
+
+        // Add expected coursemodule and data.
+        $resource1->coursemodule = $resource1->cmid;
+        $resource1->introformat = 1;
+        $resource1->contentformat = 1;
+        $resource1->section = 0;
+        $resource1->visible = true;
+        $resource1->groupmode = 0;
+        $resource1->groupingid = 0;
+        $resource1->introfiles = [];
+        $resource1->contentfiles = [];
+
+        $resource2->coursemodule = $resource2->cmid;
+        $resource2->introformat = 1;
+        $resource2->contentformat = 1;
+        $resource2->section = 0;
+        $resource2->visible = true;
+        $resource2->groupmode = 0;
+        $resource2->groupingid = 0;
+        $resource2->introfiles = [];
+        $resource2->contentfiles = [];
+
+        foreach ($expectedfields as $field) {
+            $expected1[$field] = $resource1->{$field};
+            $expected2[$field] = $resource2->{$field};
+        }
+
+        $expectedresources = array($expected2, $expected1);
+
+        // Call the external function passing course ids.
+        $result = mod_resource_external::get_resources_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        // Remove the contentfiles (to be checked bellow).
+        $result['resources'][0]['contentfiles'] = [];
+        $result['resources'][1]['contentfiles'] = [];
+
+        // Now, check that we retrieve the same data we created.
+        $this->assertEquals($expectedresources, $result['resources']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Call the external function without passing course id.
+        $result = mod_resource_external::get_resources_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        // Remove the contentfiles (to be checked bellow).
+        $result['resources'][0]['contentfiles'] = [];
+        $result['resources'][1]['contentfiles'] = [];
+
+        // Check that without course ids we still get the correct data.
+        $this->assertEquals($expectedresources, $result['resources']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Add a file to the intro.
+        $fileintroname = "fileintro.txt";
+        $filerecordinline = array(
+            'contextid' => context_module::instance($resource2->cmid)->id,
+            'component' => 'mod_resource',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => $fileintroname,
+        );
+        $fs = get_file_storage();
+        $timepost = time();
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        $result = mod_resource_external::get_resources_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        // Check that we receive correctly the files.
+        $this->assertCount(1, $result['resources'][0]['introfiles']);
+        $this->assertEquals($fileintroname, $result['resources'][0]['introfiles'][0]['filename']);
+        $this->assertCount(1, $result['resources'][0]['contentfiles']);
+        $this->assertCount(1, $result['resources'][1]['contentfiles']);
+        // Test autogenerated resource.
+        $this->assertEquals('resource2.txt', $result['resources'][0]['contentfiles'][0]['filename']);
+        $this->assertEquals('resource1.txt', $result['resources'][1]['contentfiles'][0]['filename']);
+
+        // Unenrol user from second course.
+        $enrol->unenrol_user($instance2, $student->id);
+        array_shift($expectedresources);
+
+        // Call the external function without passing course id.
+        $result = mod_resource_external::get_resources_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        // Remove the contentfiles (to be checked bellow).
+        $result['resources'][0]['contentfiles'] = [];
+        $this->assertEquals($expectedresources, $result['resources']);
+
+        // Call for the second course we unenrolled the user from, expected warning.
+        $result = mod_resource_external::get_resources_by_courses(array($course2->id));
+        $this->assertCount(1, $result['warnings']);
+        $this->assertEquals('1', $result['warnings'][0]['warningcode']);
+        $this->assertEquals($course2->id, $result['warnings'][0]['itemid']);
+    }
 }
index 1c569c2..cb8c6bd 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;    // Requires this Moodle version
 $plugin->component = 'mod_resource'; // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index c713e72..abb3c28 100644 (file)
@@ -888,7 +888,7 @@ function scorm_get_all_attempts($scormid, $userid) {
  * @param  stdClass $cm     course module object
  */
 function scorm_print_launch ($user, $scorm, $action, $cm) {
-    global $CFG, $DB, $PAGE, $OUTPUT, $COURSE;
+    global $CFG, $DB, $OUTPUT;
 
     if ($scorm->updatefreq == SCORM_UPDATE_EVERYTIME) {
         scorm_parse($scorm, false);
@@ -931,6 +931,13 @@ function scorm_print_launch ($user, $scorm, $action, $cm) {
 
     $result = scorm_get_toc($user, $scorm, $cm->id, TOCFULLURL, $orgidentifier);
     $incomplete = $result->incomplete;
+    // Get latest incomplete sco to launch first.
+    if (!empty($result->sco->id)) {
+        $launchsco = $result->sco->id;
+    } else {
+        // Use launch defined by SCORM package.
+        $launchsco = $scorm->launch;
+    }
 
     // Do we want the TOC to be displayed?
     if ($scorm->displaycoursestructure == 1) {
@@ -976,7 +983,7 @@ function scorm_print_launch ($user, $scorm, $action, $cm) {
         }
 
         echo html_writer::empty_tag('br');
-        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $scorm->launch));
+        echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'scoid', 'value' => $launchsco));
         echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'cm', 'value' => $cm->id));
         echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'currentorg', 'value' => $orgidentifier));
         echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('enter', 'scorm'),
@@ -1013,12 +1020,19 @@ function scorm_simple_play($scorm, $user, $context, $cmid) {
         }
         if ($scorm->skipview >= SCORM_SKIPVIEW_FIRST) {
             $sco = current($scoes);
-            $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id,
-                                                                'currentorg' => $orgidentifier,
-                                                                'scoid' => $sco->id));
+            $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
+            $url = new moodle_url('/mod/scorm/player.php', array('a' => $scorm->id, 'currentorg' => $orgidentifier));
+
+            // Set last incomplete sco to launch first.
+            if (!empty($result->sco->id)) {
+                $url->param('scoid', $result->sco->id);
+            } else {
+                $url->param('scoid', $sco->id);
+            }
+
             if ($scorm->skipview == SCORM_SKIPVIEW_ALWAYS || !scorm_has_tracks($scorm->id, $user->id)) {
                 if (!empty($scorm->forcenewattempt)) {
-                    $result = scorm_get_toc($user, $scorm, $cmid, TOCFULLURL, $orgidentifier);
+
                     if ($result->incomplete === false) {
                         $url->param('newattempt', 'on');
                     }
@@ -1522,6 +1536,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
 
             if ($sco->isvisible === 'true') {
                 if (!empty($sco->launch)) {
+                    // Set first sco to launch if in browse/review mode.
                     if (empty($scoid) && ($mode != 'normal')) {
                         $scoid = $sco->id;
                     }
@@ -1542,7 +1557,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
                                 ($usertrack->status == 'incomplete') ||
                                 ($usertrack->status == 'browsed')) {
                             $incomplete = true;
-                            if ($play && empty($scoid)) {
+                            if (empty($scoid)) {
                                 $scoid = $sco->id;
                             }
                         }
@@ -1561,7 +1576,7 @@ function scorm_get_toc_object($user, $scorm, $currentorg='', $scoid='', $mode='n
                         }
 
                     } else {
-                        if ($play && empty($scoid)) {
+                        if (empty($scoid)) {
                             $scoid = $sco->id;
                         }
 
index 8ec5c8c..da3aab3 100644 (file)
@@ -54,7 +54,7 @@ Feature: Scorm multi-sco completion
     Then "Student 1" user has completed "Basic Multi-sco SCORM package" activity
 
   @javascript
-  Scenario: Test completion with all scos
+  Scenario: Test completion with all scos and correct sco load on re-entry.
     When I log in as "teacher1"
     And I follow "Course 1"
     And I turn editing mode on
@@ -96,11 +96,6 @@ Feature: Scorm multi-sco completion
     And I should see "Normal"
     And I press "Enter"
     And I switch to "scorm_object" iframe
-    And I should see "Play of the game"
-
-    And I switch to the main frame
-    And I click on "Par?" "list_item"
-    And I switch to "scorm_object" iframe
     And I should see "Par"
 
     And I switch to the main frame
index 21d63b8..676f87b 100644 (file)
@@ -70,13 +70,20 @@ if (!empty($scorm->popup)) {
 
     $scoid = 0;
     $orgidentifier = '';
-    if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
-        if (($sco->organization == '') && ($sco->launch == '')) {
-            $orgidentifier = $sco->identifier;
-        } else {
-            $orgidentifier = $sco->organization;
+
+    $result = scorm_get_toc($USER, $scorm, $cm->id, TOCFULLURL);
+    // Set last incomplete sco to launch first.
+    if (!empty($result->sco->id)) {
+        $scoid = $result->sco->id;
+    } else {
+        if ($sco = scorm_get_sco($scorm->launch, SCO_ONLY)) {
+            if (($sco->organization == '') && ($sco->launch == '')) {
+                $orgidentifier = $sco->identifier;
+            } else {
+                $orgidentifier = $sco->organization;
+            }
+            $scoid = $sco->id;
         }
-        $scoid = $sco->id;
     }
 
     if (empty($preventskip) && $scorm->skipview >= SCORM_SKIPVIEW_FIRST &&
index 432c627..8787a34 100644 (file)
@@ -78,15 +78,17 @@ class core_renderer extends \core_renderer {
         $html .= html_writer::start_div('card');
         $html .= html_writer::start_div('card-block');
         $html .= html_writer::div($this->context_header_settings_menu(), 'pull-xs-right context-header-settings-menu');
+        $html .= html_writer::start_div('pull-xs-left');
         $html .= $this->context_header();
+        $html .= html_writer::end_div();
         $pageheadingbutton = $this->page_heading_button();
         if (empty($PAGE->layout_options['nonavbar'])) {
-            $html .= html_writer::start_div('clearfix', array('id' => 'page-navbar'));
+            $html .= html_writer::start_div('clearfix w-100 pull-xs-left', array('id' => 'page-navbar'));
             $html .= html_writer::tag('div', $this->navbar(), array('class' => 'breadcrumb-nav'));
-            $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button');
+            $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button pull-xs-right');
             $html .= html_writer::end_div();
         } else if ($pageheadingbutton) {
-            $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button nonavbar');
+            $html .= html_writer::div($pageheadingbutton, 'breadcrumb-button nonavbar pull-xs-right');
         }
         $html .= html_writer::tag('div', $this->course_header(), array('id' => 'course-header'));
         $html .= html_writer::end_div();
index c669075..cfa9146 100644 (file)
@@ -9,9 +9,11 @@
 .context-header-settings-menu,
 .region-main-settings-menu {
     float: right;
-    width: 4em;
+    width: auto;
+    max-width: 4em;
     height: 2em;
     display: block;
+    margin-top: 4px;
 }
 
 .context-header-settings-menu .dropdown-toggle > .icon,
@@ -1782,12 +1784,12 @@ header {
 
     .page-header-image,
     .page-header-headings {
+        float: left;
         display: block;
         position: relative;
     }
 
     .page-header-image {
-        float: left;
         margin-right: 1em;
         margin-bottom: 1em;
     }
index 9e27d2b..a7ea755 100644 (file)
@@ -192,14 +192,7 @@ select {
 }
 
 .breadcrumb-button {
-    float: right;
     margin-top: 4px;
-
-    @include media-breakpoint-up('md') {
-        &.nonavbar {
-            margin-top: -3rem;
-        }
-    }
 }
 
 .breadcrumb-button .singlebutton {
index 7cc2fd7..168ea2d 100644 (file)
@@ -830,17 +830,23 @@ if ($bulkoperations) {
         $showalllink = false;
     }
 
+    echo html_writer::start_tag('div', array('class' => 'btn-group'));
     if ($perpage < $matchcount) {
         // Select all users, refresh page showing all users and mark them all selected.
         $label = get_string('selectalluserswithcount', 'moodle', $matchcount);
-        echo '<input type="button" id="checkall" value="' . $label . '" data-showallink="' . $showalllink . '" /> ';
+        echo html_writer::tag('input', "", array('type' => 'button', 'id' => 'checkall', 'class' => 'btn btn-secondary',
+                'value' => $label, 'data-showallink' => $showalllink));
         // Select all users, mark all users on page as selected.
-        echo '<input type="button" id="checkallonpage" value="' . get_string('selectallusersonpage') . '" /> ';
+        echo html_writer::tag('input', "", array('type' => 'button', 'id' => 'checkallonpage', 'class' => 'btn btn-secondary',
+        'value' => get_string('selectallusersonpage')));
     } else {
-        echo '<input type="button" id="checkallonpage" value="' . get_string('selectall') . '" /> ';
+        echo html_writer::tag('input', "", array('type' => 'button', 'id' => 'checkallonpage', 'class' => 'btn btn-secondary',
+        'value' => get_string('selectall')));
     }
 
-    echo '<input type="button" id="checknone" value="'.get_string('deselectall').'" /> ';
+    echo html_writer::tag('input', "", array('type' => 'button', 'id' => 'checknone', 'class' => 'btn btn-secondary',
+        'value' => get_string('deselectall')));
+    echo html_writer::end_tag('div');
     $displaylist = array();
     $displaylist['messageselect.php'] = get_string('messageselectadd');
     if (!empty($CFG->enablenotes) && has_capability('moodle/notes:manage', $context) && $context->id != $frontpagectx->id) {