Merge branch 'wip-MDL-56864-master' of git://github.com/marinaglancy/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 12 Dec 2017 05:36:32 +0000 (13:36 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 12 Dec 2017 05:36:32 +0000 (13:36 +0800)
325 files changed:
.gitattributes
.travis.yml
admin/auth_config.php
admin/cli/fix_deleted_users.php
admin/environment.xml
admin/registration/confirmregistration.php
admin/registration/renewregistration.php
admin/tool/analytics/classes/clihelper.php [new file with mode: 0644]
admin/tool/analytics/classes/output/models_list.php
admin/tool/analytics/cli/enable_model.php
admin/tool/analytics/cli/evaluate_model.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/templates/models_list.mustache
admin/tool/behat/cli/util.php
admin/tool/customlang/db/upgrade.php
admin/tool/httpsreplace/tests/httpsreplace_test.php
admin/tool/log/db/upgrade.php
admin/tool/log/store/database/db/upgrade.php
admin/tool/log/store/standard/db/upgrade.php
admin/tool/messageinbound/index.php
admin/tool/monitor/db/upgrade.php
admin/tool/oauth2/classes/form/issuer.php
admin/tool/oauth2/lang/en/tool_oauth2.php
admin/tool/spamcleaner/index.php
admin/tool/uploaduser/index.php
admin/tool/uploaduser/tests/behat/upload_users.feature
admin/user.php
analytics/classes/course.php
analytics/classes/manager.php
analytics/classes/model.php
analytics/tests/dataset_manager_test.php
analytics/tests/model_test.php
auth/cas/db/upgrade.php
auth/db/lang/en/auth_db.php
auth/ldap/db/upgrade.php
auth/ldap/lang/en/auth_ldap.php
auth/ldap/settings.php
auth/manual/db/upgrade.php
auth/mnet/db/upgrade.php
backup/moodle2/restore_stepslib.php
backup/util/settings/base_setting.class.php
backup/util/settings/setting_dependency.class.php
backup/util/ui/backup_ui_setting.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature [new file with mode: 0644]
badges/badge.php
badges/classes/external.php
badges/classes/observer.php
badges/criteria/award_criteria.php
badges/criteria/award_criteria_badge.php [new file with mode: 0644]
badges/edit_form.php
badges/mybadges.php
badges/tests/badgeslib_test.php
badges/tests/behat/add_badge.feature
badges/tests/behat/award_badge.feature
blocks/badges/db/upgrade.php
blocks/calendar_month/db/upgrade.php
blocks/calendar_upcoming/db/upgrade.php
blocks/community/db/upgrade.php
blocks/completionstatus/db/upgrade.php
blocks/course_summary/db/upgrade.php
blocks/globalsearch/block_globalsearch.php
blocks/html/db/upgrade.php
blocks/lp/classes/output/summary.php
blocks/navigation/db/upgrade.php
blocks/quiz_results/db/upgrade.php
blocks/recent_activity/db/upgrade.php
blocks/rss_client/db/upgrade.php
blocks/section_links/db/upgrade.php
blocks/selfcompletion/db/upgrade.php
blocks/settings/db/upgrade.php
calendar/classes/external/month_exporter.php
calendar/classes/local/event/mappers/event_mapper.php
calendar/externallib.php
calendar/lib.php
calendar/tests/calendartype_test.php
calendar/tests/container_test.php
calendar/tests/event_factory_test.php
calendar/tests/event_mapper_test.php
calendar/tests/externallib_test.php
calendar/tests/helpers.php
calendar/tests/repeat_event_collection_test.php
calendar/view.php
cohort/classes/external/cohort_summary_exporter.php
cohort/lib.php
competency/classes/api.php
competency/classes/competency.php
competency/classes/template_competency.php
competency/classes/user_competency_plan.php
competency/tests/external_test.php
completion/classes/external.php
completion/criteria/completion_criteria_activity.php
completion/tests/externallib_test.php
composer.json
composer.lock
config-dist.php
course/externallib.php
course/tests/externallib_test.php
enrol/database/db/upgrade.php
enrol/externallib.php
enrol/flatfile/db/upgrade.php
enrol/guest/db/upgrade.php
enrol/imsenterprise/db/upgrade.php
enrol/imsenterprise/tests/imsenterprise_test.php
enrol/manual/db/upgrade.php
enrol/mnet/db/upgrade.php
enrol/paypal/db/upgrade.php
enrol/self/db/upgrade.php
enrol/tests/externallib_test.php
files/classes/converter.php
files/tests/converter_test.php
filter/mathjaxloader/db/upgrade.php
filter/mediaplugin/db/upgrade.php
filter/tex/db/upgrade.php
grade/grading/form/guide/db/upgrade.php
grade/grading/form/lib.php
grade/grading/form/rubric/db/upgrade.php
grade/grading/lib.php
grade/report/singleview/classes/local/screen/screen.php
grade/report/user/db/upgrade.php
grade/tests/edittreelib_test.php
grade/tests/importlib_test.php
grade/tests/querylib_test.php
grade/tests/report_graderlib_test.php
grade/tests/reportlib_test.php
grade/tests/reportuserlib_test.php
group/externallib.php
group/import.php
group/lib.php
group/tests/behat/groups_import.feature
group/tests/externallib_test.php
group/tests/fixtures/groups_import_multicourse.csv [new file with mode: 0644]
lang/en/auth.php
lang/en/badges.php
lang/en/cache.php
lang/en/form.php
lang/en/search.php
lib/accesslib.php
lib/amd/build/form-autocomplete.min.js
lib/amd/src/form-autocomplete.js
lib/antivirus/clamav/db/upgrade.php
lib/authlib.php
lib/badgeslib.php
lib/blocklib.php
lib/classes/access/get_user_capability_course_helper.php
lib/classes/hub/api.php
lib/classes/message/inbound/manager.php
lib/classes/oauth2/client.php
lib/classes/oauth2/issuer.php
lib/classes/plugininfo/gradingform.php
lib/classes/plugininfo/portfolio.php
lib/classes/task/delete_unconfirmed_users_task.php
lib/completionlib.php
lib/dataformatlib.php
lib/db/caches.php
lib/db/events.php
lib/db/install.xml
lib/db/upgrade.php
lib/dml/moodle_database.php
lib/dml/oci_native_moodle_package.sql
lib/dml/pgsql_native_moodle_database.php
lib/dml/pgsql_native_moodle_recordset.php
lib/dml/tests/pgsql_native_recordset_test.php [new file with mode: 0644]
lib/editor/atto/db/upgrade.php
lib/editor/atto/plugins/equation/db/upgrade.php
lib/editor/tinymce/db/upgrade.php
lib/editor/tinymce/plugins/spellchecker/db/upgrade.php
lib/enrollib.php
lib/filelib.php
lib/filestorage/file_storage.php
lib/form/dateselector.php
lib/form/datetimeselector.php
lib/form/defaultcustom.php
lib/form/passwordunmask.php
lib/form/templatable_form_element.php
lib/grouplib.php
lib/moodlelib.php
lib/navigationlib.php
lib/oauthlib.php
lib/outputcomponents.php
lib/outputlib.php
lib/outputrenderers.php
lib/tablelib.php
lib/testing/classes/util.php
lib/testing/generator/data_generator.php
lib/tests/accesslib_test.php
lib/tests/blocklib_test.php
lib/tests/formslib_test.php
lib/tests/gradelib_test.php
lib/yui/build/moodle-core-checknet/moodle-core-checknet-debug.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet-min.js
lib/yui/build/moodle-core-checknet/moodle-core-checknet.js
lib/yui/src/checknet/js/checknet.js
message/output/email/db/upgrade.php
message/output/jabber/db/upgrade.php
message/output/popup/db/upgrade.php
message/tests/externallib_test.php
mod/assign/db/upgrade.php
mod/assign/feedback/comments/db/upgrade.php
mod/assign/feedback/editpdf/db/upgrade.php
mod/assign/feedback/editpdf/lang/en/assignfeedback_editpdf.php
mod/assign/feedback/editpdf/locallib.php
mod/assign/feedback/editpdf/settings.php
mod/assign/feedback/file/db/upgrade.php
mod/assign/styles.css
mod/assign/submission/comments/db/upgrade.php
mod/assign/submission/file/db/upgrade.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/db/upgrade.php
mod/assign/submission/onlinetext/locallib.php
mod/assignment/db/upgrade.php
mod/book/db/upgrade.php
mod/chat/db/upgrade.php
mod/chat/lib.php
mod/choice/amd/build/select_all_choices.min.js [new file with mode: 0644]
mod/choice/amd/src/select_all_choices.js [new file with mode: 0644]
mod/choice/db/upgrade.php
mod/choice/renderer.php
mod/data/db/upgrade.php
mod/data/lang/en/data.php
mod/data/lib.php
mod/data/tests/lib_test.php
mod/feedback/classes/responses_table.php
mod/feedback/db/upgrade.php
mod/feedback/db/upgradelib.php [deleted file]
mod/feedback/tests/upgradelib_test.php [deleted file]
mod/feedback/upgrade.txt
mod/folder/db/upgrade.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/message/inbound/reply_handler.php
mod/forum/db/upgrade.php
mod/forum/tests/subscriptions_test.php
mod/glossary/db/upgrade.php
mod/glossary/lang/en/glossary.php
mod/glossary/lib.php
mod/glossary/print.php
mod/glossary/view.php
mod/imscp/db/upgrade.php
mod/label/db/upgrade.php
mod/lesson/db/upgrade.php
mod/lesson/lang/en/lesson.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/lib_test.php
mod/lti/backup/moodle2/backup_lti_stepslib.php
mod/lti/backup/moodle2/restore_lti_stepslib.php
mod/lti/db/upgrade.php
mod/lti/lib.php
mod/lti/locallib.php
mod/lti/tests/behat/addtype.feature
mod/lti/tests/behat/backup_restore.feature [new file with mode: 0644]
mod/lti/version.php
mod/lti/view.php
mod/page/db/upgrade.php
mod/quiz/db/upgrade.php
mod/quiz/index.php
mod/quiz/lang/en/quiz.php
mod/quiz/lib.php
mod/quiz/mod_form.php
mod/quiz/override_form.php
mod/quiz/report/overview/db/upgrade.php
mod/quiz/report/statistics/db/upgrade.php
mod/quiz/tests/attempts_test.php
mod/resource/db/upgrade.php
mod/scorm/db/upgrade.php
mod/scorm/player.php
mod/survey/db/upgrade.php
mod/url/db/upgrade.php
mod/wiki/db/upgrade.php
mod/wiki/locallib.php
mod/workshop/classes/external.php
mod/workshop/db/upgrade.php
mod/workshop/form/accumulative/db/upgrade.php
mod/workshop/form/comments/db/upgrade.php
mod/workshop/form/numerrors/db/upgrade.php
mod/workshop/form/rubric/db/upgrade.php
mod/workshop/tests/external_test.php
portfolio/boxnet/db/upgrade.php
portfolio/googledocs/db/upgrade.php
portfolio/picasa/db/upgrade.php
question/behaviour/manualgraded/db/upgrade.php
question/classes/bank/view.php
question/type/calculated/db/upgrade.php
question/type/ddmarker/db/upgrade.php
question/type/essay/db/upgrade.php
question/type/match/db/upgrade.php
question/type/multianswer/db/upgrade.php
question/type/multianswer/questiontype.php
question/type/multichoice/db/upgrade.php
question/type/numerical/db/upgrade.php
question/type/random/db/upgrade.php
question/type/randomsamatch/db/upgrade.php
question/type/shortanswer/db/upgrade.php
report/security/locallib.php
repository/boxnet/db/upgrade.php
repository/dropbox/db/upgrade.php
repository/googledocs/db/upgrade.php
repository/lib.php
repository/picasa/db/upgrade.php
search/classes/engine.php
search/classes/manager.php
search/classes/output/form/search.php
search/engine/solr/classes/engine.php
search/engine/solr/tests/engine_test.php
search/index.php
search/tests/base_test.php
search/tests/behat/behat_search.php
search/tests/behat/search_query.feature
search/tests/fixtures/mock_search_area.php
search/tests/fixtures/testable_core_search.php
search/tests/generator/lib.php
search/tests/manager_test.php
theme/boost/scss/moodle/modules.scss
theme/boost/templates/core_form/element-button-inline.mustache
theme/boost/templates/core_form/element-button.mustache
theme/bootstrapbase/less/moodle/modules.less
theme/bootstrapbase/style/moodle.css
theme/more/db/upgrade.php
user/lib.php
user/profile/lib.php
user/tests/externallib_test.php
user/tests/userlib_test.php
version.php
webservice/externallib.php
webservice/xmlrpc/lib.php

index fb39fe0..8afacce 100644 (file)
@@ -1,4 +1,5 @@
 **/yui/build/** -diff
 **/amd/build/** -diff
+lib/dml/oci_native_moodle_package.sql text eol=lf
 theme/bootstrapbase/style/editor.css -diff
 theme/bootstrapbase/style/moodle.css -diff
index 5139a7c..9e436f5 100644 (file)
@@ -23,8 +23,9 @@ addons:
     - mysql-client-core-5.6
     - mysql-client-5.6
 
-services:
-    - redis-server
+# Redis tests are currently failing on php 7.2 due to https://bugs.php.net/bug.php?id=75628
+# services:
+#     - redis-server
 
 env:
     # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to
@@ -99,8 +100,9 @@ install:
             fi
 
             # Enable Redis.
-            echo 'extension="redis.so"' > /tmp/redis.ini
-            phpenv config-add /tmp/redis.ini
+            # Redis tests are currently failing on php 7.2 due to https://bugs.php.net/bug.php?id=75628
+            # echo 'extension="redis.so"' > /tmp/redis.ini
+            # phpenv config-add /tmp/redis.ini
 
             # Install composer dependencies.
             # We need --no-interaction in case we hit API limits for composer. This causes it to fall back to a standard clone.
@@ -171,10 +173,11 @@ before_script:
         mkdir -p "$HOME"/roots/phpunit
 
         # The phpunit dataroot and prefix..
+        # Redis tests are currently failing on php 7.2 due to https://bugs.php.net/bug.php?id=75628
+        # -e "/require_once/i \\define('TEST_SESSION_REDIS_HOST', '127.0.0.1');" \
         sed -i \
           -e "/require_once/i \\\$CFG->phpunit_dataroot = '\/home\/travis\/roots\/phpunit';" \
           -e "/require_once/i \\\$CFG->phpunit_prefix = 'p_';" \
-          -e "/require_once/i \\define('TEST_SESSION_REDIS_HOST', '127.0.0.1');" \
           config.php ;
 
         # Initialise PHPUnit for Moodle.
index 6c3e315..3ee8ebc 100644 (file)
@@ -165,6 +165,12 @@ function print_auth_lock_options($auth, $user_fields, $helptext, $retrieveopts,
             // If custom field then pick name from database.
             $fieldshortname = str_replace('profile_field_', '', $fieldname);
             $fieldname = $customfieldname[$fieldshortname]->name;
+            if (core_text::strlen($fieldshortname) > 67) {
+                // If custom profile field name is longer than 67 characters we will not be able to store the setting
+                // such as 'field_updateremote_profile_field_NOTSOSHORTSHORTNAME' in the database because the character
+                // limit for the setting name is 100.
+                continue;
+            }
         } elseif ($fieldname == 'url') {
             $fieldname = get_string('webpage');
         } else {
index d2a8950..cc052b4 100644 (file)
@@ -73,6 +73,7 @@ foreach ($rs as $user) {
     echo "Redeleting user $user->id: $user->username ($user->email)\n";
     delete_user($user);
 }
+$rs->close();
 
 cli_heading('Deleting all leftovers');
 
index 1f2f001..2b8d4ef 100644 (file)
       </CUSTOM_CHECK>
     </CUSTOM_CHECKS>
   </MOODLE>
+  <MOODLE version="3.5" requires="3.1">
+    <UNICODE level="required">
+      <FEEDBACK>
+        <ON_ERROR message="unicoderequired" />
+      </FEEDBACK>
+    </UNICODE>
+    <DATABASE level="required">
+      <VENDOR name="mariadb" version="5.5.31" />
+      <VENDOR name="mysql" version="5.5.31" />
+      <VENDOR name="postgres" version="9.3" />
+      <VENDOR name="mssql" version="10.0" />
+      <VENDOR name="oracle" version="10.2" />
+    </DATABASE>
+    <PHP version="7.0.0" level="required">
+    </PHP>
+    <PCREUNICODE level="optional">
+      <FEEDBACK>
+        <ON_CHECK message="pcreunicodewarning" />
+      </FEEDBACK>
+    </PCREUNICODE>
+    <PHP_EXTENSIONS>
+      <PHP_EXTENSION name="iconv" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="iconvrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="mbstring" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="mbstringrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="curl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="curlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="openssl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="opensslrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="tokenizer" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="tokenizerrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlrpc" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="xmlrpcrecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="soap" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="soaprecommended" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="ctype" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ctyperequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zip" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="ziprequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="zlib" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="gd" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="gdrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="simplexml" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="simplexmlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="spl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="splrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="pcre" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="dom" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xml" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="xmlreader" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="intl" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="intlrequired" />
+        </FEEDBACK>
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="json" level="required">
+      </PHP_EXTENSION>
+      <PHP_EXTENSION name="hash" level="required"/>
+      <PHP_EXTENSION name="fileinfo" level="required"/>
+    </PHP_EXTENSIONS>
+    <PHP_SETTINGS>
+      <PHP_SETTING name="memory_limit" value="96M" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="settingmemorylimit" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="file_uploads" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="settingfileuploads" />
+        </FEEDBACK>
+      </PHP_SETTING>
+      <PHP_SETTING name="opcache.enable" value="1" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="opcacherecommended" />
+        </FEEDBACK>
+      </PHP_SETTING>
+    </PHP_SETTINGS>
+    <CUSTOM_CHECKS>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbstorageengine" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="quizattemptsupgradedmessage" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="slashargumentswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unsupporteddbtablerowformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="unoconvwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="libcurlwarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfileformat" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddbfilepertable" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
+        <FEEDBACK>
+          <ON_ERROR message="unsupporteddblargeprefix" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="ishttpswarning" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+      <CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
+        <FEEDBACK>
+          <ON_CHECK message="incompleteunicodesupport" />
+        </FEEDBACK>
+      </CUSTOM_CHECK>
+    </CUSTOM_CHECKS>
+  </MOODLE>
 </COMPATIBILITY_MATRIX>
index 6b20e8d..7c9d422 100644 (file)
@@ -46,6 +46,14 @@ $error = optional_param('error', '', PARAM_ALPHANUM);
 admin_externalpage_setup('registrationmoodleorg');
 
 if ($url !== HUB_MOODLEORGHUBURL) {
+    // Allow other plugins to confirm registration on hubs other than moodle.net . Plugins implementing this
+    // callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
+    $callbacks = get_plugins_with_function('hub_registration');
+    foreach ($callbacks as $plugintype => $plugins) {
+        foreach ($plugins as $plugin => $callback) {
+            $callback('confirm');
+        }
+    }
     throw new moodle_exception('errorotherhubsnotsupported', 'hub');
 }
 
index 7926e0f..84e727b 100644 (file)
@@ -40,6 +40,14 @@ $token = optional_param('token', '', PARAM_TEXT);
 admin_externalpage_setup('registrationmoodleorg');
 
 if ($url !== HUB_MOODLEORGHUBURL) {
+    // Allow other plugins to renew registration on hubs other than moodle.net . Plugins implementing this
+    // callback need to redirect or exit. See https://docs.moodle.org/en/Hub_registration .
+    $callbacks = get_plugins_with_function('hub_registration');
+    foreach ($callbacks as $plugintype => $plugins) {
+        foreach ($plugins as $plugin => $callback) {
+            $callback('renew');
+        }
+    }
     throw new moodle_exception('errorotherhubsnotsupported', 'hub');
 }
 
diff --git a/admin/tool/analytics/classes/clihelper.php b/admin/tool/analytics/classes/clihelper.php
new file mode 100644 (file)
index 0000000..c46e6fc
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Helper class that contains helper functions for cli scripts.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_analytics;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Helper class that contains helper functions for cli scripts.
+ *
+ * @package   tool_analytics
+ * @copyright 2017 onwards Ankit Agarwal
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class clihelper {
+
+    /**
+     * List all models in the system. To be used from cli scripts.
+     *
+     * @return void
+     */
+    public static function list_models() {
+        cli_heading("List of models");
+        echo str_pad(get_string('modelid', 'tool_analytics'), 15, ' ') . ' ' . str_pad(get_string('name'), 50, ' ') .
+            ' ' . str_pad(get_string('status'), 15, ' ') . "\n";
+        $models = \core_analytics\manager::get_all_models();
+        foreach ($models as $model) {
+            $modelid = $model->get_id();
+            $isenabled = $model->is_enabled() ? get_string('enabled', 'tool_analytics') : get_string('disabled', 'tool_analytics');
+            $name = $model->get_target()->get_name();
+            echo str_pad($modelid, 15, ' ') . ' ' . str_pad($name, 50, ' ') . ' ' . str_pad($isenabled, 15, ' ') . "\n";
+        }
+    }
+}
\ No newline at end of file
index 0e8d4e8..cbd0a60 100644 (file)
@@ -119,6 +119,9 @@ class models_list implements \renderable, \templatable {
                     debugging("The time splitting method '{$modeldata->timesplitting}' should include a '{$identifier}_help'
                         string to describe its purpose.", DEBUG_DEVELOPER);
                 }
+            } else {
+                $helpicon = new \help_icon('timesplittingnotdefined', 'tool_analytics');
+                $modeldata->timesplittinghelp = $helpicon->export_for_template($output);
             }
 
             // Has this model generated predictions?.
@@ -207,19 +210,22 @@ class models_list implements \renderable, \templatable {
             }
 
             // Enable / disable.
-            if ($model->is_enabled()) {
-                $action = 'disable';
-                $text = get_string('disable');
-                $icontype = 't/block';
-            } else {
-                $action = 'enable';
-                $text = get_string('enable');
-                $icontype = 'i/checked';
+            if ($model->is_enabled() || !empty($modeldata->timesplitting)) {
+                // If there is no timesplitting method set, the model can not be enabled.
+                if ($model->is_enabled()) {
+                    $action = 'disable';
+                    $text = get_string('disable');
+                    $icontype = 't/block';
+                } else {
+                    $action = 'enable';
+                    $text = get_string('enable');
+                    $icontype = 'i/checked';
+                }
+                $urlparams['action'] = $action;
+                $url = new \moodle_url('model.php', $urlparams);
+                $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
+                $actionsmenu->add($icon);
             }
-            $urlparams['action'] = $action;
-            $url = new \moodle_url('model.php', $urlparams);
-            $icon = new \action_menu_link_secondary($url, new \pix_icon($icontype, $text), $text);
-            $actionsmenu->add($icon);
 
             // Export training data.
             if (!$model->is_static() && $model->is_trained()) {
index bc1219f..e31dfec 100644 (file)
@@ -31,6 +31,7 @@ $help = "Enables the provided model.
 
 Options:
 --modelid           Model id
+--list              List models
 --timesplitting     Time splitting method full class name
 -h, --help          Print out this help
 
@@ -42,6 +43,7 @@ Example:
 list($options, $unrecognized) = cli_get_params(
     array(
         'help'            => false,
+        'list'            => false,
         'modelid'         => false,
         'timesplitting'   => false
     ),
@@ -55,7 +57,12 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['modelid'] === false || $options['timesplitting'] === false) {
+if ($options['list'] || $options['modelid'] === false) {
+    \tool_analytics\clihelper::list_models();
+    exit(0);
+}
+
+if ($options['timesplitting'] === false) {
     echo $help;
     exit(0);
 }
index 5319f0d..25d182f 100644 (file)
@@ -31,6 +31,7 @@ $help = "Evaluates the provided model.
 
 Options:
 --modelid              Model id
+--list                 List models
 --non-interactive      Not interactive questions
 --timesplitting        Restrict the evaluation to 1 single time splitting method (Optional)
 --filter               Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional)
@@ -47,6 +48,7 @@ list($options, $unrecognized) = cli_get_params(
     array(
         'help'                  => false,
         'modelid'               => false,
+        'list'                  => false,
         'timesplitting'         => false,
         'reuse-prev-analysed'   => true,
         'non-interactive'       => false,
@@ -62,8 +64,8 @@ if ($options['help']) {
     exit(0);
 }
 
-if ($options['modelid'] === false) {
-    echo $help;
+if ($options['list'] || $options['modelid'] === false) {
+    \tool_analytics\clihelper::list_models();
     exit(0);
 }
 
index f5c1014..47def2b 100644 (file)
@@ -34,6 +34,7 @@ $string['clearmodelpredictions'] = 'Are you sure you want to clear all "{$a}" pr
 $string['clienablemodel'] = 'You can enable the model by selecting a time-splitting method by its ID. Note that you can also enable it later using the web interface (\'none\' to exit).';
 $string['clievaluationandpredictions'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. You can allow these processes to be executed manually via the web interface by disabling the <a href="{$a}">\'onlycli\'</a> analytics setting.';
 $string['clievaluationandpredictionsnoadmin'] = 'A scheduled task iterates through enabled models and gets predictions. Models evaluation via the web interface is disabled. It may be enabled by a site administrator.';
+$string['disabled'] = 'Disabled';
 $string['editmodel'] = 'Edit "{$a}" model';
 $string['edittrainedwarning'] = 'This model has already been trained. Note that changing its indicators or its time-splitting method will delete its previous predictions and start generating new predictions.';
 $string['enabled'] = 'Enabled';
@@ -66,6 +67,7 @@ $string['invalidanalysablestable'] = 'Invalid site analysable elements table';
 $string['invalidprediction'] = 'Invalid to get predictions';
 $string['invalidtraining'] = 'Invalid to train the model';
 $string['loginfo'] = 'Log extra info';
+$string['modelid'] = 'Model id';
 $string['modelinvalidanalysables'] = 'Invalid analysable elements for "{$a}" model';
 $string['modelresults'] = '{$a} results';
 $string['modeltimesplitting'] = 'Time splitting';
@@ -83,6 +85,8 @@ $string['previouspage'] = 'Previous page';
 $string['samestartdate'] = 'Current start date is good';
 $string['sameenddate'] = 'Current end date is good';
 $string['target'] = 'Target';
+$string['timesplittingnotdefined'] = 'Time splitting is not defined.';
+$string['timesplittingnotdefined_help'] = 'You need to select a time-splitting method before enabling the model.';
 $string['trainandpredictmodel'] = 'Training model and calculating predictions';
 $string['trainingprocessfinished'] = 'Training process finished';
 $string['trainingresults'] = 'Training results';
index 8eb6211..efdc4ca 100644 (file)
                     {{/timesplitting}}
                     {{^timesplitting}}
                         {{#str}}notdefined, tool_analytics{{/str}}
+                        {{#timesplittinghelp}}
+                            {{>core/help_icon}}
+                        {{/timesplittinghelp}}
                     {{/timesplitting}}
                 </td>
                 <td>
index 8efa4ed..e88125b 100644 (file)
@@ -398,7 +398,7 @@ function print_combined_install_output($processes) {
     // Show process name in first row.
     foreach ($processes as $name => $process) {
         // If we don't have enough space to show full run name then show runX.
-        if ($lengthofprocessline < strlen($name + 2)) {
+        if ($lengthofprocessline < strlen($name) + 2) {
             $name = substr($name, -5);
         }
         // One extra padding as we are adding | separator for rest of the data.
index 1c04587..a7ff6a3 100644 (file)
@@ -29,9 +29,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_customlang_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index c10ea2f..e3599c6 100644 (file)
@@ -47,12 +47,12 @@ class httpsreplace_test extends \advanced_testcase {
             "Test image from another site should be replaced" => [
                 "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', true) . '">',
+                "expectedcontent" => '<img src="' . $this->get_converted_http_link('/test.jpg') . '">',
             ],
             "Test object from another site should be replaced" => [
                 "content" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<object data="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+                "expectedcontent" => '<object data="' . $this->get_converted_http_link('/test.swf') . '">',
             ],
             "Test image from a site with international name should be replaced" => [
                 "content" => '<img src="http://中国互联网络信息中心.中国/logosy/201706/W01.png">',
@@ -82,7 +82,7 @@ class httpsreplace_test extends \advanced_testcase {
             "Search for params should be case insensitive" => [
                 "content" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', false) . '">',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<object DATA="' . $this->getExternalTestFileUrl('/test.swf', true) . '">',
+                "expectedcontent" => '<object DATA="' . $this->get_converted_http_link('/test.swf') . '">',
             ],
             "URL should be case insensitive" => [
                 "content" => '<object data="HTTP://some.site/path?query">',
@@ -93,7 +93,7 @@ class httpsreplace_test extends \advanced_testcase {
                 "content" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', false) .
                     '" width="1”><p style="font-size: \'20px\'"></p>',
                 "outputregex" => '/UPDATE/',
-                "expectedcontent" => '<img alt="A picture" src="' . $this->getExternalTestFileUrl('/test.png', true) .
+                "expectedcontent" => '<img alt="A picture" src="' . $this->get_converted_http_link('/test.png') .
                     '" width="1”><p style="font-size: \'20px\'"></p>',
             ],
             "Broken URL should not be changed" => [
@@ -113,11 +113,25 @@ class httpsreplace_test extends \advanced_testcase {
                     $this->getExternalTestFileUrl('/test.jpg', false) . '"></a>',
                 "outputregex" => '/UPDATE/',
                 "expectedcontent" => '<a href="' . $this->getExternalTestFileUrl('/test.png', false) . '"><img src="' .
-                    $this->getExternalTestFileUrl('/test.jpg', true) . '"></a>',
+                    $this->get_converted_http_link('/test.jpg') . '"></a>',
             ],
         ];
     }
 
+    /**
+     * Convert the HTTP external test file URL to use HTTPS.
+     *
+     * Note: We *must not* use getExternalTestFileUrl with the True option
+     * here, becase it is reasonable to have only one of these set due to
+     * issues with SSL certificates.
+     *
+     * @param   string  $path Path to be rewritten
+     * @return  string
+     */
+    protected function get_converted_http_link($path) {
+        return preg_replace('/^http:/', 'https:', $this->getExternalTestFileUrl($path, false));
+    }
+
     /**
      * Test upgrade_http_links
      * @param string $content Example content that we'll attempt to replace.
@@ -152,7 +166,7 @@ class httpsreplace_test extends \advanced_testcase {
         // Get the http url, since the default test wwwroot is https.
         $wwwrootdomain = 'www.example.com';
         $wwwroothttp = preg_replace('/^https:/', 'http:', $CFG->wwwroot);
-        $testdomain = 'download.moodle.org';
+        $testdomain = $this->get_converted_http_link('');
         return [
             "Test image from an available site so shouldn't be reported" => [
                 "content" => '<img src="' . $this->getExternalTestFileUrl('/test.jpg', false) . '">',
index fc1e3d1..06d7441 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_tool_log_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 8f014cf..f9c8a8b 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_database_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 8880f59..610b61c 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_logstore_standard_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 8d04d45..90c2a12 100644 (file)
@@ -40,6 +40,7 @@ if (empty($classname)) {
     foreach ($records as $record) {
         $instances[] = \core\message\inbound\manager::get_handler($record->classname);
     }
+    $records->close();
 
     echo $OUTPUT->header();
     echo $renderer->messageinbound_handlers_table($instances);
index 6fbca49..1358647 100644 (file)
@@ -35,9 +35,6 @@ function xmldb_tool_monitor_upgrade($oldversion) {
 
     $dbman = $DB->get_manager();
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2016052305) {
 
         // Define field inactivedate to be added to tool_monitor_subscriptions.
index a57b4c2..23874bb 100644 (file)
@@ -78,6 +78,10 @@ class issuer extends persistent {
         $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
         $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2');
 
+        // Use basic authentication.
+        $mform->addElement('checkbox', 'basicauth', get_string('usebasicauth', 'tool_oauth2'));
+        $mform->addHelpButton('basicauth', 'usebasicauth', 'tool_oauth2');
+
         // Login scopes.
         $mform->addElement('text', 'loginscopes', get_string('issuerloginscopes', 'tool_oauth2'));
         $mform->addRule('loginscopes', null, 'required', null, 'client');
index 8c20c6a..b8fe75b 100644 (file)
@@ -95,6 +95,8 @@ $string['systemaccountconnected_help'] = 'System accounts are used to provide ad
 $string['systemaccountconnected'] = 'System account connected';
 $string['systemaccountnotconnected'] = 'System account not connected';
 $string['systemauthstatus'] = 'System account connected';
+$string['usebasicauth'] = 'Authenticate token requests via HTTP headers';
+$string['usebasicauth_help'] = 'Utilize the HTTP Basic authentication scheme when sending client ID and password with a refresh token request. Recommended by the OAuth 2 standard, but may not be available with some issuers.';
 $string['userfieldexternalfield'] = 'External field name';
 $string['userfieldexternalfield_help'] = 'Name of the field provided by the external OAuth system.';
 $string['userfieldinternalfield_help'] = 'Name of the Moodle user field that should be mapped from the external field.';
index 63d3485..2f98bf4 100644 (file)
@@ -234,15 +234,19 @@ function search_spammers($keywords) {
     $keywordlist = implode(', ', $keywords);
     echo $OUTPUT->box(get_string('spamresult', 'tool_spamcleaner').s($keywordlist)).' ...';
 
-    print_user_list(array($spamusers_desc,
-                          $spamusers_blog,
-                          $spamusers_blogsub,
-                          $spamusers_comment,
-                          $spamusers_message,
-                          $spamusers_forumpost,
-                          $spamusers_forumpostsub
-                         ),
-                         $keywords);
+    $recordsets = [
+        $spamusers_desc,
+        $spamusers_blog,
+        $spamusers_blogsub,
+        $spamusers_comment,
+        $spamusers_message,
+        $spamusers_forumpost,
+        $spamusers_forumpostsub
+    ];
+    print_user_list($recordsets, $keywords);
+    foreach ($recordsets as $rs) {
+        $rs->close();
+    }
 }
 
 
index d11332b..496b52d 100644 (file)
@@ -358,7 +358,7 @@ if ($formdata = $mform2->is_cancelled()) {
 
         // add default values for remaining fields
         $formdefaults = array();
-        if ($updatetype != UU_UPDATE_FILEOVERRIDE && $updatetype != UU_UPDATE_NOCHANGES) {
+        if (!$existinguser || ($updatetype != UU_UPDATE_FILEOVERRIDE && $updatetype != UU_UPDATE_NOCHANGES)) {
             foreach ($STD_FIELDS as $field) {
                 if (isset($user->$field)) {
                     continue;
index e217cf1..1ee26f4 100644 (file)
@@ -38,6 +38,31 @@ Feature: Upload users
     And I set the field "groups" to "Section 1 (1)"
     And the "members" select box should contain "Tom Jones"
 
+  @javascript
+  Scenario: Upload users enrolling them on courses and groups applying defaults
+    Given the following "courses" exist:
+      | fullname | shortname | category |
+      | Maths | math102 | 0 |
+    And the following "groups" exist:
+      | name | course | idnumber |
+      | Section 1 | math102 | S1 |
+      | Section 3 | math102 | S3 |
+    And I log in as "admin"
+    And I navigate to "Upload users" node in "Site administration > Users > Accounts"
+    When I upload "lib/tests/fixtures/upload_users.csv" file to "File" filemanager
+    And I press "Upload users"
+    And I set the following fields to these values:
+      | City/town  | Brighton   |
+      | Department | Purchasing |
+    And I press "Upload users"
+    And I press "Continue"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I should see "Tom Jones"
+    And I follow "Tom Jones"
+    And I follow "Edit profile"
+    And the field "City/town" matches value "Brighton"
+    And the field "Department" matches value "Purchasing"
+
   @javascript
   Scenario: Upload users with custom profile fields
     # Create user profile field.
index 65bf4e6..ac0f5f9 100644 (file)
                 $users[$key]->country = $countries[$user->country];
             }
         }
-        if ($sort == "country") {  // Need to resort by full country name, not code
+        if ($sort == "country") {
+            // Need to resort by full country name, not code.
             foreach ($users as $user) {
                 $susers[$user->id] = $user->country;
             }
-            asort($susers);
+            // Sort by country name, according to $dir.
+            if ($dir === 'DESC') {
+                arsort($susers);
+            } else {
+                asort($susers);
+            }
             foreach ($susers as $key => $value) {
                 $nusers[] = $users[$key];
             }
index eb87b75..919f36b 100644 (file)
@@ -134,7 +134,7 @@ class course implements \core_analytics\analysable {
      *
      * Lazy load of course data, students and teachers.
      *
-     * @param int|stdClass $course Course id
+     * @param int|\stdClass $course Course id
      * @return void
      */
     public function __construct($course) {
@@ -152,7 +152,7 @@ class course implements \core_analytics\analysable {
      *
      * Lazy load of course data, students and teachers.
      *
-     * @param int|stdClass $course Course object or course id
+     * @param int|\stdClass $course Course object or course id
      * @return \core_analytics\course
      */
     public static function instance($course) {
@@ -184,7 +184,7 @@ class course implements \core_analytics\analysable {
     /**
      * Loads the analytics course object.
      *
-     * @return null
+     * @return void
      */
     protected function load() {
 
@@ -451,7 +451,7 @@ class course implements \core_analytics\analysable {
     /**
      * Returns the course students.
      *
-     * @return stdClass[]
+     * @return int[]
      */
     public function get_students() {
 
@@ -595,7 +595,7 @@ class course implements \core_analytics\analysable {
      *
      * Keys are ignored.
      *
-     * @param int|float $values Sorted array of values
+     * @param int[]|float[] $values Sorted array of values
      * @return int
      */
     protected function median($values) {
@@ -605,7 +605,7 @@ class course implements \core_analytics\analysable {
             return reset($values);
         }
 
-        $middlevalue = floor(($count - 1) / 2);
+        $middlevalue = (int)floor(($count - 1) / 2);
 
         if ($count % 2) {
             // Odd number, middle is the median.
index 4b0bcac..cdf787e 100644 (file)
@@ -360,6 +360,7 @@ class manager {
             }
             $existingcalculations[$calculation->indicator][$calculation->sampleid] = $calculation->value;
         }
+        $calculations->close();
         return $existingcalculations;
     }
 
index d881645..0bee650 100644 (file)
@@ -1013,6 +1013,10 @@ class model {
             if (!$this->is_static()) {
                 $this->model->trained = 0;
             }
+        } else if (empty($this->model->timesplitting)) {
+            // A valid timesplitting method needs to be supplied before a model can be enabled.
+            throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
+
         }
 
         // Purge pages with insights as this may change things.
index 3856c5f..760e667 100644 (file)
@@ -138,6 +138,7 @@ class dataset_manager_testcase extends advanced_testcase {
         // Training and prediction files are not mixed up.
         $trainingfile1 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
             '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
+        $this->waitForSecond();
         $trainingfile2 = \core_analytics\dataset_manager::merge_datasets(array($file), $fakemodelid,
             '\core\analytics\time_splitting\quarters', \core_analytics\dataset_manager::LABELLED_FILEAREA, false);
 
index 2685b64..d5d0153 100644 (file)
@@ -226,7 +226,7 @@ class analytics_model_testcase extends advanced_testcase {
         $this->model->mark_as_trained();
         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 
-        $this->model->enable();
+        $this->model->enable('\core\analytics\time_splitting\deciles');
         $this->assertEquals($originaluniqueid, $this->model->get_unique_id());
 
         // Wait 1 sec so the timestamp changes.
index 167c76b..f3999b3 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_cas_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index e2daf57..3b21d60 100644 (file)
@@ -45,7 +45,7 @@ $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an O
 $string['auth_dbname_key'] = 'DB name';
 $string['auth_dbpass'] = 'Password matching the above username';
 $string['auth_dbpass_key'] = 'Password';
-$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using. MD5 hashing is useful for connecting to other common web applications like PostNuke.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
+$string['auth_dbpasstype'] = '<p>Specify the format that the password field is using.</p> <p>Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you <i>must</i> provide a populated email address field in the external database, and you must execute both admin/cron.php and auth/db/cli/sync_users.php regularly. Moodle will send an email to new users with a temporary password.</p>';
 $string['auth_dbpasstype_key'] = 'Password format';
 $string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}';
 $string['auth_dbrevivedusererror'] = 'Error reviving user {$a}';
index 6811fd7..02f1323 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_ldap_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index fdb9541..843e1b0 100644 (file)
@@ -41,7 +41,7 @@ $string['auth_ldapdescription'] = 'This method provides authentication against a
                                   entry in its database. This module can read user attributes from LDAP and prefill
                                   wanted fields in Moodle.  For following logins only the username and
                                   password are checked.';
-$string['auth_ldap_expiration_desc'] = 'Select No to disable expired password checking or LDAP to read passwordexpiration time directly from LDAP';
+$string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiration time directly from the LDAP server';
 $string['auth_ldap_expiration_key'] = 'Expiration';
 $string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiration warning is issued.';
 $string['auth_ldap_expiration_warning_key'] = 'Expiration warning';
index af1ddf7..43fb8eb 100644 (file)
@@ -185,12 +185,24 @@ if ($ADMIN->fulltree) {
                 new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), ''));
 
         // Password Expiration.
+
+        // Create the description lang_string object.
+        $strno = get_string('no');
+        $strldapserver = get_string('pluginname', 'auth_ldap');
+        $langobject = new stdClass();
+        $langobject->no = $strno;
+        $langobject->ldapserver = $strldapserver;
+        $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject);
+
+        // Now create the options.
         $expiration = array();
-        $expiration['0'] = 'no';
-        $expiration['1'] = 'LDAP';
+        $expiration['0'] = $strno;
+        $expiration['1'] = $strldapserver;
+
+        // Add the setting.
         $settings->add(new admin_setting_configselect('auth_ldap/expiration',
                 new lang_string('auth_ldap_expiration_key', 'auth_ldap'),
-                new lang_string('auth_ldap_expiration_desc', 'auth_ldap'), 0 , $expiration));
+                $description, 0 , $expiration));
 
         // Password Expiration warning.
         $settings->add(new admin_setting_configtext('auth_ldap/expiration_warning',
index 50a5266..03dbdb2 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 925124e..9db6da9 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_auth_mnet_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
     if ($oldversion < 2017020700) {
index b53ffa6..f17a6ed 100644 (file)
@@ -981,8 +981,8 @@ class restore_process_course_modules_availability extends restore_execution_step
                 $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
                         array('id' => $thingid));
             }
+            $rs->close();
         }
-        $rs->close();
     }
 }
 
index 4988d52..c8717fa 100644 (file)
@@ -65,6 +65,7 @@ abstract class base_setting {
 
     protected $name;  // name of the setting
     protected $value; // value of the setting
+    protected $unlockedvalue; // Value to set after the setting is unlocked.
     protected $vtype; // type of value (setting_base::IS_BOOLEAN/setting_base::IS_INTEGER...)
 
     protected $visibility; // visibility of the setting (setting_base::VISIBLE/setting_base::HIDDEN)
@@ -118,6 +119,7 @@ abstract class base_setting {
         $this->value       = $value;
         $this->visibility  = $visibility;
         $this->status      = $status;
+        $this->unlockedvalue = $this->value;
 
         // Generate a default ui
         $this->uisetting = new base_setting_ui($this);
@@ -225,6 +227,11 @@ abstract class base_setting {
         $this->status = $status;
         if ($status !== $oldstatus) { // Status has changed, let's inform dependencies
             $this->inform_dependencies(self::CHANGED_STATUS, $oldstatus);
+
+            if ($status == base_setting::NOT_LOCKED) {
+                // When setting gets unlocked set it to the original value.
+                $this->set_value($this->unlockedvalue);
+            }
         }
     }
 
index 425a328..079537f 100644 (file)
@@ -153,7 +153,7 @@ abstract class setting_dependency {
      */
     abstract public function get_moodleform_properties();
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     abstract public function is_locked();
@@ -185,7 +185,7 @@ class setting_dependency_disabledif_equals extends setting_dependency {
         $this->value = ($value)?(string)$value:0;
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -193,8 +193,8 @@ class setting_dependency_disabledif_equals extends setting_dependency {
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || $this->setting->get_value() == $this->value) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
     /**
      * Processes a value change in the primary setting
@@ -343,7 +343,7 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
         $this->value = $value;
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -351,8 +351,8 @@ class setting_dependency_disabledif_equals2 extends setting_dependency {
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || in_array($this->setting->get_value(), $this->value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
     /**
      * Processes a value change in the primary setting
@@ -537,7 +537,7 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
     }
 
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -545,8 +545,8 @@ class setting_dependency_disabledif_not_empty extends setting_dependency_disable
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || !empty($value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
 }
 
@@ -601,7 +601,7 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         return ($prevalue != $this->dependentsetting->get_value());
     }
     /**
-     * Returns true if the dependent setting is locked.
+     * Returns true if the dependent setting is locked by this setting_dependency.
      * @return bool
      */
     public function is_locked() {
@@ -609,7 +609,7 @@ class setting_dependency_disabledif_empty extends setting_dependency_disabledif_
         if ($this->setting->get_status() !== base_setting::NOT_LOCKED || empty($value)) {
             return true;
         }
-        // Else return based upon the dependent settings status
-        return ($this->dependentsetting->get_status() !== base_setting::NOT_LOCKED);
+        // Else the dependent setting is not locked by this setting_dependency.
+        return false;
     }
 }
index d928743..7109162 100644 (file)
@@ -307,10 +307,12 @@ abstract class backup_setting_ui extends base_setting_ui {
      * 2. The setting is locked but only by settings that are of the same level (same page)
      *
      * Condition 2 is really why we have this function
-     *
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if ($this->setting->get_status() === backup_setting::NOT_LOCKED) {
             // Its not locked so its chanegable.
             return true;
@@ -319,6 +321,9 @@ abstract class backup_setting_ui extends base_setting_ui {
             return false;
         } else if ($this->setting->has_dependencies_on_settings()) {
             foreach ($this->setting->get_settings_depended_on() as $dependency) {
+                if ($level && $dependency->get_setting()->get_level() >= $level) {
+                    continue;
+                }
                 if ($dependency->is_locked() && $dependency->get_setting()->get_level() !== $this->setting->get_level()) {
                     // Its not changeable because one or more dependancies arn't changeable.
                     return false;
@@ -458,13 +463,16 @@ class backup_setting_ui_checkbox extends backup_setting_ui {
 
     /**
      * Returns true if the setting is changeable
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if ($this->changeable === false) {
             return false;
         } else {
-            return parent::is_changeable();
+            return parent::is_changeable($level);
         }
     }
 
@@ -639,13 +647,16 @@ class backup_setting_ui_select extends backup_setting_ui {
     /**
      * Returns true if the setting is changeable, false otherwise
      *
+     * @param int $level Optional, if provided only depedency_settings below or equal to this level are considered,
+     *          when checking if the ui_setting is changeable. Although dependencies might cause a lock on this setting,
+     *          they could be changeable in the same view.
      * @return bool
      */
-    public function is_changeable() {
+    public function is_changeable($level = null) {
         if (count($this->values) == 1) {
             return false;
         } else {
-            return parent::is_changeable();
+            return parent::is_changeable($level);
         }
     }
 
index 4f8928e..34820e4 100644 (file)
@@ -183,11 +183,22 @@ abstract class base_moodleform extends moodleform {
     public function add_settings(array $settingstasks) {
         global $OUTPUT;
 
+        // Determine highest setting level, which is displayed in this stage. This is relevant for considering only
+        // locks of dependency settings for parent settings, which are not displayed in this stage.
+        $highestlevel = backup_setting::ACTIVITY_LEVEL;
+        foreach ($settingstasks as $st) {
+            list($setting, $task) = $st;
+            if ($setting->get_level() < $highestlevel) {
+                $highestlevel = $setting->get_level();
+            }
+        }
+
         $defaults = array();
         foreach ($settingstasks as $st) {
             list($setting, $task) = $st;
             // If the setting cant be changed or isn't visible then add it as a fixed setting.
-            if (!$setting->get_ui()->is_changeable() || $setting->get_visibility() != backup_setting::VISIBLE) {
+            if (!$setting->get_ui()->is_changeable($highestlevel) ||
+                $setting->get_visibility() != backup_setting::VISIBLE) {
                 $this->add_fixed_setting($setting, $task);
                 continue;
             }
diff --git a/backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature b/backup/util/ui/tests/behat/restore_moodle2_courses_settings.feature
new file mode 100644 (file)
index 0000000..b851957
--- /dev/null
@@ -0,0 +1,125 @@
+@core @core_backup
+Feature: Restore Moodle 2 course backups with different user data settings
+  In order to decide upon including user data during backup and restore of courses
+  As a teacher and an admin
+  I need to be able to set and override backup and restore settings
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | student1 | Student | 1 | student1@example.com |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1 | 0 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And the following "activities" exist:
+      | activity | name               | intro | course | idnumber |
+      | data     | Test database name | n     | C1     | data1    |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I add a "Text input" field to "Test database name" database and I fill the form with:
+      | Field name | Test field name |
+      | Field description | Test field description |
+    And I follow "Templates"
+    And I wait until the page is ready
+    And I log out
+    And I log in as "student1"
+    And I am on "Course 1" course homepage
+    And I add an entry to "Test database name" database with:
+      | Test field name | Student entry |
+    And I press "Save and view"
+    And I log out
+    And I log in as "admin"
+    And I backup "Course 1" course using this options:
+      | Initial |  Include enrolled users | 1 |
+      | Confirmation | Filename | test_backup.mbz |
+
+  @javascript
+  Scenario: Restore a backup with user data
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 1 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for data activity
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for section and data activity
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 0 |
+      | Schema | - | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup without user data for section
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | - | 1 |
+      | Schema | User data | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with local config for including users set to 0
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with site config for including users set to 0
+    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    And I set the field "s_restore_restore_general_users" to ""
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I navigate to "Restore" node in "Course administration"
+    # "User data" marks the user data field for the section
+    # "-" marks the user data field for the data activity
+    And I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 1 |
+      | Schema | User data | 1 |
+      | Schema | - | 1 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should see "Student entry"
+
+  @javascript
+  Scenario: Restore a backup with user data with local and site config config for including users set to 0
+    Given I navigate to "General restore defaults" node in "Site administration > Courses > Backups"
+    And I set the field "s_restore_restore_general_users" to ""
+    And I press "Save changes"
+    And I am on "Course 1" course homepage
+    And I navigate to "Restore" node in "Course administration"
+    When I restore "test_backup.mbz" backup into a new course using this options:
+      | Settings |  Include enrolled users | 0 |
+    Then I should see "Test database name"
+    When I follow "Test database name"
+    Then I should not see "Student entry"
\ No newline at end of file
index d21b96e..560bb88 100644 (file)
@@ -42,6 +42,7 @@ $badge = new issued_badge($id);
 if (!empty($badge->recipient->id)) {
     if ($bake && ($badge->recipient->id == $USER->id)) {
         $name = str_replace(' ', '_', $badge->badgeclass['name']) . '.png';
+        $name = clean_param($name, PARAM_FILE);
         $filehash = badges_bake($id, $badge->badgeid, $USER->id, true);
         $fs = get_file_storage();
         $file = $fs->get_file_by_hash($filehash);
index 25081bf..e5b2de0 100644 (file)
@@ -159,7 +159,7 @@ class core_badges_external extends external_api {
                     new external_single_structure(
                         array(
                             'id' => new external_value(PARAM_INT, 'Badge id.', VALUE_OPTIONAL),
-                            'name' => new external_value(PARAM_FILE, 'Badge name.'),
+                            'name' => new external_value(PARAM_TEXT, 'Badge name.'),
                             'description' => new external_value(PARAM_NOTAGS, 'Badge description.'),
                             'badgeurl' => new external_value(PARAM_URL, 'Badge URL.'),
                             'timecreated' => new external_value(PARAM_INT, 'Time created.', VALUE_OPTIONAL),
index 34cf105..31be94e 100644 (file)
@@ -106,6 +106,37 @@ class core_badges_observer {
         }
     }
 
+    /**
+     * Triggered when 'badge_awarded' event happens.
+     *
+     * @param \core\event\badge_awarded $event event generated when a badge is awarded.
+     */
+    public static function badge_criteria_review(\core\event\badge_awarded $event) {
+        global $DB, $CFG;
+
+        if (!empty($CFG->enablebadges)) {
+            require_once($CFG->dirroot.'/lib/badgeslib.php');
+            $userid = $event->relateduserid;
+
+            if ($rs = $DB->get_records('badge_criteria', array('criteriatype' => BADGE_CRITERIA_TYPE_BADGE))) {
+                foreach ($rs as $r) {
+                    $badge = new badge($r->badgeid);
+                    if (!$badge->is_active() || $badge->is_issued($userid)) {
+                        continue;
+                    }
+
+                    if ($badge->criteria[BADGE_CRITERIA_TYPE_BADGE]->review($userid)) {
+                        $badge->criteria[BADGE_CRITERIA_TYPE_BADGE]->mark_complete($userid);
+
+                        if ($badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->review($userid)) {
+                            $badge->criteria[BADGE_CRITERIA_TYPE_OVERALL]->mark_complete($userid);
+                            $badge->issue($userid);
+                        }
+                    }
+                }
+            }
+        }
+    }
     /**
      * Triggered when 'user_updated' event happens.
      *
index ee13151..5427783 100644 (file)
@@ -68,6 +68,12 @@ define('BADGE_CRITERIA_TYPE_COURSESET', 5);
  */
 define('BADGE_CRITERIA_TYPE_PROFILE', 6);
 
+/*
+ * Badge completion criteria type
+ * Criteria type constant, primarily for storing criteria type in the database.
+ */
+define('BADGE_CRITERIA_TYPE_BADGE', 7);
+
 /*
  * Criteria type constant to class name mapping
  */
@@ -79,7 +85,8 @@ $BADGE_CRITERIA_TYPES = array(
     BADGE_CRITERIA_TYPE_SOCIAL    => 'social',
     BADGE_CRITERIA_TYPE_COURSE    => 'course',
     BADGE_CRITERIA_TYPE_COURSESET => 'courseset',
-    BADGE_CRITERIA_TYPE_PROFILE   => 'profile'
+    BADGE_CRITERIA_TYPE_PROFILE   => 'profile',
+    BADGE_CRITERIA_TYPE_BADGE     => 'badge',
 );
 
 /**
diff --git a/badges/criteria/award_criteria_badge.php b/badges/criteria/award_criteria_badge.php
new file mode 100644 (file)
index 0000000..d9bd795
--- /dev/null
@@ -0,0 +1,271 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the badge earned badge award criteria type class
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2017 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Badge award criteria -- award on badge completion
+ *
+ * @package    core
+ * @subpackage badges
+ * @copyright  2017 Stephen Bourget
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class award_criteria_badge extends award_criteria {
+
+    /* @var int Criteria [BADGE_CRITERIA_TYPE_BADGE] */
+    public $criteriatype = BADGE_CRITERIA_TYPE_BADGE;
+
+    public $required_param = 'badge';
+    public $optional_params = array();
+
+    /**
+     * Get criteria details for displaying to users
+     * @param string $short Print short version of criteria
+     * @return string
+     */
+    public function get_details($short = '') {
+        global $DB, $OUTPUT;
+        $output = array();
+        foreach ($this->params as $p) {
+            $badgename = $DB->get_field('badge', 'name', array('id' => $p['badge']));
+            if (!$badgename) {
+                $str = $OUTPUT->error_text(get_string('error:nosuchbadge', 'badges'));
+            } else {
+                $str = html_writer::tag('b', '"' . $badgename . '"');
+            }
+            $output[] = $str;
+        }
+
+        if ($short) {
+            return implode(', ', $output);
+        } else {
+            return html_writer::alist($output, array(), 'ul');
+        }
+    }
+
+    /**
+     * Add appropriate new criteria options to the form
+     * @param object $mform moodle form
+     */
+    public function get_options(&$mform) {
+        global $DB;
+        $none = false;
+        $availablebadges = null;
+
+        $mform->addElement('header', 'first_header', $this->get_title());
+        $mform->addHelpButton('first_header', 'criteria_' . $this->criteriatype, 'badges');
+
+        // Determine if this badge is a course badge or a site badge.
+        $thisbadge = $DB->get_record('badge', array('id' => $this->badgeid));
+
+        if ($thisbadge->type == BADGE_TYPE_SITE) {
+            // Only list site badges that are enabled.
+            $select = " type = :site AND (status = :status1 OR status = :status2)";
+            $params = array('site' => BADGE_TYPE_SITE,
+                            'status1' => BADGE_STATUS_ACTIVE,
+                            'status2' => BADGE_STATUS_ACTIVE_LOCKED);
+            $availablebadges = $DB->get_records_select_menu('badge', $select, $params, 'name ASC', 'id, name');
+
+        } else if ($thisbadge->type == BADGE_TYPE_COURSE) {
+            // List both site badges and course badges belonging to this course.
+            $select = " (type = :site OR (type = :course AND courseid = :courseid)) AND (status = :status1 OR status = :status2)";
+            $params = array('site' => BADGE_TYPE_SITE,
+                            'course' => BADGE_TYPE_COURSE,
+                            'courseid' => $thisbadge->courseid,
+                            'status1' => BADGE_STATUS_ACTIVE,
+                            'status2' => BADGE_STATUS_ACTIVE_LOCKED);
+            $availablebadges = $DB->get_records_select_menu('badge', $select, $params, 'name ASC', 'id, name');
+        }
+        if (!empty($availablebadges)) {
+            $select = array();
+            $selected = array();
+            foreach ($availablebadges as $bid => $badgename) {
+                if ($bid != $this->badgeid) {
+                    // Do not let it use itself as criteria.
+                    $select[$bid] = format_string($badgename, true);
+                }
+            }
+
+            if ($this->id !== 0) {
+                $selected = array_keys($this->params);
+            }
+            $settings = array('multiple' => 'multiple', 'size' => 20, 'class' => 'selectbadge');
+            $mform->addElement('select', 'badge_badges', get_string('addbadge', 'badges'), $select, $settings);
+            $mform->addRule('badge_badges', get_string('requiredbadge', 'badges'), 'required');
+            $mform->addHelpButton('badge_badges', 'addbadge', 'badges');
+
+            if ($this->id !== 0) {
+                $mform->setDefault('badge_badges', $selected);
+            }
+        } else {
+            $mform->addElement('static', 'nobadges', '', get_string('error:nobadges', 'badges'));
+            $none = true;
+        }
+
+        // Add aggregation.
+        if (!$none) {
+            $mform->addElement('header', 'aggregation', get_string('method', 'badges'));
+            $agg = array();
+            $agg[] =& $mform->createElement('radio', 'agg', '', get_string('allmethodbadges', 'badges'), 1);
+            $agg[] =& $mform->createElement('radio', 'agg', '', get_string('anymethodbadges', 'badges'), 2);
+            $mform->addGroup($agg, 'methodgr', '', array('<br/>'), false);
+            if ($this->id !== 0) {
+                $mform->setDefault('agg', $this->method);
+            } else {
+                $mform->setDefault('agg', BADGE_CRITERIA_AGGREGATION_ANY);
+            }
+        }
+
+        return array($none, get_string('noparamstoadd', 'badges'));
+    }
+
+    /**
+     * Save criteria records
+     *
+     * @param array $params Values from the form or any other array.
+     */
+    public function save($params = array()) {
+        $badges = $params['badge_badges'];
+        unset($params['badge_badges']);
+        foreach ($badges as $badgeid) {
+            $params["badge_{$badgeid}"] = $badgeid;
+        }
+
+        parent::save($params);
+    }
+
+    /**
+     * Review this criteria and decide if it has been completed
+     *
+     * @param int $userid User whose criteria completion needs to be reviewed.
+     * @param bool $filtered An additional parameter indicating that user list
+     *        has been reduced and some expensive checks can be skipped.
+     *
+     * @return bool Whether criteria is complete.
+     */
+    public function review($userid, $filtered = false) {
+
+        global $DB;
+        $overall = false;
+
+        foreach ($this->params as $param) {
+            $badge = $DB->get_record('badge', array('id' => $param['badge']));
+            // See if the user has earned this badge.
+            $awarded = $DB->get_record('badge_issued', array('badgeid' => $param['badge'], 'userid' => $userid));
+
+            // Extra check in case a badge was deleted while this badge is still active.
+            if (!$badge) {
+                if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) {
+                    return false;
+                } else {
+                    continue;
+                }
+            }
+
+            if ($this->method == BADGE_CRITERIA_AGGREGATION_ALL) {
+
+                if ($awarded) {
+                    $overall = true;
+                    continue;
+                } else {
+                    return false;
+                }
+            } else if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
+                if ($awarded) {
+                    return true;
+                } else {
+                    $overall = false;
+                    continue;
+                }
+            }
+        }
+
+        return $overall;
+    }
+
+    /**
+     * Checks criteria for any major problems.
+     *
+     * @return array A list containing status and an error message (if any).
+     */
+    public function validate() {
+        global $DB;
+        $params = array_keys($this->params);
+        $method = ($this->method == BADGE_CRITERIA_AGGREGATION_ALL);
+        $singleparam = (count($params) == 1);
+
+        foreach ($params as $param) {
+            // Perform check if there only one parameter with any type of aggregation,
+            // Or there are more than one parameter with aggregation ALL.
+
+            if (($singleparam || $method) && !$DB->record_exists('badge', array('id' => $param))) {
+                return array(false, get_string('error:invalidparambadge', 'badges'));
+            }
+        }
+
+        return array(true, '');
+    }
+
+    /**
+     * Returns array with sql code and parameters returning all ids
+     * of users who meet this particular criterion.
+     *
+     * @return array list($join, $where, $params)
+     */
+    public function get_completed_criteria_sql() {
+        $join = '';
+        $where = '';
+        $params = array();
+
+        if ($this->method == BADGE_CRITERIA_AGGREGATION_ANY) {
+            // User has received ANY of the required badges.
+            $join = " LEFT JOIN {badge_issued} bi2 ON bi2.userid = u.id";
+            $where = "AND (";
+            $i = 0;
+            foreach ($this->params as $param) {
+                if ($i == 0) {
+                    $where .= ' bi2.badgeid = :badgeid'.$i;
+                } else {
+                    $where .= ' OR bi2.badgeid = :badgeid'.$i;
+                }
+                $params['badgeid'.$i] = $param['badge'];
+                $i++;
+            }
+            $where .= ") ";
+            return array($join, $where, $params);
+        } else {
+            // User has received ALL of the required badges.
+            $join = " LEFT JOIN {badge_issued} bi2 ON bi2.userid = u.id";
+            $i = 0;
+            foreach ($this->params as $param) {
+                $i++;
+                $where = ' AND bi2.badgeid = :badgeid'.$i;
+                $params['badgeid'.$i] = $param['badge'];
+            }
+            return array($join, $where, $params);
+        }
+    }
+}
index cebc483..1c2a40a 100644 (file)
@@ -48,8 +48,8 @@ class edit_details_form extends moodleform {
 
         $mform->addElement('header', 'badgedetails', get_string('badgedetails', 'badges'));
         $mform->addElement('text', 'name', get_string('name'), array('size' => '70'));
-        // Using PARAM_FILE to avoid problems later when downloading badge files.
-        $mform->setType('name', PARAM_FILE);
+        // When downloading badge, it will be necessary to clean the name as PARAM_FILE.
+        $mform->setType('name', PARAM_TEXT);
         $mform->addRule('name', null, 'required');
         $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client');
 
index 8437ad4..97c4aee 100644 (file)
@@ -72,6 +72,7 @@ if ($hide) {
     require_sesskey();
     $badge = new badge($download);
     $name = str_replace(' ', '_', $badge->name) . '.png';
+    $name = clean_param($name, PARAM_FILE);
     $filehash = badges_bake($hash, $download, $USER->id, true);
     $fs = get_file_storage();
     $file = $fs->get_file_by_hash($filehash);
index 0f1f6ca..23ae6dc 100644 (file)
@@ -48,7 +48,7 @@ class core_badges_badgeslib_testcase extends advanced_testcase {
 
         $fordb = new stdClass();
         $fordb->id = null;
-        $fordb->name = "Test badge";
+        $fordb->name = "Test badge with 'apostrophe' and other friends (<>&@#)";
         $fordb->description = "Testing badges";
         $fordb->timecreated = time();
         $fordb->timemodified = time();
index 8c09573..d228bbc 100644 (file)
@@ -31,14 +31,14 @@ Feature: Add badges to the system
   Scenario: Add a badge
     Given I navigate to "Add a new badge" node in "Site administration > Badges"
     And I set the following fields to these values:
-      | Name | Test Badge |
+      | Name | Test badge with 'apostrophe' and other friends (<>&@#) |
       | Description | Test badge description |
       | issuername | Test Badge Site |
       | issuercontact | testuser@example.com |
     And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
     When I press "Create badge"
     Then I should see "Edit details"
-    And I should see "Test Badge"
+    And I should see "Test badge with 'apostrophe' and other friends (&@#)"
     And I should not see "Create badge"
     And I follow "Manage badges"
     And I should see "Number of badges available: 1"
index 13c040a..cd64b32 100644 (file)
@@ -4,6 +4,71 @@ Feature: Award badges
   As an admin
   I need to add criteria to badges in the system
 
+  @javascript
+  Scenario: Award badge on other badges as criteria
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    # Create course badge 1.
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge 1 |
+      | Description | Course badge 1 description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    And I set the field "type" to "Manual issue by role"
+    And I expand all fieldsets
+    # Set to ANY of the roles awards badge.
+    And I set the field "Teacher" to "1"
+    And I set the field "Any of the selected roles awards the badge" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    # Badge #2
+    And I navigate to "Add a new badge" node in "Course administration > Badges"
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge 2 |
+      | Description | Course badge 2 description |
+      | issuername | Tester of course badge |
+    And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
+    And I press "Create badge"
+    # Set "course badge 1" as criteria
+    And I set the field "type" to "Awarded badges"
+    And I set the field "id_badge_badges" to "Course Badge 1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I follow "Manage badges"
+    And I follow "Course Badge 1"
+    And I follow "Recipients (0)"
+    And I press "Award badge"
+    # Award course badge 1 to student 1.
+    And I set the field "potentialrecipients[]" to "Student 1 (student1@example.com)"
+    When I press "Award badge"
+    And I follow "Course Badge 1"
+    And I follow "Recipients (1)"
+    Then I should see "Recipients (1)"
+    And I log out
+    # Student 1 should have both badges.
+    And I log in as "student1"
+    And I follow "Profile" in the user menu
+    When I click on "Course 1" "link" in the "region-main" "region"
+    Then I should see "Course Badge 1"
+    And I should see "Course Badge 2"
+
   @javascript
   Scenario: Award profile badge
     Given I log in as "admin"
index 980e5f9..bb9b3cf 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_badges_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 397cb90..4403a77 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_calendar_month_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index e7aa7fe..dbe19a5 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_calendar_upcoming_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index f5f0ae3..d01face 100644 (file)
@@ -46,9 +46,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_community_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index c74148a..f0837fc 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_completionstatus_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 9186a40..763a538 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_course_summary_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 5761d4c..d3d96b2 100644 (file)
@@ -74,12 +74,19 @@ class block_globalsearch extends block_base {
         // Input.
         $this->content->text .= html_writer::tag('label', get_string('search', 'search'),
             array('for' => 'searchform_search', 'class' => 'accesshide'));
-        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'type' => 'text', 'size' => '15');
+        $inputoptions = array('id' => 'searchform_search', 'name' => 'q', 'class' => 'form-control',
+            'type' => 'text', 'size' => '15');
         $this->content->text .= html_writer::empty_tag('input', $inputoptions);
 
+        // Context id.
+        if ($this->page->context && $this->page->context->contextlevel !== CONTEXT_SYSTEM) {
+            $this->content->text .= html_writer::empty_tag('input', ['type' => 'hidden',
+                    'name' => 'context', 'value' => $this->page->context->id]);
+        }
+
         // Search button.
         $this->content->text .= html_writer::tag('button', get_string('search', 'search'),
-            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch'));
+            array('id' => 'searchform_button', 'type' => 'submit', 'title' => 'globalsearch', 'class' => 'btn btn-secondary'));
         $this->content->text .= html_writer::end_tag('fieldset');
         $this->content->text .= html_writer::end_tag('form');
         $this->content->text .= html_writer::end_tag('div');
index e386f20..5ad7f22 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_html_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index e5cee22..995f6d4 100644 (file)
@@ -35,6 +35,7 @@ use core_competency\url;
 use renderable;
 use renderer_base;
 use templatable;
+use required_capability_exception;
 
 /**
  * Summary renderable class.
@@ -68,7 +69,11 @@ class summary implements renderable, templatable {
         $this->user = $user;
 
         // Get the plans.
-        $this->plans = api::list_user_plans($this->user->id);
+        try {
+            $this->plans = api::list_user_plans($this->user->id);
+        } catch (required_capability_exception $e) {
+            $this->plans = [];
+        }
 
         // Get the competencies to review.
         $this->compstoreview = api::list_user_competencies_to_review(0, 3);
index 04dc110..07b4381 100644 (file)
@@ -55,9 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_navigation_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 0bf77eb..18ee857 100644 (file)
@@ -45,9 +45,6 @@
 function xmldb_block_quiz_results_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 4a9f5fb..d9bf407 100644 (file)
@@ -47,9 +47,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_recent_activity_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 518fffa..d968dda 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_rss_client_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index f0d5534..0200032 100644 (file)
@@ -49,9 +49,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_section_links_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 1f0f0a7..7e5e56a 100644 (file)
@@ -48,9 +48,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_selfcompletion_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index ba37266..2a9dceb 100644 (file)
@@ -55,9 +55,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_block_settings_upgrade($oldversion, $block) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 12b910e..b735d36 100644 (file)
@@ -274,13 +274,12 @@ class month_exporter extends exporter {
 
         // Calculate which day number is the first, and last day of the week.
         $firstdayofweek = $this->firstdayofweek;
-        $lastdayofweek = ($firstdayofweek + $daysinweek - 1) % $daysinweek;
 
         // The first week is special as it may have padding at the beginning.
         $day = reset($alldays);
         $firstdayno = $day['wday'];
 
-        $prepadding = ($firstdayno + $daysinweek - 1) % $daysinweek;
+        $prepadding = ($firstdayno + $daysinweek - $firstdayofweek) % $daysinweek;
         $daysinfirstweek = $daysinweek - $prepadding;
         $days = array_slice($alldays, 0, $daysinfirstweek);
         $week = new week_exporter($this->calendar, $days, $prepadding, ($daysinweek - count($days) - $prepadding), $this->related);
index 2e33869..80ef5ed 100644 (file)
@@ -119,6 +119,7 @@ class event_mapper implements event_mapper_interface {
             'description'      => $event->get_description()->get_value(),
             'format'           => $event->get_description()->get_format(),
             'courseid'         => $event->get_course() ? $event->get_course()->get('id') : null,
+            'categoryid'       => $event->get_category() ? $event->get_category()->get('id') : null,
             'groupid'          => $event->get_group() ? $event->get_group()->get('id') : null,
             'userid'           => $event->get_user() ? $event->get_user()->get('id') : null,
             'repeatid'         => $event->get_repeats()->get_id(),
index 213930f..049f13d 100644 (file)
@@ -127,23 +127,19 @@ class core_calendar_external extends external_api {
                                     'eventids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'event ids')
                                             , 'List of event ids',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'courseids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'course ids')
                                             , 'List of course ids for which events will be returned',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'groupids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'group ids')
                                             , 'List of group ids for which events should be returned',
-                                            VALUE_DEFAULT, array(), NULL_ALLOWED
-                                                ),
+                                            VALUE_DEFAULT, array()),
                                     'categoryids' => new external_multiple_structure(
                                             new external_value(PARAM_INT, 'Category ids'),
                                             'List of category ids for which events will be returned',
-                                            VALUE_DEFAULT, array()
-                                ),
+                                            VALUE_DEFAULT, array()),
                             ), 'Event details', VALUE_DEFAULT, array()),
                     'options' => new external_single_structure(
                             array(
@@ -226,25 +222,36 @@ class core_calendar_external extends external_api {
         }
 
         $categories = array();
-        if (empty($params['events']['categoryids']) && !empty($courses)) {
-            list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
-            $wheresql = "id $wheresql";
-            $courseswithcategory = $DB->get_records_select('course', $wheresql, $sqlparams);
+        if ($hassystemcap || !empty($courses)) {
 
-            // Grab the list of course categories for the requested course list.
             $coursecategories = array();
-            foreach ($courseswithcategory as $course) {
-                if (empty($course->visible)) {
-                    if (!has_capability('moodle/course:viewhidden', context_course::instance($course->id))) {
-                        continue;
+            if (!empty($courses)) {
+                list($wheresql, $sqlparams) = $DB->get_in_or_equal($courses);
+                $wheresql = "id $wheresql";
+                $courseswithcategory = $DB->get_records_select('course', $wheresql, $sqlparams);
+
+                // Grab the list of course categories for the requested course list.
+                foreach ($courseswithcategory as $course) {
+                    if (empty($course->visible)) {
+                        if (!has_capability('moodle/course:viewhidden', context_course::instance($course->id))) {
+                            continue;
+                        }
                     }
+                    $category = \coursecat::get($course->category);
+                    // Fetch parent categories.
+                    $coursecategories = array_merge($coursecategories, [$category->id], $category->get_parents());
                 }
-                $category = \coursecat::get($course->category);
-                $coursecategories[] = $category;
             }
 
             foreach (\coursecat::get_all() as $category) {
-                if (has_capability('moodle/category:manage', $category->get_context(), $USER, false)) {
+                // Skip categories not requested.
+                if (!empty($params['events']['categoryids'])) {
+                    if (!in_array($category->id, $params['events']['categoryids'])) {
+                        continue;
+                    }
+                }
+
+                if (has_capability('moodle/category:manage', $category->get_context())) {
                     // If a user can manage a category, then they can see all child categories. as well as all parent categories.
                     $categories[] = $category->id;
 
@@ -254,29 +261,14 @@ class core_calendar_external extends external_api {
                         }
                     }
                     $categories = array_merge($categories, $category->get_parents());
-                } else if (isset($coursecategories[$category->id])) {
+                } else if (in_array($category->id, $coursecategories)) {
+
                     // The user has access to a course in this category.
                     // Fetch all of the parents too.
                     $categories = array_merge($categories, [$category->id], $category->get_parents());
                     $categories[] = $category->id;
                 }
             }
-        } else {
-            // Build the category list.
-            // This includes the current category.
-            foreach ($params['events']['categoryids'] as $categoryid) {
-                $category = \coursecat::get($categoryid);
-                $categories = [$category->id];
-                // All of its descendants.
-                foreach (\coursecat::get_all() as $cat) {
-                    if (array_search($categoryid, $cat->get_parents()) !== false) {
-                        $categories[] = $cat->id;
-                    }
-                }
-
-                // And all of its parents.
-                $categories = array_merge($categories, $category->get_parents());
-            }
         }
 
         $funcparam['categories'] = array_unique($categories);
index 4790d70..45c1785 100644 (file)
@@ -1056,7 +1056,7 @@ class calendar_information {
             }
 
             $courses = [$course->id => $course];
-            $category = (\coursecat::get($course->category))->get_db_record();
+            $category = (\coursecat::get($course->category, MUST_EXIST, true))->get_db_record();
         } else if (!empty($categoryid)) {
             $course = get_site();
             $courses = calendar_get_default_courses();
@@ -1147,7 +1147,7 @@ class calendar_information {
             // A specific course was requested.
             // Fetch the category that this course is in, along with all parents.
             // Do not include child categories of this category, as the user many not have enrolments in those siblings or children.
-            $category = \coursecat::get($course->category);
+            $category = \coursecat::get($course->category, MUST_EXIST, true);
             $this->categoryid = $category->id;
 
             $this->categories = $category->get_parents();
@@ -2538,7 +2538,7 @@ function calendar_get_allowed_types(&$allowed, $course = null, $groups = null, $
 
     if (!empty($course)) {
         if (!is_object($course)) {
-            $course = $DB->get_record('course', array('id' => $course), '*', MUST_EXIST);
+            $course = $DB->get_record('course', array('id' => $course), 'id, groupmode, groupmodeforce', MUST_EXIST);
         }
         if ($course->id != SITEID) {
             $coursecontext = \context_course::instance($course->id);
@@ -2602,7 +2602,7 @@ function calendar_get_all_allowed_types() {
     // This function warms the context cache for the course so the calls
     // to load the course context in calendar_get_allowed_types don't result
     // in additional DB queries.
-    $courses = calendar_get_default_courses(null, '*', true);
+    $courses = calendar_get_default_courses(null, 'id, groupmode, groupmodeforce', true);
 
     // We want to pre-fetch all of the groups for each course in a single
     // query to avoid calendar_get_allowed_types from hitting the DB for
index 5d9af03..859acc6 100644 (file)
@@ -221,9 +221,11 @@ class core_calendar_type_testcase extends advanced_testcase {
         $counter++;
 
         if ($element == 'dateselector') {
-            $el = $this->mform->addElement('date_selector', 'dateselector' . $counter, null, array('timezone' => 0.0, 'step' => 1));
+            $el = $this->mform->addElement('date_selector',
+                    'dateselector' . $counter, null, array('timezone' => 0.0));
         } else {
-            $el = $this->mform->addElement('date_time_selector', 'dateselector' . $counter, null, array('timezone' => 0.0, 'step' => 1, 'optional' => false));
+            $el = $this->mform->addElement('date_time_selector',
+                    'dateselector' . $counter, null, array('timezone' => 0.0, 'optional' => false));
         }
         $submitvalues = array('dateselector' . $counter => $date);
 
index 3298187..3323e1f 100644 (file)
@@ -533,6 +533,7 @@ class core_calendar_container_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 9375126..e2bf0bf 100644 (file)
@@ -465,6 +465,7 @@ class core_calendar_event_factory_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 8294651..47f3f4b 100644 (file)
@@ -147,6 +147,7 @@ class core_calendar_event_mapper_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index 2fe4bbb..178bfff 100644 (file)
@@ -485,31 +485,58 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase {
         // Create some category events.
         $this->setAdminUser();
         $record = new stdClass();
+        $record->courseid = 0;
         $record->categoryid = $category->id;
-        $this->create_calendar_event('category a', $USER->id, 'category', 0, time(), $record);
+        $record->timestart = time() - DAYSECS;
+        $catevent1 = $this->create_calendar_event('category a', $USER->id, 'category', 0, time(), $record);
 
+        $record = new stdClass();
+        $record->courseid = 0;
         $record->categoryid = $category2->id;
-        $this->create_calendar_event('category b', $USER->id, 'category', 0, time(), $record);
+        $record->timestart = time() + DAYSECS;
+        $catevent2 = $this->create_calendar_event('category b', $USER->id, 'category', 0, time(), $record);
 
         // Now as student, make sure we get the events of the courses I am enrolled.
         $this->setUser($user2);
         $paramevents = array('categoryids' => array($category2b->id));
-        $options = array('timeend' => time() + 7 * WEEKSECS);
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
 
         // Should be just one, since there's just one category event of the course I am enrolled (course3 - cat2b).
         $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
+        $this->assertEquals(0, count($events['warnings']));
+
+        // Now get category events but by course (there aren't course events in the course).
+        $paramevents = array('courseids' => array($course3->id));
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
+        $this->assertEquals(0, count($events['warnings']));
+
+        // Empty events in one where I'm not enrolled and one parent category
+        // (parent of a category where this is a course where the user is enrolled).
+        $paramevents = array('categoryids' => array($category2->id, $category->id));
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
+        $events = core_calendar_external::get_calendar_events($paramevents, $options);
+        $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
+        $this->assertEquals(1, count($events['events']));
+        $this->assertEquals($catevent2->id, $events['events'][0]['id']);
         $this->assertEquals(0, count($events['warnings']));
 
         // Admin can see all category events.
         $this->setAdminUser();
         $paramevents = array('categoryids' => array($category->id, $category2->id, $category2b->id));
-        $options = array('timeend' => time() + 7 * WEEKSECS);
+        $options = array('timeend' => time() + 7 * WEEKSECS, 'userevents' => false, 'siteevents' => false);
         $events = core_calendar_external::get_calendar_events($paramevents, $options);
         $events = external_api::clean_returnvalue(core_calendar_external::get_calendar_events_returns(), $events);
         $this->assertEquals(2, count($events['events']));
         $this->assertEquals(0, count($events['warnings']));
+        $this->assertEquals($catevent1->id, $events['events'][0]['id']);
+        $this->assertEquals($catevent2->id, $events['events'][1]['id']);
     }
 
     /**
index 2c3b461..014ae60 100644 (file)
@@ -56,6 +56,7 @@ function create_event($properties) {
     $record->timesort = 0;
     $record->type = CALENDAR_EVENT_TYPE_STANDARD;
     $record->courseid = 0;
+    $record->categoryid = 0;
 
     foreach ($properties as $name => $value) {
         $record->$name = $value;
index ec6320c..0b935af 100644 (file)
@@ -136,6 +136,7 @@ class core_calendar_repeat_event_collection_testcase extends advanced_testcase {
         $record->timesort = 0;
         $record->type = 1;
         $record->courseid = 0;
+        $record->categoryid = 0;
 
         foreach ($properties as $name => $value) {
             $record->$name = $value;
index f6b0e6f..b7fba4d 100644 (file)
@@ -123,21 +123,10 @@ echo $renderer->start_layout();
 echo html_writer::start_tag('div', array('class'=>'heightcontainer'));
 echo $OUTPUT->heading(get_string('calendar', 'calendar'));
 
-if ($view == 'day' || $view == 'upcoming') {
-    switch($view) {
-        case 'day':
-            list($data, $template) = calendar_get_view($calendar, $view);
-            echo $renderer->render_from_template($template, $data);
-        break;
-        case 'upcoming':
-            list($data, $template) = calendar_get_view($calendar, $view);
-            echo $renderer->render_from_template($template, $data);
-        break;
-    }
-} else if ($view == 'month') {
-    list($data, $template) = calendar_get_view($calendar, $view);
-    echo $renderer->render_from_template($template, $data);
-}
+
+list($data, $template) = calendar_get_view($calendar, $view);
+echo $renderer->render_from_template($template, $data);
+
 echo html_writer::end_tag('div');
 
 list($data, $template) = calendar_get_footer_options($calendar);
index 44d8e25..55e7529 100644 (file)
@@ -52,6 +52,16 @@ class cohort_summary_exporter extends \core\external\exporter {
                 'default' => '',
                 'null' => NULL_ALLOWED
             ),
+            'description' => array(
+                'type' => PARAM_TEXT,
+                'default' => '',
+                'null' => NULL_ALLOWED
+            ),
+            'descriptionformat' => array(
+                'type' => PARAM_INT,
+                'default' => FORMAT_HTML,
+                'null' => NULL_ALLOWED
+            ),
             'visible' => array(
                 'type' => PARAM_BOOL,
             )
index 4de545e..5f257cd 100644 (file)
@@ -504,6 +504,7 @@ function cohort_get_invisible_contexts() {
             $excludedcontexts[] = $ctx->id;
         }
     }
+    $records->close();
     return $excludedcontexts;
 }
 
index fc16c0c..9d04083 100644 (file)
@@ -165,7 +165,7 @@ class api {
         require_capability('moodle/competency:competencymanage', $competency->get_context());
 
         // Reset the sortorder, use reorder instead.
-        $competency->set('sortorder', null);
+        $competency->set('sortorder', 0);
         $competency->create();
 
         \core\event\competency_created::create_from_competency($competency)->trigger();
index c537508..9e417e9 100644 (file)
@@ -76,7 +76,7 @@ class competency extends persistent {
                 'default' => FORMAT_HTML
             ),
             'sortorder' => array(
-                'default' => null,
+                'default' => 0,
                 'type' => PARAM_INT
             ),
             'parentid' => array(
index 3e0c8f8..8c6df0b 100644 (file)
@@ -53,7 +53,7 @@ class template_competency extends persistent {
             ),
             'sortorder' => array(
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ),
         );
     }
index 3880a2a..5b53589 100644 (file)
@@ -66,7 +66,7 @@ class user_competency_plan extends persistent {
             ),
             'sortorder' => array(
                 'type' => PARAM_INT,
-                'default' => null,
+                'default' => 0,
             ),
         );
     }
index f8a09e0..f0b50c8 100644 (file)
@@ -281,8 +281,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
             'descriptionformat' => FORMAT_HTML,
-            'competencyframeworkid' => $frameworkid,
-            'sortorder' => 0
+            'competencyframeworkid' => $frameworkid
         );
         $result = external::create_competency($competency);
         return (object) external_api::clean_returnvalue(external::create_competency_returns(), $result);
@@ -294,8 +293,7 @@ class core_competency_external_testcase extends externallib_advanced_testcase {
             'shortname' => 'shortname' . $number,
             'idnumber' => 'idnumber' . $number,
             'description' => 'description' . $number,
-            'descriptionformat' => FORMAT_HTML,
-            'sortorder' => 0
+            'descriptionformat' => FORMAT_HTML
         );
         $result = external::update_competency($competency);
         return external_api::clean_returnvalue(external::update_competency_returns(), $result);
index 142496b..24384e9 100644 (file)
@@ -249,7 +249,7 @@ class core_completion_external extends external_api {
 
         $completion = new completion_info($course);
         $activities = $completion->get_activities();
-        $progresses = $completion->get_progress_all();
+        $progresses = $completion->get_progress_all('u.id = :uid', ['uid' => $params['userid']]);
         $userprogress = $progresses[$user->id];
 
         $results = array();
@@ -265,9 +265,11 @@ class core_completion_external extends external_api {
                 $thisprogress  = $userprogress->progress[$activity->id];
                 $state         = $thisprogress->completionstate;
                 $timecompleted = $thisprogress->timemodified;
+                $overrideby    = $thisprogress->overrideby;
             } else {
                 $state = COMPLETION_INCOMPLETE;
                 $timecompleted = 0;
+                $overrideby = null;
             }
 
             $results[] = array(
@@ -276,7 +278,8 @@ class core_completion_external extends external_api {
                        'instance'      => $activity->instance,
                        'state'         => $state,
                        'timecompleted' => $timecompleted,
-                       'tracking'      => $activity->completion
+                       'tracking'      => $activity->completion,
+                       'overrideby'    => $overrideby
             );
         }
 
@@ -308,6 +311,8 @@ class core_completion_external extends external_api {
                             'timecompleted' => new external_value(PARAM_INT, 'timestamp for completed activity'),
                             'tracking'      => new external_value(PARAM_INT, 'type of tracking:
                                                                     0 means none, 1 manual, 2 automatic'),
+                            'overrideby' => new external_value(PARAM_INT, 'The user id who has overriden the status, or null',
+                                VALUE_OPTIONAL),
                         ), 'Activity'
                     ), 'List of activities status'
                 ),
index 29c570b..592c84e 100644 (file)
@@ -279,7 +279,8 @@ class completion_criteria_activity extends completion_criteria {
             $details['requirement'][] = get_string('markingyourselfcomplete', 'completion');
         } elseif ($cm->completion == COMPLETION_TRACKING_AUTOMATIC) {
             if ($cm->completionview) {
-                $details['requirement'][] = get_string('viewingactivity', 'completion', $this->module);
+                $modulename = core_text::strtolower(get_string('modulename', $this->module));
+                $details['requirement'][] = get_string('viewingactivity', 'completion', $modulename);
             }
 
             if (!is_null($cm->completiongradeitemnumber)) {
index 0a5ec14..355ecc2 100644 (file)
@@ -164,7 +164,25 @@ class core_completion_externallib_testcase extends externallib_advanced_testcase
         // We added 4 activities, but only 3 with completion enabled and one of those is hidden.
         $this->assertCount(3, $result['statuses']);
 
-        // Change teacher role capabilities (disable access al goups).
+        // Override status by teacher.
+        $completion->update_state($cmforum, COMPLETION_INCOMPLETE, $student->id, true);
+
+        $result = core_completion_external::get_activities_completion_status($course->id, $student->id);
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(
+            core_completion_external::get_activities_completion_status_returns(), $result);
+
+        // Check forum has been overriden by the teacher.
+        foreach ($result['statuses'] as $status) {
+            if ($status['cmid'] == $forum->cmid) {
+                $this->assertEquals(COMPLETION_INCOMPLETE, $status['state']);
+                $this->assertEquals(COMPLETION_TRACKING_MANUAL, $status['tracking']);
+                $this->assertEquals($teacher->id, $status['overrideby']);
+                break;
+            }
+        }
+
+        // Change teacher role capabilities (disable access all groups).
         $context = context_course::instance($course->id);
         assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $context);
         accesslib_clear_all_caches_for_unit_testing();
index 6203b8a..3dd8de4 100644 (file)
@@ -7,7 +7,7 @@
     "require-dev": {
         "phpunit/phpunit": "6.4.*",
         "phpunit/dbUnit": "3.0.*",
-        "moodlehq/behat-extension": "3.34.1",
+        "moodlehq/behat-extension": "3.35.0",
         "mikey179/vfsStream": "^1.6"
     }
 }
index d69ba0b..f90ae23 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
         "This file is @generated automatically"
     ],
-    "content-hash": "b36746ace2486c033136c855a63f3793",
+    "content-hash": "7cd70172c941fb07f0a2d4173baef5f1",
     "packages": [],
     "packages-dev": [
         {
         },
         {
             "name": "behat/mink-extension",
-            "version": "v2.2",
+            "version": "2.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Behat/MinkExtension.git",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59"
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/5b4bda64ff456104564317e212c823e45cad9d59",
-                "reference": "5b4bda64ff456104564317e212c823e45cad9d59",
+                "url": "https://api.github.com/repos/Behat/MinkExtension/zipball/badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
+                "reference": "badc565b7a1d05c4a4bf49c789045bcf7af6c6de",
                 "shasum": ""
             },
             "require": {
-                "behat/behat": "~3.0,>=3.0.5",
-                "behat/mink": "~1.5",
+                "behat/behat": "^3.0.5",
+                "behat/mink": "^1.5",
                 "php": ">=5.3.2",
-                "symfony/config": "~2.2|~3.0"
+                "symfony/config": "^2.7|^3.0|^4.0"
             },
             "require-dev": {
-                "behat/mink-goutte-driver": "~1.1",
-                "phpspec/phpspec": "~2.0"
+                "behat/mink-goutte-driver": "^1.1",
+                "phpspec/phpspec": "^2.0"
             },
             "type": "behat-extension",
             "extra": {
                 "test",
                 "web"
             ],
-            "time": "2016-02-15T07:55:18+00:00"
+            "time": "2017-11-24T19:30:49+00:00"
         },
         {
             "name": "behat/mink-goutte-driver",
         },
         {
             "name": "fabpot/goutte",
-            "version": "v3.2.1",
+            "version": "v3.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/FriendsOfPHP/Goutte.git",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638"
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/db5c28f4a010b4161d507d5304e28a7ebf211638",
-                "reference": "db5c28f4a010b4161d507d5304e28a7ebf211638",
+                "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/395f61d7c2e15a813839769553a4de16fa3b3c96",
+                "reference": "395f61d7c2e15a813839769553a4de16fa3b3c96",
                 "shasum": ""
             },
             "require": {
                 "guzzlehttp/guzzle": "^6.0",
                 "php": ">=5.5.0",
-                "symfony/browser-kit": "~2.1|~3.0",
-                "symfony/css-selector": "~2.1|~3.0",
-                "symfony/dom-crawler": "~2.1|~3.0"
+                "symfony/browser-kit": "~2.1|~3.0|~4.0",
+                "symfony/css-selector": "~2.1|~3.0|~4.0",
+                "symfony/dom-crawler": "~2.1|~3.0|~4.0"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^3.3 || ^4"
             },
             "type": "application",
             "extra": {
             "autoload": {
                 "psr-4": {
                     "Goutte\\": "Goutte"
-                }
+                },
+                "exclude-from-classmap": [
+                    "Goutte/Tests"
+                ]
             },
             "notification-url": "https://packagist.org/downloads/",
             "license": [
             "keywords": [
                 "scraper"
             ],
-            "time": "2017-01-03T13:21:43+00:00"
+            "time": "2017-11-19T08:45:40+00:00"
         },
         {
             "name": "guzzlehttp/guzzle",
         },
         {
             "name": "moodlehq/behat-extension",
-            "version": "v3.34.1",
+            "version": "v3.35.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/moodlehq/moodle-behat-extension.git",
         },
         {
             "name": "phpspec/prophecy",
-            "version": "v1.7.2",
+            "version": "1.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
-                "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
+                "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "phpspec/phpspec": "^2.5|^3.2",
-                "phpunit/phpunit": "^4.8 || ^5.6.5"
+                "phpunit/phpunit": "^4.8.35 || ^5.7"
             },
             "type": "library",
             "extra": {
                 "spy",
                 "stub"
             ],
-            "time": "2017-09-04T11:05:03+00:00"
+            "time": "2017-11-24T13:59:53+00:00"
         },
         {
             "name": "phpunit/dbunit",
-            "version": "3.0.1",
+            "version": "3.0.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/dbunit.git",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d"
+                "reference": "403350339b6aca748ee0067d027d85621992e21f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/6b9cec80dca8694243aade33bceb425ccafbbd0d",
-                "reference": "6b9cec80dca8694243aade33bceb425ccafbbd0d",
+                "url": "https://api.github.com/repos/sebastianbergmann/dbunit/zipball/403350339b6aca748ee0067d027d85621992e21f",
+                "reference": "403350339b6aca748ee0067d027d85621992e21f",
                 "shasum": ""
             },
             "require": {
                 "ext-simplexml": "*",
                 "php": "^7.0",
                 "phpunit/phpunit": "^6.0",
-                "symfony/yaml": "^3.0"
+                "symfony/yaml": "^3.0 || ^4.0"
             },
             "type": "library",
             "extra": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-10-19T13:21:48+00:00"
+            "time": "2017-11-18T17:40:34+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "5.2.2",
+            "version": "5.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b"
+                "reference": "8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8ed1902a57849e117b5651fc1a5c48110946c06b",
-                "reference": "8ed1902a57849e117b5651fc1a5c48110946c06b",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d",
+                "reference": "8e1d2397d8adf59a3f12b2878a3aaa66d1ab189d",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
                 "phpunit/php-file-iterator": "^1.4.2",
                 "phpunit/php-text-template": "^1.2.1",
-                "phpunit/php-token-stream": "^1.4.11 || ^2.0",
+                "phpunit/php-token-stream": "^2.0",
                 "sebastian/code-unit-reverse-lookup": "^1.0.1",
                 "sebastian/environment": "^3.0",
                 "sebastian/version": "^2.0.1",
                 "testing",
                 "xunit"
             ],
-            "time": "2017-08-03T12:40:43+00:00"
+            "time": "2017-11-03T13:47:33+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
-            "version": "1.4.2",
+            "version": "1.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5"
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
-                "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/8ebba84e5bd74fc5fdeb916b38749016c7232f93",
+                "reference": "8ebba84e5bd74fc5fdeb916b38749016c7232f93",
                 "shasum": ""
             },
             "require": {
                 "filesystem",
                 "iterator"
             ],
-            "time": "2016-10-03T07:40:28+00:00"
+            "time": "2017-11-24T15:00:59+00:00"
         },
         {
             "name": "phpunit/php-text-template",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "6.4.3",
+            "version": "6.4.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13"
+                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
-                "reference": "06b28548fd2b4a20c3cd6e247dc86331a7d4db13",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/562f7dc75d46510a4ed5d16189ae57fbe45a9932",
+                "reference": "562f7dc75d46510a4ed5d16189ae57fbe45a9932",
                 "shasum": ""
             },
             "require": {
                 "testing",
                 "xunit"
             ],
-            "time": "2017-10-16T13:18:59+00:00"
+            "time": "2017-11-08T11:26:09+00:00"
         },
         {
             "name": "phpunit/phpunit-mock-objects",
         },
         {
             "name": "sebastian/comparator",
-            "version": "2.0.2",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/comparator.git",
-                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a"
+                "reference": "1174d9018191e93cb9d719edec01257fc05f8158"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
-                "reference": "ae068fede81d06e7bb9bb46a367210a3d3e1fe6a",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1174d9018191e93cb9d719edec01257fc05f8158",
+                "reference": "1174d9018191e93cb9d719edec01257fc05f8158",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.0",
                 "sebastian/diff": "^2.0",
-                "sebastian/exporter": "^3.0"
+                "sebastian/exporter": "^3.1"
             },
             "require-dev": {
-                "phpunit/phpunit": "^6.0"
+                "phpunit/phpunit": "^6.4"
             },
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-master": "2.0.x-dev"
+                    "dev-master": "2.1.x-dev"
                 }
             },
             "autoload": {
                 }
             ],
             "description": "Provides the functionality to compare PHP values for equality",
-            "homepage": "http://www.github.com/sebastianbergmann/comparator",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
             "keywords": [
                 "comparator",
                 "compare",
                 "equality"
             ],
-            "time": "2017-08-03T07:14:59+00:00"
+            "time": "2017-11-03T07:16:52+00:00"
         },
         {
             "name": "sebastian/diff",
         },
         {
             "name": "symfony/browser-kit",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/browser-kit.git",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839"
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/317d5bdf0127f06db7ea294186132b4f5b036839",
-                "reference": "317d5bdf0127f06db7ea294186132b4f5b036839",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/03f957cd24bf939524f07b8b910c89cfcad722a8",
+                "reference": "03f957cd24bf939524f07b8b910c89cfcad722a8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony BrowserKit Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/class-loader",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/class-loader.git",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01"
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/class-loader/zipball/7572c904b209fa9907c69a6a9a68243c265a4d01",
-                "reference": "7572c904b209fa9907c69a6a9a68243c265a4d01",
+                "url": "https://api.github.com/repos/symfony/class-loader/zipball/df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
+                "reference": "df173ac2af96ce202bf8bb5a3fc0bec8a4fdd4d1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony ClassLoader Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/config",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/config.git",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
-                "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+                "url": "https://api.github.com/repos/symfony/config/zipball/8d2649077dc54dfbaf521d31f217383d82303c5f",
+                "reference": "8d2649077dc54dfbaf521d31f217383d82303c5f",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Config Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T18:56:58+00:00"
+            "time": "2017-11-07T14:16:22+00:00"
         },
         {
             "name": "symfony/console",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c"
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/116bc56e45a8e5572e51eb43ab58c769a352366c",
-                "reference": "116bc56e45a8e5572e51eb43ab58c769a352366c",
+                "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805",
+                "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Console Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-16T15:24:32+00:00"
         },
         {
             "name": "symfony/css-selector",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/css-selector.git",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332"
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/css-selector/zipball/07447650225ca9223bd5c97180fe7c8267f7d332",
-                "reference": "07447650225ca9223bd5c97180fe7c8267f7d332",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/66e6e046032ebdf1f562c26928549f613d428bd1",
+                "reference": "66e6e046032ebdf1f562c26928549f613d428bd1",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony CssSelector Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/debug",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/debug.git",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd"
+                "reference": "74557880e2846b5c84029faa96b834da37e29810"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/debug/zipball/eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
-                "reference": "eb95d9ce8f18dcc1b3dfff00cb624c402be78ffd",
+                "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810",
+                "reference": "74557880e2846b5c84029faa96b834da37e29810",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Debug Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-10T16:38:39+00:00"
         },
         {
             "name": "symfony/dependency-injection",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dependency-injection.git",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
-                "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
+                "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
+                "reference": "4e84f5af2c2d51ee3dee72df40b7fc08f49b4ab8",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DependencyInjection Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-04T17:15:30+00:00"
+            "time": "2017-11-13T18:10:32+00:00"
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1"
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/40dafd42d5dad7fe5ad4e958413d92a207522ac1",
-                "reference": "40dafd42d5dad7fe5ad4e958413d92a207522ac1",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/cebe3c068867956e012d9135282ba6a05d8a259e",
+                "reference": "cebe3c068867956e012d9135282ba6a05d8a259e",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony DomCrawler Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/event-dispatcher.git",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423"
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d7ba037e4b8221956ab1e221c73c9e27e05dd423",
-                "reference": "d7ba037e4b8221956ab1e221c73c9e27e05dd423",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/271d8c27c3ec5ecee6e2ac06016232e249d638d9",
+                "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony EventDispatcher Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-05T15:47:03+00:00"
         },
         {
             "name": "symfony/filesystem",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/filesystem.git",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
-                "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
+                "url": "https://api.github.com/repos/symfony/filesystem/zipball/77db266766b54db3ee982fe51868328b887ce15c",
+                "reference": "77db266766b54db3ee982fe51868328b887ce15c",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Filesystem Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-03T13:33:10+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
         },
         {
             "name": "symfony/process",
-            "version": "v2.8.28",
+            "version": "v2.8.31",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/process.git",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176"
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/process/zipball/26c9fb02bf06bd6b90f661a5bd17e510810d0176",
-                "reference": "26c9fb02bf06bd6b90f661a5bd17e510810d0176",
+                "url": "https://api.github.com/repos/symfony/process/zipball/d25449e031f600807949aab7cadbf267712f4eee",
+                "reference": "d25449e031f600807949aab7cadbf267712f4eee",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Process Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-01T21:00:16+00:00"
+            "time": "2017-11-05T15:25:56+00:00"
         },
         {
             "name": "symfony/translation",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/translation.git",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f"
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/translation/zipball/409bf229cd552bf7e3faa8ab7e3980b07672073f",
-                "reference": "409bf229cd552bf7e3faa8ab7e3980b07672073f",
+                "url": "https://api.github.com/repos/symfony/translation/zipball/373e553477e55cd08f8b86b74db766c75b987fdb",
+                "reference": "373e553477e55cd08f8b86b74db766c75b987fdb",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Translation Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-02T06:42:24+00:00"
+            "time": "2017-11-07T14:12:55+00:00"
         },
         {
             "name": "symfony/yaml",
-            "version": "v3.3.10",
+            "version": "v3.3.13",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
-                "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/0938408c4faa518d95230deabb5f595bf0de31b9",
+                "reference": "0938408c4faa518d95230deabb5f595bf0de31b9",
                 "shasum": ""
             },
             "require": {
             ],
             "description": "Symfony Yaml Component",
             "homepage": "https://symfony.com",
-            "time": "2017-10-05T14:43:42+00:00"
+            "time": "2017-11-10T18:26:04+00:00"
         },
         {
             "name": "theseer/tokenizer",
index 6c60e81..5308052 100644 (file)
@@ -70,6 +70,15 @@ $CFG->dboptions = array(
                                 // can be removed for MySQL (by default it will
                                 // use 'utf8mb4_unicode_ci'. This option should
                                 // be removed for all other databases.
+    // 'fetchbuffersize' => 100000, // On PostgreSQL, this option sets a limit
+                                // on the number of rows that are fetched into
+                                // memory when doing a large recordset query
+                                // (e.g. search indexing). Default is 100000.
+                                // Uncomment and set to a value to change it,
+                                // or zero to turn off the limit. You need to
+                                // set to zero if you are using pg_bouncer in
+                                // 'transaction' mode (it is fine in 'session'
+                                // mode).
 );
 
 
index d3aba3b..012f8f7 100644 (file)
@@ -504,10 +504,10 @@ class core_course_external extends external_api {
                 $courseinfo['groupmode'] = $course->groupmode;
                 $courseinfo['groupmodeforce'] = $course->groupmodeforce;
                 $courseinfo['defaultgroupingid'] = $course->defaultgroupingid;
-                $courseinfo['lang'] = $course->lang;
+                $courseinfo['lang'] = clean_param($course->lang, PARAM_LANG);
                 $courseinfo['timecreated'] = $course->timecreated;
                 $courseinfo['timemodified'] = $course->timemodified;
-                $courseinfo['forcetheme'] = $course->theme;
+                $courseinfo['forcetheme'] = clean_param($course->theme, PARAM_THEME);
                 $courseinfo['enablecompletion'] = $course->enablecompletion;
                 $courseinfo['completionnotify'] = $course->completionnotify;
                 $courseinfo['courseformatoptions'] = array();
@@ -1726,7 +1726,7 @@ class core_course_external extends external_api {
                         $categoryinfo['visible'] = $category->visible;
                         $categoryinfo['visibleold'] = $category->visibleold;
                         $categoryinfo['timemodified'] = $category->timemodified;
-                        $categoryinfo['theme'] = $category->theme;
+                        $categoryinfo['theme'] = clean_param($category->theme, PARAM_THEME);
                     }
 
                     $categoriesinfo[] = $categoryinfo;
@@ -3068,6 +3068,14 @@ class core_course_external extends external_api {
             foreach ($coursefields as $field) {
                 $coursesdata[$course->id][$field] = $course->{$field};
             }
+
+            // Clean lang and auth fields for external functions (it may content uninstalled themes or language packs).
+            if (isset($coursesdata[$course->id]['theme'])) {
+                $coursesdata[$course->id]['theme'] = clean_param($coursesdata[$course->id]['theme'], PARAM_THEME);
+            }
+            if (isset($coursesdata[$course->id]['lang'])) {
+                $coursesdata[$course->id]['lang'] = clean_param($coursesdata[$course->id]['lang'], PARAM_LANG);
+            }
         }
 
         return array(
index 63aa87f..2b6e4bd 100644 (file)
@@ -2211,6 +2211,21 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(0, $result['courses']);
     }
 
+    /**
+     * Test get_courses_by_field_invalid_theme_and_lang
+     */
+    public function test_get_courses_by_field_invalid_theme_and_lang() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+
+        $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl'));
+        $result = core_course_external::get_courses_by_field('id', $course->id);
+        $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
+        $this->assertEmpty($result['courses']['0']['theme']);
+        $this->assertEmpty($result['courses']['0']['lang']);
+    }
+
+
     public function test_check_updates() {
         global $DB;
         $this->resetAfterTest(true);
index c38575a..351cf66 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_database_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 3a0c38f..55b074c 100644 (file)
@@ -343,7 +343,7 @@ class core_enrol_external extends external_api {
                 'summaryformat' => $course->summaryformat,
                 'format' => $course->format,
                 'showgrades' => $course->showgrades,
-                'lang' => $course->lang,
+                'lang' => clean_param($course->lang, PARAM_LANG),
                 'enablecompletion' => $course->enablecompletion,
                 'category' => $course->category,
                 'progress' => $progress,
@@ -453,7 +453,11 @@ class core_enrol_external extends external_api {
                                                $params['perpage']);
 
         $results = array();
-        $requiredfields = ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'];
+        // Add also extra user fields.
+        $requiredfields = array_merge(
+            ['id', 'fullname', 'profileimageurl', 'profileimageurlsmall'],
+            get_extra_user_fields($context)
+        );
         foreach ($users['users'] as $id => $user) {
             // Note: We pass the course here to validate that the current user can at least view user details in this course.
             // The user we are looking at is not in this course yet though - but we only fetch the minimal set of
index 91ea55b..59ae42e 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_flatfile_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 51a3a0b..cb6fb76 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_guest_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 53366c6..a4e1df7 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_imsenterprise_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index e3ed112..dd8d9a8 100644 (file)
@@ -206,7 +206,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
         $this->set_xml_file(array($imsuser));
 
         $this->imsplugin->cron();
-        $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+        $this->assertEquals(1, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
     }
 
     /**
@@ -227,7 +227,7 @@ class enrol_imsenterprise_testcase extends advanced_testcase {
         $this->set_xml_file(array($imsuser));
 
         $this->imsplugin->cron();
-        $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), '*', MUST_EXIST));
+        $this->assertEquals(0, $DB->get_field('user', 'deleted', array('id' => $user->id), MUST_EXIST));
     }
 
     /**
index 0086edf..cec4ea4 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_manual_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 29f8564..6f22cca 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_mnet_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 5ed2ae7..bdbbbf6 100644 (file)
@@ -45,9 +45,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_paypal_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index f9dff59..4965298 100644 (file)
@@ -27,9 +27,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_enrol_self_upgrade($oldversion) {
     global $CFG, $DB;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2016052301) {
         // Get roles with manager archetype.
         $managerroles = get_archetype_roles('manager');
index d85f4d3..22c3c5c 100644 (file)
@@ -376,8 +376,12 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
             'enddate'          => $timenow + WEEKSECS
         );
 
+        $coursedata2 = array(
+            'lang'             => 'kk', // Check invalid language pack.
+        );
+
         $course1 = self::getDataGenerator()->create_course($coursedata1);
-        $course2 = self::getDataGenerator()->create_course();
+        $course2 = self::getDataGenerator()->create_course($coursedata2);
         $courses = array($course1, $course2);
 
         // Enrol $USER in the courses.
@@ -414,6 +418,9 @@ class core_enrol_externallib_testcase extends externallib_advanced_testcase {
                 foreach ($coursedata1 as $fieldname => $value) {
                     $this->assertEquals($courseenrol[$fieldname], $course1->$fieldname);
                 }
+            } else {
+                // Check language pack. Should be empty since an incorrect one was used when creating the course.
+                $this->assertEmpty($courseenrol['lang']);
             }
         }
     }
index 32793d3..da8c5ba 100644 (file)
@@ -127,7 +127,7 @@ class converter {
         if ($status === conversion::STATUS_PENDING || $status === conversion::STATUS_FAILED) {
             // The current status is either pending or failed.
             // Attempt to pick up a new converter and convert the document.
-            $from = \core_filetypes::get_file_extension($file->get_mimetype());
+            $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
             $converters = $this->get_document_converter_classes($from, $format);
             $currentconverter = $this->get_next_converter($converters, $conversion->get('converter'));
 
@@ -225,9 +225,9 @@ class converter {
             return false;
         }
 
-        $from = \core_filetypes::get_file_extension($file->get_mimetype());
+        $from = pathinfo($file->get_filename(), PATHINFO_EXTENSION);
         if (!$from) {
-            // No mime type could be found. Unable to determine converter.
+            // No file extension could be found. Unable to determine converter.
             return false;
         }
 
index 950d593..e345c1e 100644 (file)
@@ -341,7 +341,7 @@ class core_files_converter_testcase extends advanced_testcase {
     }
 
     /**
-     * Test the can_convert_storedfile_to function with a file with indistinguished mimetype.
+     * Test the can_convert_storedfile_to function with a file with a known mimetype and extension.
      */
     public function test_can_convert_storedfile_to_docx() {
         $returnvalue = (object) [];
@@ -352,8 +352,7 @@ class core_files_converter_testcase extends advanced_testcase {
 
         $types = \core_filetypes::get_types();
 
-        // A file with filename '.' is a directory.
-        $file = $this->get_stored_file('example content', 'example', [
+        $file = $this->get_stored_file('example content', 'example.docx', [
                 'mimetype' => $types['docx']['type'],
             ]);
 
index 7edde8a..5294046 100644 (file)
@@ -33,26 +33,6 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
 
     require_once($CFG->dirroot . '/filter/mathjaxloader/db/upgradelib.php');
 
-    if ($oldversion < 2016032200) {
-
-        $httpurl = get_config('filter_mathjaxloader', 'httpurl');
-        // Don't change the config if it has been manually changed to something besides the default setting value.
-        if ($httpurl === "http://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js") {
-            set_config('httpurl', 'http://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js', 'filter_mathjaxloader');
-        }
-
-        $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
-        // Don't change the config if it has been manually changed to something besides the default setting value.
-        if ($httpsurl === "https://cdn.mathjax.org/mathjax/2.5-latest/MathJax.js") {
-            set_config('httpsurl', 'https://cdn.mathjax.org/mathjax/2.6-latest/MathJax.js', 'filter_mathjaxloader');
-        }
-
-        upgrade_plugin_savepoint(true, 2016032200, 'filter', 'mathjaxloader');
-    }
-
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     if ($oldversion < 2016080200) {
         // We are consolodating the two settings for http and https url into only the https
         // setting. Since it is preferably to always load the secure resource.
@@ -78,8 +58,10 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
         }
         upgrade_plugin_savepoint(true, 2016102500, 'filter', 'mathjaxloader');
     }
+
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
+    //
     if ($oldversion < 2017040300) {
 
         $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
@@ -97,6 +79,7 @@ function xmldb_filter_mathjaxloader_upgrade($oldversion) {
 
         upgrade_plugin_savepoint(true, 2017040300, 'filter', 'mathjaxloader');
     }
+
     if ($oldversion < 2017042602) {
 
         $httpsurl = get_config('filter_mathjaxloader', 'httpsurl');
index 8b58139..c89fab9 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_filter_mediaplugin_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 459183d..53cc010 100644 (file)
@@ -32,9 +32,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_filter_tex_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index 68f39ad..5677911 100644 (file)
@@ -37,18 +37,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_gradingform_guide_upgrade($oldversion) {
     global $DB;
 
-    if ($oldversion < 2016051100) {
-        // Clean up empty string or null marking guide comments.
-        $sql = $DB->sql_isempty('gradingform_guide_comments', 'description', true, true);
-        $sql .= " OR description IS NULL ";
-        $DB->delete_records_select('gradingform_guide_comments', $sql);
-        // Main savepoint reached.
-        upgrade_plugin_savepoint(true, 2016051100, 'gradingform', 'guide');
-    }
-
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index b79f5c7..2f06662 100644 (file)
@@ -428,6 +428,7 @@ abstract class gradingform_controller {
         foreach ($records as $record) {
             $rv[] = $this->get_instance($record);
         }
+        $records->close();
         return $rv;
     }
 
index bab220d..92e65e3 100644 (file)
@@ -33,9 +33,6 @@ defined('MOODLE_INTERNAL') || die();
 function xmldb_gradingform_rubric_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index d7c8294..d2f57e9 100644 (file)
@@ -185,8 +185,8 @@ class grading_manager {
         } else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
             list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
 
-            if (strval($cm->name) !== '') {
-                $title = $cm->name;
+            if ($cm && strval($cm->name) !== '') {
+                $title = format_string($cm->name, true, array('context' => $context));
             } else {
                 debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
                 $title = $this->get_component();
index 56709df..db3ecb6 100644 (file)
@@ -419,6 +419,7 @@ abstract class screen {
         while ($user = $gui->next_user()) {
             $users[$user->user->id] = $user->user;
         }
+        $gui->close();
         return $users;
     }
 
index 3e662ba..ea9d000 100644 (file)
@@ -29,9 +29,6 @@
 function xmldb_gradereport_user_upgrade($oldversion) {
     global $CFG;
 
-    // Moodle v3.1.0 release upgrade line.
-    // Put any upgrade step following this.
-
     // Automatically generated Moodle v3.2.0 release upgrade line.
     // Put any upgrade step following this.
 
index cb96fa0..b24608d 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/edit/tree/lib.php.
  *
- * @pacakge  core_grade
+ * @package  core_grades
  * @category phpunit
  * @author   Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index 0acea9e..234e6a4 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/import/lib.php.
  *
- * @package   core_grade
+ * @package   core_grades
  * @category  phpunit
  * @copyright 2015 Adrian Greeve <adrian@moodle.com>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
index c284123..28e6cde 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade quering
  *
- * @pacakge   core_grade
+ * @package   core_grades
  * @category  phpunit
  * @copyright 2011 Petr Skoda {@link http://skodak.org}
  * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
index d65e285..75f52c3 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/user/lib.php.
  *
- * @package  core_grade
+ * @package  core_grades
  * @category phpunit
  * @copyright 2012 Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index bade609..d8928e5 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/lib.php.
  *
- * @pacakge  core_grade
+ * @package  core_grades
  * @category phpunit
  * @author   Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index 7bc3289..94ae292 100644 (file)
@@ -17,7 +17,7 @@
 /**
  * Unit tests for grade/report/user/lib.php.
  *
- * @package  core_grade
+ * @package  core_grades
  * @category phpunit
  * @copyright 2012 Andrew Davis
  * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
index 53d2dfc..4c04acd 100644 (file)
@@ -90,9 +90,6 @@ class core_group_external extends external_api {
             if ($DB->get_record('groups', array('courseid'=>$group->courseid, 'name'=>$group->name))) {
                 throw new invalid_parameter_exception('Group with the same name already exists in the course');
             }
-            if (!empty($group->idnumber) && $DB->count_records('groups', array('idnumber' => $group->idnumber))) {
-                throw new invalid_parameter_exception('Group with the same idnumber already exists');
-            }
 
             // now security checks
             $context = context_course::instance($group->courseid, IGNORE_MISSING);
@@ -627,9 +624,6 @@ class core_group_external extends external_api {
             if ($DB->count_records('groupings', array('courseid'=>$grouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('Grouping with the same name already exists in the course');
             }
-            if (!empty($grouping->idnumber) && $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
-                throw new invalid_parameter_exception('Grouping with the same idnumber already exists');
-            }
 
             // Now security checks            .
             $context = context_course::instance($grouping->courseid);
@@ -731,11 +725,6 @@ class core_group_external extends external_api {
                     $DB->count_records('groupings', array('courseid'=>$currentgrouping->courseid, 'name'=>$grouping->name))) {
                 throw new invalid_parameter_exception('A different grouping with the same name already exists in the course');
             }
-            // Check if the new modified grouping idnumber already exists.
-            if (!empty($grouping->idnumber) && $grouping->idnumber != $currentgrouping->idnumber &&
-                    $DB->count_records('groupings', array('idnumber' => $grouping->idnumber))) {
-                throw new invalid_parameter_exception('A different grouping with the same idnumber already exists');
-            }
 
             $grouping->courseid = $currentgrouping->courseid;
 
index 2c09e6d..41f8b2a 100644 (file)
@@ -118,7 +118,7 @@ if ($mform_post->is_cancelled()) {
             //decode encoded commas
             $record[$header[$key]] = preg_replace($csv_encode, $csv_delimiter, trim($value));
         }
-        if ($record[$header[0]]) {
+        if (trim($rawline) !== '') {
             // add a new group to the database
 
             // add fields to object $user
@@ -134,28 +134,32 @@ if ($mform_post->is_cancelled()) {
                 }
             }
 
-            if (isset($newgroup->idnumber)){
+            if (isset($newgroup->idnumber) && strlen($newgroup->idnumber)) {
                 //if idnumber is set, we use that.
                 //unset invalid courseid
                 if (!$mycourse = $DB->get_record('course', array('idnumber'=>$newgroup->idnumber))) {
                     echo $OUTPUT->notification(get_string('unknowncourseidnumber', 'error', $newgroup->idnumber));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
-            } else if (isset($newgroup->coursename)){
+            } else if (isset($newgroup->coursename) && strlen($newgroup->coursename)) {
                 //else use course short name to look up
                 //unset invalid coursename (if no id)
-                if (!$mycourse = $DB->get_record('course', array('shortname', $newgroup->coursename))) {
+                if (!$mycourse = $DB->get_record('course', array('shortname' => $newgroup->coursename))) {
                     echo $OUTPUT->notification(get_string('unknowncourse', 'error', $newgroup->coursename));
                     unset($newgroup->courseid);//unset so 0 doesn't get written to database
+                } else {
+                    $newgroup->courseid = $mycourse->id;
                 }
-                $newgroup->courseid = $mycourse->id;
 
             } else {