Merge branch 'MDL-49423-master' of git://github.com/jleyva/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 24 Jan 2017 18:05:51 +0000 (19:05 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 24 Jan 2017 18:05:51 +0000 (19:05 +0100)
43 files changed:
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/assign/lang/en/assign.php
mod/assign/renderer.php
mod/assign/tests/behat/submit_without_group.feature
mod/folder/classes/external.php
mod/folder/db/services.php
mod/folder/tests/externallib_test.php
mod/folder/version.php
mod/forum/classes/output/big_search_form.php
mod/forum/forum.js [deleted file]
mod/forum/lib.php
mod/forum/templates/big_search_form.mustache
mod/forum/tests/lib_test.php
mod/forum/upgrade.txt
mod/glossary/classes/external.php
mod/glossary/tests/external_test.php
mod/page/classes/external.php
mod/page/db/services.php
mod/page/tests/externallib_test.php
mod/page/version.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/config.php
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/modules.scss
theme/boost/templates/mod_forum/big_search_form.mustache
user/index.php

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 dabe025..893b4d8 100644 (file)
@@ -314,6 +314,7 @@ $string['modulename_link'] = 'mod/assignment/view';
 $string['modulenameplural'] = 'Assignments';
 $string['moreusers'] = '{$a} more...';
 $string['multipleteams'] = 'Member of more than one group';
+$string['multipleteams_desc'] = 'The assignment requires submission in groups. You are a member of more than one group. To be able to submit you must be member of exactly one group so that your submission can be mapped correctly to your group. Please contact your teacher to update your group membership.';
 $string['multipleteamsgrader'] = 'Member of more than one group, so unable to make submissions.';
 $string['mysubmission'] = 'My submission: ';
 $string['newsubmissions'] = 'Assignments submitted';
@@ -332,6 +333,7 @@ $string['nosavebutnext'] = 'Next';
 $string['nosubmission'] = 'Nothing has been submitted for this assignment';
 $string['nosubmissionsacceptedafter'] = 'No submissions accepted after ';
 $string['noteam'] = 'Not a member of any group';
+$string['noteam_desc'] = 'This assignment requires submission in groups. You are not a member of any group, so you cannot create a submission. Please contact your teacher to be added to a group.';
 $string['noteamgrader'] = 'Not a member of any group, so unable to make submissions.';
 $string['notgraded'] = 'Not graded';
 $string['notgradedyet'] = 'Not graded yet';
index 665a0d3..19ec082 100644 (file)
@@ -635,6 +635,7 @@ class mod_assign_renderer extends plugin_renderer_base {
 
         $t = new html_table();
 
+        $warningmsg = '';
         if ($status->teamsubmissionenabled) {
             $row = new html_table_row();
             $cell1 = new html_table_cell(get_string('submissionteam', 'assign'));
@@ -643,13 +644,19 @@ class mod_assign_renderer extends plugin_renderer_base {
                 $cell2 = new html_table_cell(format_string($group->name, false, $status->context));
             } else if ($status->preventsubmissionnotingroup) {
                 if (count($status->usergroups) == 0) {
+                    $notification = new \core\output\notification(get_string('noteam', 'assign'), 'error');
+                    $notification->set_show_closebutton(false);
                     $cell2 = new html_table_cell(
-                        html_writer::span(get_string('noteam', 'assign'), 'alert alert-error')
+                        $this->output->render($notification)
                     );
+                    $warningmsg = $this->output->notification(get_string('noteam_desc', 'assign'), 'error');
                 } else if (count($status->usergroups) > 1) {
+                    $notification = new \core\output\notification(get_string('multipleteams', 'assign'), 'error');
+                    $notification->set_show_closebutton(false);
                     $cell2 = new html_table_cell(
-                        html_writer::span(get_string('multipleteams', 'assign'), 'alert alert-error')
+                        $this->output->render($notification)
                     );
+                    $warningmsg = $this->output->notification(get_string('multipleteams_desc', 'assign'), 'error');
                 }
             } else {
                 $cell2 = new html_table_cell(get_string('defaultteam', 'assign'));
@@ -906,6 +913,7 @@ class mod_assign_renderer extends plugin_renderer_base {
             }
         }
 
+        $o .= $warningmsg;
         $o .= html_writer::table($t);
         $o .= $this->output->box_end();
 
index 6c24033..bffaf65 100644 (file)
@@ -48,6 +48,7 @@ Feature: Submit assignment without group
     And I follow "Course 1"
     And I follow "Allow default group"
     Then I should not see "Not a member of any group"
+    And I should not see "This assignment requires submission in groups. You are not a member of any group"
     And I should see "Nothing has been submitted for this assignment"
     And I press "Add submission"
     And I set the following fields to these values:
@@ -59,6 +60,7 @@ Feature: Submit assignment without group
     And I follow "Course 1"
     And I follow "Require group membership"
     And I should see "Not a member of any group"
+    And I should see "This assignment requires submission in groups. You are not a member of any group"
     And I should see "Nothing has been submitted for this assignment"
     And I should not see "Add submission"
     And I am on homepage
@@ -121,6 +123,7 @@ Feature: Submit assignment without group
     And I follow "Course 3"
     And I follow "Require group membership"
     And I should see "Member of more than one group"
+    And I should see "The assignment requires submission in groups. You are a member of more than one group."
     And I should see "Nothing has been submitted for this assignment"
     And I should not see "Add submission"
     And I log out
index c547800..0e4d33f 100644 (file)
@@ -104,4 +104,106 @@ class mod_folder_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_folders_by_courses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_folders_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 folders in a provided list of courses.
+     * If no list is provided all folders that the user can view will be returned.
+     *
+     * @param array $courseids course ids
+     * @return array of warnings and folders
+     * @since Moodle 3.3
+     */
+    public static function get_folders_by_courses($courseids = array()) {
+
+        $warnings = array();
+        $returnedfolders = array();
+
+        $params = array(
+            'courseids' => $courseids,
+        );
+        $params = self::validate_parameters(self::get_folders_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 folders in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $folders = get_all_instances_in_courses("folder", $courses);
+            foreach ($folders as $folder) {
+                $context = context_module::instance($folder->coursemodule);
+                // Entry to return.
+                $folder->name = external_format_string($folder->name, $context->id);
+
+                list($folder->intro, $folder->introformat) = external_format_text($folder->intro,
+                                                                $folder->introformat, $context->id, 'mod_folder', 'intro', null);
+                $folder->introfiles = external_util::get_area_files($context->id, 'mod_folder', 'intro', false, false);
+
+                $returnedfolders[] = $folder;
+            }
+        }
+
+        $result = array(
+            'folders' => $returnedfolders,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_folders_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_folders_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'folders' => 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'),
+                            'revision' => new external_value(PARAM_INT, 'Incremented when after each file changes, to avoid cache'),
+                            'timemodified' => new external_value(PARAM_INT, 'Last time the folder was modified'),
+                            'display' => new external_value(PARAM_INT, 'Display type of folder contents on a separate page or inline'),
+                            'showexpanded' => new external_value(PARAM_INT, '1 = expanded, 0 = collapsed for sub-folders'),
+                            'showdownloadfolder' => new external_value(PARAM_INT, 'Whether to show the download folder button'),
+                            '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 dc99134..cd84167 100644 (file)
@@ -36,5 +36,13 @@ $functions = array(
         'capabilities'  => 'mod/folder:view',
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
-
+    'mod_folder_get_folders_by_courses' => array(
+        'classname'     => 'mod_folder_external',
+        'methodname'    => 'get_folders_by_courses',
+        'description'   => 'Returns a list of folders in a provided list of courses, if no list is provided all folders that
+                            the user can view will be returned. Please note that this WS is not returning the folder contents.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/folder:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
index 17f0ba9..ad44efb 100644 (file)
@@ -109,6 +109,124 @@ class mod_folder_external_testcase extends externallib_advanced_testcase {
         } catch (moodle_exception $e) {
             $this->assertEquals('requireloginerror', $e->errorcode);
         }
+    }
+
+    /**
+     * Test test_mod_folder_get_folders_by_courses
+     */
+    public function test_mod_folder_get_folders_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 folder.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $folder1 = self::getDataGenerator()->create_module('folder', $record);
+
+        // Second folder.
+        $record = new stdClass();
+        $record->course = $course2->id;
+        $folder2 = self::getDataGenerator()->create_module('folder', $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_folder_external::get_folders_by_courses_returns();
+
+        // Create what we expect to be returned when querying the two courses.
+        $expectedfields = array('id', 'course', 'name', 'intro', 'introformat', 'introfiles', 'revision', 'timemodified',
+                                'display', 'showexpanded', 'showdownloadfolder', 'section', 'visible', 'groupmode', 'groupingid');
+
+        // Add expected coursemodule and data.
+        $folder1->coursemodule = $folder1->cmid;
+        $folder1->introformat = 1;
+        $folder1->section = 0;
+        $folder1->visible = true;
+        $folder1->groupmode = 0;
+        $folder1->groupingid = 0;
+        $folder1->introfiles = [];
+
+        $folder2->coursemodule = $folder2->cmid;
+        $folder2->introformat = 1;
+        $folder2->section = 0;
+        $folder2->visible = true;
+        $folder2->groupmode = 0;
+        $folder2->groupingid = 0;
+        $folder2->introfiles = [];
+
+        foreach ($expectedfields as $field) {
+            $expected1[$field] = $folder1->{$field};
+            $expected2[$field] = $folder2->{$field};
+        }
 
+        $expectedfolders = array($expected2, $expected1);
+
+        // Call the external function passing course ids.
+        $result = mod_folder_external::get_folders_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedfolders, $result['folders']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Call the external function without passing course id.
+        $result = mod_folder_external::get_folders_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedfolders, $result['folders']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Add a file to the intro.
+        $fileintroname = "fileintro.txt";
+        $filerecordinline = array(
+            'contextid' => context_module::instance($folder2->cmid)->id,
+            'component' => 'mod_folder',
+            '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_folder_external::get_folders_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertCount(1, $result['folders'][0]['introfiles']);
+        $this->assertEquals($fileintroname, $result['folders'][0]['introfiles'][0]['filename']);
+
+        // Unenrol user from second course.
+        $enrol->unenrol_user($instance2, $student->id);
+        array_shift($expectedfolders);
+
+        // Call the external function without passing course id.
+        $result = mod_folder_external::get_folders_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedfolders, $result['folders']);
+
+        // Call for the second course we unenrolled the user from, expected warning.
+        $result = mod_folder_external::get_folders_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 dcd742a..e5a9d08 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_folder';     // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
index e2bd7ce..6a77898 100644 (file)
@@ -48,7 +48,6 @@ class big_search_form implements renderable, templatable {
     public $fullwords;
     public $notwords;
     public $phrase;
-    public $scripturl;
     public $showfullwords;
     public $subject;
     public $user;
@@ -65,7 +64,6 @@ class big_search_form implements renderable, templatable {
     public function __construct($course) {
         global $DB;
         $this->course = $course;
-        $this->scripturl = new moodle_url('/mod/forum/forum.js');
         $this->showfullwords = $DB->get_dbfamily() == 'mysql' || $DB->get_dbfamily() == 'postgres';
         $this->actionurl = new moodle_url('/mod/forum/search.php');
 
@@ -153,7 +151,6 @@ class big_search_form implements renderable, templatable {
     public function export_for_template(renderer_base $output) {
         $data = new stdClass();
 
-        $data->scripturl = $this->scripturl->out(false);
         $data->courseid = $this->course->id;
         $data->words = $this->words;
         $data->phrase = $this->phrase;
diff --git a/mod/forum/forum.js b/mod/forum/forum.js
deleted file mode 100644 (file)
index a6881b3..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-var timefromitems = ['fromday','frommonth','fromyear','fromhour', 'fromminute'];
-var timetoitems = ['today','tomonth','toyear','tohour','tominute'];
-
-function forum_produce_subscribe_link(forumid, backtoindex, ltext, ltitle) {
-    var elementid = "subscriptionlink";
-    var subs_link = document.getElementById(elementid);
-    if(subs_link){
-        subs_link.innerHTML = "<a title='"+ltitle+"' href='"+M.cfg.wwwroot+"/mod/forum/subscribe.php?id="+forumid+backtoindex+"&amp;sesskey="+M.cfg.sesskey+"'>"+ltext+"<\/a>";
-    }
-}
-
-function forum_produce_tracking_link(forumid, ltext, ltitle) {
-    var elementid = "trackinglink";
-    var subs_link = document.getElementById(elementid);
-    if(subs_link){
-        subs_link.innerHTML = "<a title='"+ltitle+"' href='"+M.cfg.wwwroot+"/mod/forum/settracking.php?id="+forumid+"&amp;sesskey="+M.cfg.sesskey+"'>"+ltext+"<\/a>";
-    }
-}
-
-function lockoptions_timetoitems() {
-    lockoptions('searchform','timefromrestrict', timefromitems);
-}
-
-function lockoptions_timefromitems() {
-    lockoptions('searchform','timetorestrict', timetoitems);
-}
-
-function lockoptions(formid, master, subitems) {
-    // Subitems is an array of names of sub items.
-    // Optionally, each item in subitems may have a
-    // companion hidden item in the form with the
-    // same name but prefixed by "h".
-    var form = document.forms[formid], i;
-    if (form[master].checked) {
-        for (i=0; i<subitems.length; i++) {
-            unlockoption(form, subitems[i]);
-        }
-    } else {
-        for (i=0; i<subitems.length; i++) {
-            lockoption(form, subitems[i]);
-        }
-    }
-    return(true);
-}
-
-
-function lockoption(form,item) {
-    form[item].setAttribute('disabled', 'disabled');
-    if (form.elements['h'+item]) {
-        form.elements['h'+item].value=1;
-    }
-}
-
-function unlockoption(form,item) {
-    form[item].removeAttribute('disabled');
-    if (form.elements['h'+item]) {
-        form.elements['h'+item].value=0;
-    }
-}
index 79168f5..970396c 100644 (file)
@@ -3499,7 +3499,7 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
 
     // Mark the forum post as read if required
     if ($istracked && !$CFG->forum_usermarksread && !$postisread) {
-        forum_tp_mark_post_read($USER->id, $post, $forum->id);
+        forum_tp_mark_post_read($USER->id, $post);
     }
 
     if ($return) {
@@ -4393,7 +4393,7 @@ function forum_add_new_post($post, $mform, $unused = null) {
     $DB->set_field("forum_discussions", "usermodified", $post->userid, array("id" => $post->discussion));
 
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
-        forum_tp_mark_post_read($post->userid, $post, $post->forum);
+        forum_tp_mark_post_read($post->userid, $post);
     }
 
     // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
@@ -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;
@@ -4458,7 +4458,7 @@ function forum_update_post($newpost, $mform, $unused = null) {
     forum_add_attachment($post, $forum, $cm, $mform);
 
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
-        forum_tp_mark_post_read($USER->id, $post, $post->forum);
+        forum_tp_mark_post_read($USER->id, $post);
     }
 
     // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
@@ -4536,7 +4536,7 @@ function forum_add_discussion($discussion, $mform=null, $unused=null, $userid=nu
     }
 
     if (forum_tp_can_track_forums($forum) && forum_tp_is_tracked($forum)) {
-        forum_tp_mark_post_read($post->userid, $post, $post->forum);
+        forum_tp_mark_post_read($post->userid, $post);
     }
 
     // Let Moodle know that assessable content is uploaded (eg for plagiarism detection)
@@ -4791,12 +4791,13 @@ function forum_post_subscription($fromform, $forum, $discussion) {
  *      Any strings not passed in are taken from the $defaultmessages array
  *      at the top of the function.
  * @param bool $cantaccessagroup
- * @param bool $fakelink
+ * @param bool $unused1
  * @param bool $backtoindex
- * @param array $subscribed_forums
+ * @param array $unused2
  * @return string
  */
-function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $fakelink=true, $backtoindex=false, $subscribed_forums=null) {
+function forum_get_subscribe_link($forum, $context, $messages = array(), $cantaccessagroup = false, $unused1 = true,
+    $backtoindex = false, $unused2 = null) {
     global $CFG, $USER, $PAGE, $OUTPUT;
     $defaultmessages = array(
         'subscribed' => get_string('unsubscribe', 'forum'),
@@ -4835,22 +4836,11 @@ function forum_get_subscribe_link($forum, $context, $messages = array(), $cantac
         } else {
             $backtoindexlink = '';
         }
-        $link = '';
 
-        if ($fakelink) {
-            $PAGE->requires->js('/mod/forum/forum.js');
-            $PAGE->requires->js_function_call('forum_produce_subscribe_link', array($forum->id, $backtoindexlink, $linktext, $linktitle));
-            $link = "<noscript>";
-        }
         $options['id'] = $forum->id;
         $options['sesskey'] = sesskey();
         $url = new moodle_url('/mod/forum/subscribe.php', $options);
-        $link .= $OUTPUT->single_button($url, $linktext, 'get', array('title'=>$linktitle));
-        if ($fakelink) {
-            $link .= '</noscript>';
-        }
-
-        return $link;
+        return $OUTPUT->single_button($url, $linktext, 'get', array('title' => $linktitle));
     }
 }
 
@@ -6202,9 +6192,12 @@ function forum_tp_add_read_record($userid, $postid) {
 /**
  * If its an old post, do nothing. If the record exists, the maintenance will clear it up later.
  *
+ * @param   int     $userid The ID of the user to mark posts read for.
+ * @param   object  $post   The post record for the post to mark as read.
+ * @param   mixed   $unused
  * @return bool
  */
-function forum_tp_mark_post_read($userid, $post, $forumid) {
+function forum_tp_mark_post_read($userid, $post, $unused = null) {
     if (!forum_tp_is_post_old($post)) {
         return forum_tp_add_read_record($userid, $post->id);
     } else {
index ffdc388..8a8a7f6 100644 (file)
@@ -21,7 +21,6 @@
 
     Example context (json):
     {
-        "scripturl": "https://example.com/mod/forum/forum.js",
         "actionurl": "https://example.com/mod/forum/search.php",
         "courseid": "2",
         "words": "apples",
@@ -52,9 +51,8 @@
     {{#str}}searchforumintro, forum{{/str}}
 </div>
 <div class="box generalbox boxaligncenter">
-    <script type="text/javascript" src="{{scripturl}}"></script>
     <form id="searchform" action="{{actionurl}}" method="get">
-        <table class="searchbox" id="form" cellpadding="10">
+        <table class="searchbox table" id="form">
             <tr>
                 <td class="c0">
                     <label for="words">{{#str}}searchwords, forum{{/str}}</label>
@@ -95,7 +93,7 @@
                     {{#str}}searchdatefrom, forum{{/str}}
                 </td>
                 <td class="c1">
-                    <input type="checkbox" name="timefromrestrict" value="1" onclick="return lockoptions('searchform', 'timefromrestrict', timefromitems)" {{#datefromchecked}}checked{{/datefromchecked}}>
+                    <input type="checkbox" name="timefromrestrict" value="1" {{#datefromchecked}}checked{{/datefromchecked}}>
                     {{{datefromfields}}}
                     <input type="hidden" name="hfromday" value="0">
                     <input type="hidden" name="hfrommonth" value="0">
                     {{#str}}searchdateto, forum{{/str}}
                 </td>
                 <td class="c1">
-                    <input type="checkbox" name="timetorestrict" value="1" onclick="return lockoptions('searchform', 'timetorestrict', timetoitems)" {{#datetochecked}}checked{{/datetochecked}}>
+                    <input type="checkbox" name="timetorestrict" value="1" {{#datetochecked}}checked{{/datetochecked}}>
                     {{{datetofields}}}
                     <input type="hidden" name="htoday" value="0">
                     <input type="hidden" name="htomonth" value="0">
                 </td>
             </tr>
             <tr>
-                <td colspan="2" class="submit" align="center">
-                    <input type="submit" value={{#quote}}{{#str}}searchforums, forum{{/str}}{{/quote}}>
+                <td colspan="2" class="submit">
+                    <div class="text-center">
+                        <input type="submit" value={{#quote}}{{#str}}searchforums, forum{{/str}}{{/quote}}>
+                    </div>
                 </td>
             </tr>
         </table>
     </form>
-    {{#js}}
-        lockoptions_timetoitems();
-        lockoptions_timefromitems();
-    {{/js}}
 </div>
+{{#js}}
+    require(['jquery'], function($) {
+        var toggleDateFields = function(prefix, disabled) {
+            $('#searchform select[name^=' + prefix + ']').prop('disabled', disabled);
+            $('#searchform input[name^=h' + prefix + ']').val(disabled ? 1 : 0);
+        };
+
+        toggleDateFields('from', true);
+        $("#searchform input[name='timefromrestrict']").click(function() {
+            toggleDateFields('from', !this.checked);
+        });
+
+        toggleDateFields('to', true);
+        $("#searchform input[name='timetorestrict']").click(function() {
+            toggleDateFields('to', !this.checked);
+        });
+    });
+{{/js}}
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 7cd1a70..966d9af 100644 (file)
@@ -4,6 +4,9 @@ information provided here is intended especially for developers.
 === 3.3 ===
   * External function get_forums_by_courses now returns and additional field "istracked" that indicates if the user
    is tracking the related forum.
+* The legacy forum.js file has been removed, this includes the js functions:
+    forum_produce_subscribe_link, forum_produce_tracking_link, lockoptions_timetoitems,
+    lockoptions_timefromitems, lockoptions, lockoption, unlockoption
 
 === 3.2 ===
  * The setting $CFG->forum_replytouser has been removed in favour of a centralized noreplyaddress setting.
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 417d8d6..9ef7336 100644 (file)
@@ -104,4 +104,114 @@ class mod_page_external extends external_api {
         );
     }
 
+    /**
+     * Describes the parameters for get_pages_by_courses.
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.3
+     */
+    public static function get_pages_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 pages in a provided list of courses.
+     * If no list is provided all pages that the user can view will be returned.
+     *
+     * @param array $courseids course ids
+     * @return array of warnings and pages
+     * @since Moodle 3.3
+     */
+    public static function get_pages_by_courses($courseids = array()) {
+
+        $warnings = array();
+        $returnedpages = array();
+
+        $params = array(
+            'courseids' => $courseids,
+        );
+        $params = self::validate_parameters(self::get_pages_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 pages in this course, this function checks users visibility permissions.
+            // We can avoid then additional validate_context calls.
+            $pages = get_all_instances_in_courses("page", $courses);
+            foreach ($pages as $page) {
+                $context = context_module::instance($page->coursemodule);
+                // Entry to return.
+                $page->name = external_format_string($page->name, $context->id);
+
+                list($page->intro, $page->introformat) = external_format_text($page->intro,
+                                                                $page->introformat, $context->id, 'mod_page', 'intro', null);
+                $page->introfiles = external_util::get_area_files($context->id, 'mod_page', 'intro', false, false);
+
+                list($page->content, $page->contentformat) = external_format_text($page->content, $page->contentformat,
+                                                                $context->id, 'mod_page', 'content', $page->revision);
+                $page->contentfiles = external_util::get_area_files($context->id, 'mod_page', 'content');
+
+                $returnedpages[] = $page;
+            }
+        }
+
+        $result = array(
+            'pages' => $returnedpages,
+            'warnings' => $warnings
+        );
+        return $result;
+    }
+
+    /**
+     * Describes the get_pages_by_courses return value.
+     *
+     * @return external_single_structure
+     * @since Moodle 3.3
+     */
+    public static function get_pages_by_courses_returns() {
+        return new external_single_structure(
+            array(
+                'pages' => 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'),
+                            'content' => new external_value(PARAM_RAW, 'Page content'),
+                            'contentformat' => new external_format_value('content', 'Content format'),
+                            'contentfiles' => new external_files('Files in the content'),
+                            '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 page'),
+                            'displayoptions' => new external_value(PARAM_RAW, 'Display options (width, height)'),
+                            'revision' => new external_value(PARAM_INT, 'Incremented when after each file changes, to avoid cache'),
+                            'timemodified' => new external_value(PARAM_INT, 'Last time the page 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 6080069..087019d 100644 (file)
@@ -37,4 +37,13 @@ $functions = array(
         'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE)
     ),
 
+    'mod_page_get_pages_by_courses' => array(
+        'classname'     => 'mod_page_external',
+        'methodname'    => 'get_pages_by_courses',
+        'description'   => 'Returns a list of pages in a provided list of courses, if no list is provided all pages that the user
+                            can view will be returned.',
+        'type'          => 'read',
+        'capabilities'  => 'mod/page:view',
+        'services'      => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
 );
index 075af6e..e30506f 100644 (file)
@@ -110,4 +110,126 @@ class mod_page_external_testcase extends externallib_advanced_testcase {
         }
 
     }
+
+    /**
+     * Test test_mod_page_get_pages_by_courses
+     */
+    public function test_mod_page_get_pages_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);
+
+        // First page.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $page1 = self::getDataGenerator()->create_module('page', $record);
+
+        // Second page.
+        $record = new stdClass();
+        $record->course = $course2->id;
+        $page2 = self::getDataGenerator()->create_module('page', $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);
+
+        self::setUser($student);
+
+        $returndescription = mod_page_external::get_pages_by_courses_returns();
+
+        // Create what we expect to be returned when querying the two courses.
+        $expectedfields = array('id', 'course', 'name', 'intro', 'introformat', 'introfiles',
+                                'content', 'contentformat', 'contentfiles', 'legacyfiles', 'legacyfileslast', 'display',
+                                'displayoptions', 'revision', 'timemodified', 'section', 'visible', 'groupmode', 'groupingid');
+
+        // Add expected coursemodule and data.
+        $page1->coursemodule = $page1->cmid;
+        $page1->introformat = 1;
+        $page1->contentformat = 1;
+        $page1->section = 0;
+        $page1->visible = true;
+        $page1->groupmode = 0;
+        $page1->groupingid = 0;
+        $page1->introfiles = [];
+        $page1->contentfiles = [];
+
+        $page2->coursemodule = $page2->cmid;
+        $page2->introformat = 1;
+        $page2->contentformat = 1;
+        $page2->section = 0;
+        $page2->visible = true;
+        $page2->groupmode = 0;
+        $page2->groupingid = 0;
+        $page2->introfiles = [];
+        $page2->contentfiles = [];
+
+        foreach ($expectedfields as $field) {
+            $expected1[$field] = $page1->{$field};
+            $expected2[$field] = $page2->{$field};
+        }
+
+        $expectedpages = array($expected2, $expected1);
+
+        // Call the external function passing course ids.
+        $result = mod_page_external::get_pages_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertEquals($expectedpages, $result['pages']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Call the external function without passing course id.
+        $result = mod_page_external::get_pages_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedpages, $result['pages']);
+        $this->assertCount(0, $result['warnings']);
+
+        // Add a file to the intro.
+        $filename = "file.txt";
+        $filerecordinline = array(
+            'contextid' => context_module::instance($page2->cmid)->id,
+            'component' => 'mod_page',
+            'filearea'  => 'intro',
+            'itemid'    => 0,
+            'filepath'  => '/',
+            'filename'  => $filename,
+        );
+        $fs = get_file_storage();
+        $timepost = time();
+        $fs->create_file_from_string($filerecordinline, 'image contents (not really)');
+
+        $result = mod_page_external::get_pages_by_courses(array($course2->id, $course1->id));
+        $result = external_api::clean_returnvalue($returndescription, $result);
+
+        $this->assertCount(1, $result['pages'][0]['introfiles']);
+        $this->assertEquals($filename, $result['pages'][0]['introfiles'][0]['filename']);
+
+        // Unenrol user from second course.
+        $enrol->unenrol_user($instance2, $student->id);
+        array_shift($expectedpages);
+
+        // Call the external function without passing course id.
+        $result = mod_page_external::get_pages_by_courses();
+        $result = external_api::clean_returnvalue($returndescription, $result);
+        $this->assertEquals($expectedpages, $result['pages']);
+
+        // Call for the second course we unenrolled the user from, expected warning.
+        $result = mod_page_external::get_pages_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 de35e1a..28784e7 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_page';       // Full name of the plugin (used for diagnostics)
 $plugin->cron      = 0;
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 f7ea46b..2cfb908 100644 (file)
@@ -36,7 +36,7 @@ $THEME->scss = function($theme) {
 $THEME->layouts = [
     // Most backwards compatible layout without the blocks - this is the layout used by default.
     'base' => array(
-        'file' => 'columns1.php',
+        'file' => 'columns2.php',
         'regions' => array(),
     ),
     // Standard layout with blocks, this is recommended for most pages with general information.
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 3e96080..32d30d4 100644 (file)
@@ -21,7 +21,6 @@
 
     Example context (json):
     {
-        "scripturl": "https://example.com/mod/forum/forum.js",
         "actionurl": "https://example.com/mod/forum/search.php",
         "courseid": "2",
         "words": "apples",
@@ -52,9 +51,8 @@
     {{#str}}searchforumintro, forum{{/str}}
 </div>
 <div class="box generalbox boxaligncenter">
-    <script type="text/javascript" src="{{scripturl}}"></script>
     <form id="searchform" action="{{actionurl}}" method="get">
-        <table class="searchbox table table-striped" id="form" cellpadding="10">
+        <table class="searchbox table table-striped" id="form">
             <tr>
                 <td class="c0 text-xs-right">
                     <label for="words">{{#str}}searchwords, forum{{/str}}</label>
@@ -95,7 +93,7 @@
                     {{#str}}searchdatefrom, forum{{/str}}
                 </td>
                 <td class="c1 text-nowrap form-inline">
-                    <input type="checkbox" name="timefromrestrict" value="1" onclick="return lockoptions('searchform', 'timefromrestrict', timefromitems)" {{#datefromchecked}}checked{{/datefromchecked}}>
+                    <input type="checkbox" name="timefromrestrict" value="1" {{#datefromchecked}}checked{{/datefromchecked}}>
                     {{{datefromfields}}}
                     <input type="hidden" name="hfromday" value="0">
                     <input type="hidden" name="hfrommonth" value="0">
                     {{#str}}searchdateto, forum{{/str}}
                 </td>
                 <td class="c1 text-nowrap form-inline">
-                    <input type="checkbox" name="timetorestrict" value="1" onclick="return lockoptions('searchform', 'timetorestrict', timetoitems)" {{#datetochecked}}checked{{/datetochecked}}>
+                    <input type="checkbox" name="timetorestrict" value="1" {{#datetochecked}}checked{{/datetochecked}}>
                     {{{datetofields}}}
                     <input type="hidden" name="htoday" value="0">
                     <input type="hidden" name="htomonth" value="0">
             </tr>
         </table>
     </form>
-    {{#js}}
-        lockoptions_timetoitems();
-        lockoptions_timefromitems();
-    {{/js}}
 </div>
+{{#js}}
+require(['jquery'], function($) {
+    var toggleDateFields = function(prefix, disabled) {
+        $('#searchform select[name^=' + prefix + ']').prop('disabled', disabled);
+        $('#searchform input[name^=h' + prefix + ']').val(disabled ? 1 : 0);
+    };
+
+    toggleDateFields('from', true);
+    $("#searchform input[name='timefromrestrict']").click(function() {
+        toggleDateFields('from', !this.checked);
+    });
+
+    toggleDateFields('to', true);
+    $("#searchform input[name='timetorestrict']").click(function() {
+        toggleDateFields('to', !this.checked);
+    });
+});
+{{/js}}
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) {