Merge branch 'MDL-64500_master' of git://github.com/dmonllao/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Feb 2019 16:10:38 +0000 (17:10 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Feb 2019 16:10:38 +0000 (17:10 +0100)
305 files changed:
admin/index.php
admin/renderer.php
admin/tool/behat/cli/util.php
admin/tool/behat/cli/util_single_run.php
admin/tool/behat/lang/en/tool_behat.php
admin/tool/behat/tests/behat/manipulate_forms.feature
admin/tool/filetypes/edit_form.php
admin/tool/langimport/classes/locale.php [new file with mode: 0644]
admin/tool/langimport/index.php
admin/tool/langimport/lang/en/tool_langimport.php
admin/tool/langimport/tests/locale_test.php [new file with mode: 0644]
admin/tool/lp/classes/external.php
admin/tool/lp/classes/external/user_competency_summary_in_course_exporter.php
admin/tool/lp/classes/output/course_competencies_page.php
admin/tool/lp/templates/course_competencies_page.mustache
admin/tool/lp/templates/user_competency_summary_in_course.mustache
admin/tool/lp/tests/behat/plan_crud.feature
admin/tool/uploadcourse/classes/base_form.php
admin/tool/uploadcourse/classes/step2_form.php
admin/tool/uploaduser/user_form.php
admin/tool/usertours/classes/local/target/block.php
admin/tool/usertours/classes/local/target/selector.php
admin/tool/usertours/classes/local/target/unattached.php
admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php [new file with mode: 0644]
admin/tool/xmldb/actions/edit_table/edit_table.class.php
admin/tool/xmldb/lang/en/tool_xmldb.php
auth/cas/auth.php
auth/cas/cas_form.html [deleted file]
auth/cas/lang/en/auth_cas.php
auth/cas/lang/en/deprecated.txt [new file with mode: 0644]
auth/cas/lib.php [new file with mode: 0644]
auth/cas/settings.php
auth/cas/version.php
auth/ldap/auth.php
auth/ldap/lang/en/auth_ldap.php
auth/mnet/classes/privacy/provider.php
auth/oauth2/classes/privacy/provider.php
auth/shibboleth/auth.php
auth/shibboleth/index_form.html [deleted file]
auth/shibboleth/lang/en/auth_shibboleth.php
auth/shibboleth/login.php
auth/shibboleth/templates/login_form.mustache [new file with mode: 0644]
auth/tests/behat/behat_auth.php
badges/criteria/award_criteria_activity.php
badges/tests/behat/criteria_activity.feature [new file with mode: 0644]
blocks/community/classes/privacy/provider.php
blocks/html/classes/privacy/provider.php
blocks/login/block_login.php
blocks/myoverview/amd/build/view.min.js
blocks/myoverview/amd/src/view.js
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/lang/en/deprecated.txt
blocks/myoverview/templates/placeholders.mustache
blocks/recentlyaccessedcourses/amd/build/main.min.js
blocks/recentlyaccessedcourses/amd/src/main.js
blocks/recentlyaccessedcourses/classes/output/main.php
blocks/recentlyaccessedcourses/lang/en/block_recentlyaccessedcourses.php
blocks/recentlyaccessedcourses/templates/main.mustache
blocks/recentlyaccessedcourses/templates/no-courses.mustache
blocks/recentlyaccessedcourses/templates/recentlyaccessedcourses-view.mustache
blocks/rss_client/classes/privacy/provider.php
blocks/starredcourses/amd/build/main.min.js
blocks/starredcourses/amd/src/main.js
blocks/starredcourses/lang/en/block_starredcourses.php
blocks/starredcourses/templates/no-courses.mustache
blocks/starredcourses/templates/placeholder-course.mustache [deleted file]
blocks/starredcourses/templates/view-cards.mustache [deleted file]
blocks/starredcourses/templates/view.mustache
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
competency/classes/api.php
competency/tests/api_test.php
completion/classes/privacy/provider.php
completion/tests/privacy_test.php
composer.json
config-dist.php
course/classes/deletecategory_form.php
course/completion.js
course/lib.php
course/modlib.php
course/moodleform_mod.php
course/renderer.php
course/templates/no-courses.mustache [moved from blocks/myoverview/templates/no-courses.mustache with 68% similarity]
course/templates/placeholder-course.mustache [moved from blocks/recentlyaccessedcourses/templates/placeholder-course.mustache with 91% similarity]
course/templates/view-cards.mustache [moved from blocks/recentlyaccessedcourses/templates/view-cards.mustache with 95% similarity]
course/tests/behat/app_courselist.feature [new file with mode: 0644]
course/tests/behat/view_subfolders_inline.feature
enrol/classes/privacy/provider.php
enrol/manual/amd/build/form-potential-user-selector.min.js
enrol/manual/amd/src/form-potential-user-selector.js
enrol/manual/classes/enrol_users_form.php
enrol/manual/tests/behat/quickenrolment.feature
grade/edit/tree/calculation.php
group/externallib.php
group/tests/externallib_test.php
install/lang/hr/admin.php
install/lang/ja/install.php
lang/en/admin.php
lang/en/competency.php
lib/accesslib.php
lib/amd/build/checkbox-toggleall.min.js [new file with mode: 0644]
lib/amd/build/icon_system_fontawesome.min.js
lib/amd/build/storagewrapper.min.js
lib/amd/src/checkbox-toggleall.js [new file with mode: 0644]
lib/amd/src/icon_system_fontawesome.js
lib/amd/src/storagewrapper.js
lib/behat/behat_base.php
lib/behat/classes/behat_command.php
lib/behat/classes/behat_config_util.php
lib/behat/form_field/behat_form_field.php
lib/classes/analytics/analyser/courses.php
lib/classes/analytics/analyser/site_courses.php
lib/classes/output/icon_system_fontawesome.php
lib/cronlib.php
lib/db/messages.php
lib/db/upgrade.php
lib/form/amd/build/showadvanced.min.js [new file with mode: 0644]
lib/form/amd/src/showadvanced.js [new file with mode: 0644]
lib/form/modgrade.php
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js [deleted file]
lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/build.json [deleted file]
lib/form/yui/src/showadvanced/js/showadvanced.js [deleted file]
lib/form/yui/src/showadvanced/meta/showadvanced.json [deleted file]
lib/formslib.php
lib/moodlelib.php
lib/navigationlib.php
lib/outputcomponents.php
lib/outputrenderers.php
lib/outputrequirementslib.php
lib/phpmailer/README_MOODLE.txt
lib/phpmailer/src/PHPMailer.php
lib/phpunit/classes/arraydataset.php
lib/requirejs/moodle-config.js
lib/templates/loginform.mustache
lib/templates/pix_icon_fontawesome.mustache
lib/templates/url_select.mustache
lib/tests/behat/app_behat_runtime.js [new file with mode: 0644]
lib/tests/behat/behat_app.php [new file with mode: 0644]
lib/tests/behat/behat_hooks.php
lib/tests/moodlelib_test.php
lib/tests/outputcomponents_test.php
lib/typo3/class.t3lib_div.php
lib/typo3/readme_moodle.txt
lib/upgrade.txt
login/change_password_form.php
login/forgot_password_form.php
message/amd/build/notification_processor_settings.min.js
message/amd/build/preferences_notifications_list_controller.min.js
message/amd/build/preferences_processor_form.min.js
message/amd/src/notification_processor_settings.js
message/amd/src/preferences_notifications_list_controller.js
message/amd/src/preferences_processor_form.js
message/classes/api.php
message/classes/helper.php
message/output/airnotifier/classes/privacy/provider.php
message/pendingcontactrequests.php [deleted file]
message/templates/preferences_processor.mustache
message/tests/externallib_test.php
mod/assign/amd/build/grading_navigation_user_info.min.js
mod/assign/amd/src/grading_navigation_user_info.js
mod/assign/classes/event/base.php
mod/assign/classes/event/remove_submission_form_viewed.php [new file with mode: 0644]
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-debug.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor-min.js
mod/assign/feedback/editpdf/yui/build/moodle-assignfeedback_editpdf-editor/moodle-assignfeedback_editpdf-editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/editor.js
mod/assign/feedback/editpdf/yui/src/editor/js/globals.js
mod/assign/gradingbatchoperationsform.php
mod/assign/gradingtable.php
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/locallib.php
mod/assign/renderer.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/submissionplugin.php
mod/assign/tests/behat/allow_another_attempt.feature
mod/assign/tests/behat/edit_student_submission.feature
mod/assign/tests/behat/page_titles.feature [new file with mode: 0644]
mod/assign/tests/behat/remove_submission.feature [new file with mode: 0644]
mod/assign/tests/events_test.php
mod/assign/upgrade.txt
mod/book/edit.php
mod/book/edit_form.php
mod/book/lang/en/book.php
mod/book/locallib.php
mod/book/tests/behat/create_chapters.feature
mod/book/tests/behat/reorganize_chapters.feature
mod/book/tests/behat/show_hide_chapters.feature
mod/book/tool/print/classes/output/print_book_chapter_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/print_book_page.php [new file with mode: 0644]
mod/book/tool/print/classes/output/renderer.php [new file with mode: 0644]
mod/book/tool/print/index.php
mod/book/tool/print/locallib.php
mod/book/tool/print/print.css
mod/book/tool/print/templates/print_book.mustache [new file with mode: 0644]
mod/book/tool/print/templates/print_book_chapter.mustache [new file with mode: 0644]
mod/data/lib.php
mod/forum/index.php
mod/forum/lib.php
mod/forum/post.php
mod/forum/tests/behat/app_basic_usage.feature [new file with mode: 0644]
mod/forum/tests/behat/posts_ordering_general.feature
mod/forum/tests/lib_test.php
mod/glossary/lib.php
mod/lesson/lib.php
mod/lesson/locallib.php
mod/lesson/tests/lib_test.php
mod/lesson/tests/locallib_test.php
mod/lti/service/gradebookservices/classes/local/service/gradebookservices.php
mod/quiz/amd/build/repaginate.min.js [new file with mode: 0644]
mod/quiz/amd/src/repaginate.js [new file with mode: 0644]
mod/quiz/attemptlib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/lib.php
mod/quiz/tests/attempt_test.php
mod/quiz/tests/lib_test.php
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-debug.js [deleted file]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate-min.js [deleted file]
mod/quiz/yui/build/moodle-mod_quiz-repaginate/moodle-mod_quiz-repaginate.js [deleted file]
mod/quiz/yui/src/repaginate/build.json [deleted file]
mod/quiz/yui/src/repaginate/js/repaginate.js [deleted file]
mod/quiz/yui/src/repaginate/meta/repaginate.json [deleted file]
mod/scorm/lib.php
mod/scorm/locallib.php
mod/scorm/tests/lib_test.php
mod/workshop/lib.php
portfolio/classes/privacy/provider.php
privacy/classes/local/request/moodle_content_writer.php
question/amd/build/qbankmanager.min.js [new file with mode: 0644]
question/amd/src/qbankmanager.js [new file with mode: 0644]
question/behaviour/adaptive/tests/walkthrough_test.php
question/behaviour/behaviourbase.php
question/behaviour/interactivecountback/tests/walkthrough_test.php
question/behaviour/manualgraded/tests/walkthrough_test.php
question/category_class.php
question/classes/bank/checkbox_column.php
question/classes/bank/tags_action_column.php
question/classes/bank/view.php
question/engine/lib.php
question/engine/tests/helpers.php
question/engine/tests/questionutils_test.php
question/tests/behat/question_categories.feature
question/tests/behat/question_categories_idnumber.feature
question/tests/behat/select_questions.feature [new file with mode: 0644]
question/type/ddimageortext/amd/build/question.min.js
question/type/ddimageortext/amd/src/question.js
question/type/ddimageortext/edit_ddimageortext_form.php
question/type/ddmarker/amd/build/question.min.js
question/type/ddmarker/amd/src/question.js
question/type/ddwtos/tests/behat/edit.feature
question/type/gapselect/edit_form_base.php
question/type/gapselect/lang/en/qtype_gapselect.php
question/type/gapselect/renderer.php
question/type/gapselect/rendererbase.php
question/type/gapselect/tests/helper.php
question/type/gapselect/tests/walkthrough_test.php
question/type/match/tests/walkthrough_test.php
question/type/multianswer/tests/walkthrough_test.php
question/type/numerical/tests/walkthrough_test.php
question/type/randomsamatch/tests/walkthrough_test.php
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js [deleted file]
question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js [deleted file]
question/yui/src/qbankmanager/build.json [deleted file]
question/yui/src/qbankmanager/js/qbankmanager.js [deleted file]
question/yui/src/qbankmanager/meta/qbankmanager.json [deleted file]
report/completion/index.php
report/insights/templates/insight.mustache
report/insights/templates/insight_details.mustache
report/insights/templates/insights_list.mustache
report/progress/index.php
report/security/locallib.php
repository/classes/privacy/provider.php
repository/equella/lib.php
repository/onedrive/classes/privacy/provider.php
theme/boost/layout/columns2.php
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/loginform.mustache
theme/boost/templates/core/navbar.mustache
theme/boost/templates/core_form/element-password.mustache
theme/boost/templates/flat_navigation.mustache
theme/boost/templates/footer.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/navbar-secure.mustache
theme/boost/templates/secure.mustache
user/classes/privacy/provider.php
user/editadvanced_form.php
user/editlib.php
user/language_form.php
user/lib.php
user/profile/field/checkbox/classes/privacy/provider.php
user/profile/field/datetime/classes/privacy/provider.php
user/profile/field/menu/classes/privacy/provider.php
user/profile/field/text/classes/privacy/provider.php
user/profile/field/textarea/classes/privacy/provider.php
user/tests/behat/behat_user.php
user/tests/behat/input-purpose.feature [new file with mode: 0644]
version.php
webservice/classes/privacy/provider.php

index e04c319..c34a437 100644 (file)
@@ -823,9 +823,11 @@ if (isset($SESSION->pluginuninstallreturn)) {
 // Print default admin page with notifications.
 $errorsdisplayed = defined('WARN_DISPLAY_ERRORS_ENABLED');
 
-// We make the assumption that at least one schedule task should run once per day.
-$lastcron = $DB->get_field_sql('SELECT MAX(lastruntime) FROM {task_scheduled}');
+$lastcron = get_config('tool_task', 'lastcronstart');
 $cronoverdue = ($lastcron < time() - 3600 * 24);
+$lastcroninterval = get_config('tool_task', 'lastcroninterval');
+$expectedfrequency = $CFG->expectedcronfrequency ?? 200;
+$croninfrequent = !$cronoverdue && ($lastcroninterval > $expectedfrequency || $lastcron < time() - $expectedfrequency);
 $dbproblems = $DB->diagnose();
 $maintenancemode = !empty($CFG->maintenance_enabled);
 
@@ -886,4 +888,4 @@ $output = $PAGE->get_renderer('core', 'admin');
 echo $output->admin_notifications_page($maturity, $insecuredataroot, $errorsdisplayed, $cronoverdue, $dbproblems,
                                        $maintenancemode, $availableupdates, $availableupdatesfetch, $buggyiconvnomb,
                                        $registered, $cachewarnings, $eventshandlers, $themedesignermode, $devlibdir,
-                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl);
+                                       $mobileconfigured, $overridetossl, $invalidforgottenpasswordurl, $croninfrequent);
index da59ef6..57eaa0e 100644 (file)
@@ -281,6 +281,7 @@ class core_admin_renderer extends plugin_renderer_base {
      * @param bool $mobileconfigured Whether the mobile web services have been enabled
      * @param bool $overridetossl Whether or not ssl is being forced.
      * @param bool $invalidforgottenpasswordurl Whether the forgotten password URL does not link to a valid URL.
+     * @param bool $croninfrequent If true, warn that cron hasn't run in the past few minutes
      *
      * @return string HTML to output.
      */
@@ -288,7 +289,7 @@ class core_admin_renderer extends plugin_renderer_base {
             $cronoverdue, $dbproblems, $maintenancemode, $availableupdates, $availableupdatesfetch,
             $buggyiconvnomb, $registered, array $cachewarnings = array(), $eventshandlers = 0,
             $themedesignermode = false, $devlibdir = false, $mobileconfigured = false,
-            $overridetossl = false, $invalidforgottenpasswordurl = false) {
+            $overridetossl = false, $invalidforgottenpasswordurl = false, $croninfrequent = false) {
         global $CFG;
         $output = '';
 
@@ -302,6 +303,7 @@ class core_admin_renderer extends plugin_renderer_base {
         $output .= $this->display_errors_warning($errorsdisplayed);
         $output .= $this->buggy_iconv_warning($buggyiconvnomb);
         $output .= $this->cron_overdue_warning($cronoverdue);
+        $output .= $this->cron_infrequent_warning($croninfrequent);
         $output .= $this->db_problems($dbproblems);
         $output .= $this->maintenance_mode_warning($maintenancemode);
         $output .= $this->overridetossl_warning($overridetossl);
@@ -614,6 +616,24 @@ class core_admin_renderer extends plugin_renderer_base {
                 $this->help_icon('cron', 'admin'));
     }
 
+    /**
+     * Render an appropriate message if cron is not being run frequently (recommended every minute).
+     *
+     * @param bool $croninfrequent
+     * @return string HTML to output.
+     */
+    public function cron_infrequent_warning(bool $croninfrequent) : string {
+        global $CFG;
+
+        if (!$croninfrequent) {
+            return '';
+        }
+
+        $expectedfrequency = $CFG->expectedcronfrequency ?? 200;
+        return $this->warning(get_string('croninfrequent', 'admin', $expectedfrequency) . '&nbsp;' .
+                $this->help_icon('cron', 'admin'));
+    }
+
     /**
      * Render an appropriate message if there are any problems with the DB set-up.
      * @param bool $dbproblems
index e88125b..83a19af 100644 (file)
@@ -109,6 +109,17 @@ require_once(__DIR__ . '/../../../../lib/behat/lib.php');
 require_once(__DIR__ . '/../../../../lib/behat/classes/behat_command.php');
 require_once(__DIR__ . '/../../../../lib/behat/classes/behat_config_manager.php');
 
+// Remove error handling overrides done in config.php. This is consistent with admin/tool/behat/cli/util_single_run.php.
+$CFG->debug = (E_ALL | E_STRICT);
+$CFG->debugdisplay = 1;
+error_reporting($CFG->debug);
+ini_set('display_errors', '1');
+ini_set('log_errors', '1');
+
+// Import the necessary libraries.
+require_once($CFG->libdir . '/setuplib.php');
+require_once($CFG->libdir . '/behat/classes/util.php');
+
 // For drop option check if parallel site.
 if ((empty($options['parallel'])) && ($options['drop']) || $options['updatesteps']) {
     $options['parallel'] = behat_config_manager::get_behat_run_config_value('parallel');
index 2e98656..967fde3 100644 (file)
@@ -218,7 +218,7 @@ if ($options['install']) {
     // Run behat command to get steps in feature files.
     $featurestepscmd = behat_command::get_behat_command(true);
     $featurestepscmd .= ' --config ' . behat_config_manager::get_behat_cli_config_filepath();
-    $featurestepscmd .= ' --dry-run --format=moodle_step_count';
+    $featurestepscmd .= ' --dry-run --format=moodle_stepcount';
     $processes = cli_execute_parallel(array($featurestepscmd), __DIR__ . "/../../../../");
     $status = print_update_step_output(array_pop($processes), $behatstepfile);
 
index f2cd279..88e3365 100644 (file)
@@ -24,6 +24,7 @@
 
 $string['aim'] = 'This administration tool helps developers and test writers to create .feature files describing Moodle\'s functionalities and run them automatically. Step definitions available for use in .feature files are listed below.';
 $string['allavailablesteps'] = 'All available step definitions';
+$string['errorapproot'] = '$CFG->behat_ionic_dirroot is not pointing to a valid Moodle Mobile developer install.';
 $string['errorbehatcommand'] = 'Error running behat CLI command. Try running "{$a} --help" manually from CLI to find out more about the problem.';
 $string['errorcomposer'] = 'Composer dependencies are not installed.';
 $string['errordataroot'] = '$CFG->behat_dataroot is not set or is invalid.';
index 85f52de..8e4f776 100644 (file)
@@ -28,6 +28,6 @@ Feature: Forms manipulation
     When I expand all fieldsets
     Then I should see "Close the quiz"
     And I should see "Group mode"
-    And I should see "Grouping"
+    And I should see "ID number"
     And I should not see "Show more..." in the "region-main" "region"
     And I should see "Show less..."
index f2c60aa..ed94462 100644 (file)
@@ -65,12 +65,12 @@ class tool_filetypes_form extends moodleform {
         $mform->addElement('text', 'description',  get_string('description', 'tool_filetypes'));
         $mform->setType('description', PARAM_TEXT);
         $mform->addHelpButton('description', 'description', 'tool_filetypes');
-        $mform->disabledIf('description', 'descriptiontype', 'ne', 'custom');
+        $mform->hideIf('description', 'descriptiontype', 'ne', 'custom');
 
         $mform->addElement('text', 'corestring',  get_string('corestring', 'tool_filetypes'));
         $mform->setType('corestring', PARAM_ALPHANUMEXT);
         $mform->addHelpButton('corestring', 'corestring', 'tool_filetypes');
-        $mform->disabledIf('corestring', 'descriptiontype', 'ne', 'lang');
+        $mform->hideIf('corestring', 'descriptiontype', 'ne', 'lang');
 
         $mform->addElement('checkbox', 'defaulticon',  get_string('defaulticon', 'tool_filetypes'));
         $mform->addHelpButton('defaulticon', 'defaulticon', 'tool_filetypes');
diff --git a/admin/tool/langimport/classes/locale.php b/admin/tool/langimport/classes/locale.php
new file mode 100644 (file)
index 0000000..69c43b6
--- /dev/null
@@ -0,0 +1,84 @@
+<?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 for the language import tool.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace tool_langimport;
+
+use coding_exception;
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Helper class for the language import tool.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale {
+    /**
+     * Checks availability of locale on current operating system.
+     *
+     * @param string $langpackcode E.g.: en, es, fr, de.
+     * @return bool TRUE if the locale is available on OS.
+     * @throws coding_exception when $langpackcode parameter is a non-empty string.
+     */
+    public function check_locale_availability(string $langpackcode) : bool {
+        global $CFG;
+
+        if (empty($langpackcode)) {
+            throw new coding_exception('Invalid language pack code in \\'.__METHOD__.'() call, only non-empty string is allowed');
+        }
+
+        // Fetch the correct locale based on ostype.
+        if ($CFG->ostype === 'WINDOWS') {
+            $stringtofetch = 'localewin';
+        } else {
+            $stringtofetch = 'locale';
+        }
+
+        // Store current locale.
+        $currentlocale = $this->set_locale(LC_ALL, 0);
+
+        $locale = get_string_manager()->get_string($stringtofetch, 'langconfig', $a = null, $langpackcode);
+
+        // Try to set new locale.
+        $return = $this->set_locale(LC_ALL, $locale);
+
+        // Restore current locale.
+        $this->set_locale(LC_ALL, $currentlocale);
+
+        // If $return is not equal to false, it means that setlocale() succeed to change locale.
+        return $return !== false;
+    }
+
+    /**
+     * Wrap for the native PHP function setlocale().
+     *
+     * @param int $category Specifying the category of the functions affected by the locale setting.
+     * @param string $locale E.g.: en_AU.utf8, en_GB.utf8, es_ES.utf8, fr_FR.utf8, de_DE.utf8.
+     * @return string|false Returns the new current locale, or FALSE on error.
+     */
+    protected function set_locale(int $category = LC_ALL, string $locale = '0') {
+        return setlocale($category, $locale);
+    }
+}
index 92a09da..1180abc 100644 (file)
@@ -109,9 +109,16 @@ echo $OUTPUT->header();
 echo $OUTPUT->heading(get_string('langimport', 'tool_langimport'));
 
 $installedlangs = get_string_manager()->get_list_of_translations(true);
+$locale = new \tool_langimport\locale();
 
+$missinglocales = '';
 $missingparents = array();
-foreach ($installedlangs as $installedlang => $unused) {
+foreach ($installedlangs as $installedlang => $langpackname) {
+    // Check locale availability.
+    if (!$locale->check_locale_availability($installedlang)) {
+        $missinglocales .= '<li>'.$langpackname.'</li>';
+    }
+
     $parent = get_parent_language($installedlang);
     if (empty($parent)) {
         continue;
@@ -121,6 +128,14 @@ foreach ($installedlangs as $installedlang => $unused) {
     }
 }
 
+if (!empty($missinglocales)) {
+    // There is at least one missing locale.
+    $a = new stdClass();
+    $a->globallocale = moodle_getlocale();
+    $a->missinglocales = $missinglocales;
+    $controller->errors[] = get_string('langunsupported', 'tool_langimport', $a);
+}
+
 if ($availablelangs = $controller->availablelangs) {
     $remote = true;
 } else {
index faeb02c..27739ed 100644 (file)
@@ -37,6 +37,7 @@ $string['langpackupdateskipped'] = 'Update of \'{$a}\' language pack skipped';
 $string['langpackuptodate'] = 'Language pack \'{$a}\' is up-to-date';
 $string['langpackupdated'] = 'Language pack \'{$a}\' was successfully updated';
 $string['langpackupdatedevent'] = 'Language pack updated';
+$string['langunsupported'] = '<p>Your server does not seem to fully support the following languages:</p><ul>{$a->missinglocales}</ul><p>Instead, the global locale ({$a->globallocale}) will be used to format certain strings such as dates or numbers.</p>';
 $string['langupdatecomplete'] = 'Language pack update completed';
 $string['missingcfglangotherroot'] = 'Missing configuration value $CFG->langotherroot';
 $string['missinglangparent'] = 'Missing parent language <em>{$a->parent}</em> of <em>{$a->lang}</em>.';
diff --git a/admin/tool/langimport/tests/locale_test.php b/admin/tool/langimport/tests/locale_test.php
new file mode 100644 (file)
index 0000000..4d1ffef
--- /dev/null
@@ -0,0 +1,71 @@
+<?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/>.
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @package    tool_langimport
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Tests for \tool_langimport\locale class.
+ *
+ * @copyright  2018 Université Rennes 2 {@link https://www.univ-rennes2.fr}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class locale_testcase extends \advanced_testcase {
+    /**
+     * Test that \tool_langimport\locale::check_locale_availability() works as expected.
+     *
+     * @return void
+     */
+    public function test_check_locale_availability() {
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which try to set new 'es' locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', 'es', 'en'));
+
+        // Test what happen when locale is available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertTrue($result);
+
+        // Create a mock of set_locale() method to simulate :
+        // - first setlocale() call which backup current locale
+        // - second setlocale() call which fail to set new locale
+        // - third setlocale() call which restore locale.
+        $mock = $this->getMockBuilder(\tool_langimport\locale::class)
+            ->setMethods(['set_locale'])
+            ->getMock();
+        $mock->method('set_locale')->will($this->onConsecutiveCalls('en', false, 'en'));
+
+        // Test what happen when locale is not available on system.
+        $result = $mock->check_locale_availability('en');
+        $this->assertFalse($result);
+
+        // Test an invalid parameter.
+        $locale = new \tool_langimport\locale();
+        $this->expectException(coding_exception::class);
+        $locale->check_locale_availability('');
+    }
+}
index 77cf893..f798e7d 100644 (file)
@@ -420,6 +420,9 @@ class external extends external_api {
                     ))
                 ),
                 'comppath' => competency_path_exporter::get_read_structure(),
+                'plans' => new external_multiple_structure(
+                    plan_exporter::get_read_structure()
+                ),
             ))),
             'manageurl' => new external_value(PARAM_LOCALURL, 'Url to the manage competencies page.'),
         ));
index d1b7b7f..78ea24a 100644 (file)
@@ -26,11 +26,13 @@ defined('MOODLE_INTERNAL') || die();
 
 use core_competency\api;
 use core_competency\user_competency;
+use core_competency\external\plan_exporter;
 use core_course\external\course_module_summary_exporter;
 use core_course\external\course_summary_exporter;
 use context_course;
 use renderer_base;
 use stdClass;
+use moodle_url;
 
 /**
  * Class for exporting user competency data with additional related data in a plan.
@@ -62,7 +64,14 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
             'coursemodules' => array(
                 'type' => course_module_summary_exporter::read_properties_definition(),
                 'multiple' => true
-            )
+            ),
+            'plans' => array(
+                'type' => plan_exporter::read_properties_definition(),
+                'multiple' => true
+            ),
+            'pluginbaseurl' => [
+                'type' => PARAM_URL
+            ],
         );
     }
 
@@ -95,6 +104,16 @@ class user_competency_summary_in_course_exporter extends \core\external\exporter
         }
         $result->coursemodules = $exportedmodules;
 
+        // User learning plans.
+        $plans = api::list_plans_with_competency($this->related['user']->id, $this->related['competency']);
+        $exportedplans = array();
+        foreach ($plans as $plan) {
+            $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+            $exportedplans[] = $planexporter->export($output);
+        }
+        $result->plans = $exportedplans;
+        $result->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
+
         return (array) $result;
     }
 }
index eaf7d34..218d8a5 100644 (file)
@@ -41,6 +41,7 @@ use core_competency\external\course_competency_exporter;
 use core_competency\external\course_competency_settings_exporter;
 use core_competency\external\user_competency_course_exporter;
 use core_competency\external\user_competency_exporter;
+use core_competency\external\plan_exporter;
 use tool_lp\external\competency_path_exporter;
 use tool_lp\external\course_competency_statistics_exporter;
 use core_course\external\course_module_summary_exporter;
@@ -113,6 +114,7 @@ class course_competencies_page implements renderable, templatable {
         $data->courseid = $this->courseid;
         $data->pagecontextid = $this->context->id;
         $data->competencies = array();
+        $data->pluginbaseurl = (new moodle_url('/admin/tool/lp'))->out(true);
 
         $gradable = is_enrolled($this->context, $USER, 'moodle/competency:coursecompetencygradable');
         if ($gradable) {
@@ -154,12 +156,21 @@ class course_competencies_page implements renderable, templatable {
                 'context' => $context
             ]);
 
+            // User learning plans.
+            $plans = api::list_plans_with_competency($USER->id, $competency);
+            $exportedplans = array();
+            foreach ($plans as $plan) {
+                $planexporter = new plan_exporter($plan, array('template' => $plan->get_template()));
+                $exportedplans[] = $planexporter->export($output);
+            }
+
             $onerow = array(
                 'competency' => $compexporter->export($output),
                 'coursecompetency' => $ccexporter->export($output),
                 'ruleoutcomeoptions' => $ccoutcomeoptions,
                 'coursemodules' => $exportedmodules,
-                'comppath' => $pathexporter->export($output)
+                'comppath' => $pathexporter->export($output),
+                'plans' => $exportedplans
             );
             if ($gradable) {
                 $foundusercompetencycourse = false;
index eaa0f43..6d89b20 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
+    @template tool_lp/course_competencies_page
+
     Course competencies template.
 
     For a full list of the context for this template see the course_competencies_page renderable.
+
+    This template includes ajax functionality, so it cannot be shown in the template library.
 }}
 <div data-region="coursecompetenciespage">
     <div data-region="actions" class="clearfix">
@@ -66,7 +70,7 @@
         <div class="clearfix"></div>
         {{/canmanagecoursecompetencies}}
         {{#competency}}
-            <a href="{{pluginbaseurl}}user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
+            <a href="{{pluginbaseurl}}/user_competency_in_course.php?courseid={{courseid}}&competencyid={{competency.id}}&userid={{gradableuserid}}"
                    id="competency-info-link-{{competency.id}}"
                    title="{{#str}}viewdetails, tool_lp{{/str}}">
                 <p><strong>{{{competency.shortname}}} <em>{{competency.idnumber}}</em></strong></p>
         {{/canmanagecoursecompetencies}}
         <div data-region="coursecompetencyactivities">
         <p>
-        <ul class="inline list-inline">
+        <strong>{{#str}}activities{{/str}}</strong>
+        <ul class="inline list-inline p-2">
         {{#coursemodules}}
             <li class="list-inline-item"><a href="{{url}}"><img src="{{iconurl}}"> {{name}} </a></li>
         {{/coursemodules}}
         {{^coursemodules}}
-            <li class="list-inline-item"><span class="alert">{{#str}}noactivities, tool_lp{{/str}}</span></li>
+            <li class="list-inline-item">{{#str}}noactivities, tool_lp{{/str}}</li>
         {{/coursemodules}}
         </ul>
         </p>
         </div>
+        <div data-region="learningplans">
+        <p>
+        <strong>{{#str}}userplans, core_competency{{/str}}</strong>
+        <ul class="inline list-inline p-2">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li class="list-inline-item">{{#str}}nouserplanswithcompetency, core_competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </div>
     </td>
     </tr>
 {{/competencies}}
index 22cf96d..ce80bbb 100644 (file)
         </dd>
         {{/user}}
         {{/displayuser}}
+        <dt>{{#str}}userplans, competency{{/str}}</dt>
+        <dd>
+        <p>
+        <ul class="inline list-inline">
+        {{#plans}}
+            <li class="list-inline-item"><a href="{{pluginbaseurl}}/plan.php?id={{id}}">{{{name}}}</a></li>
+        {{/plans}}
+        {{^plans}}
+            <li>{{#str}}nouserplanswithcompetency, competency{{/str}}</li>
+        {{/plans}}
+        </ul>
+        </p>
+        </dd>
         {{#usercompetencycourse}}
         <dt>{{#str}}proficient, tool_lp{{/str}}</dt>
         <dd>
index d2ca869..0fbbfd1 100644 (file)
@@ -130,3 +130,35 @@ Feature: Manage plearning plan
     When I click on "Delete" "button" in the "Confirm" "dialogue"
     And I wait until the page is ready
     Then I should not see "Science plan Year-4"
+
+  Scenario: See a learning plan from a course
+    Given the following lp "plans" exist:
+      | name | user | description |
+      | Science plan Year-manage | admin | science plan description |
+    And the following lp "frameworks" exist:
+      | shortname | idnumber |
+      | Framework 1 | sc-y-2 |
+    And the following lp "competencies" exist:
+      | shortname | framework |
+      | comp1 | sc-y-2 |
+      | comp2 | sc-y-2 |
+    And I follow "Learning plans"
+    And I should see "Science plan Year-manage"
+    And I follow "Science plan Year-manage"
+    And I should see "Add competency"
+    And I press "Add competency"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    When I click on "Add" "button" in the "Competency picker" "dialogue"
+    Then "comp1" "table_row" should exist
+    And I create a course with:
+      | Course full name | New course fullname |
+      | Course short name | New course shortname |
+    And I follow "New course fullname"
+    And I follow "Competencies"
+    And I press "Add competencies to course"
+    And "Competency picker" "dialogue" should be visible
+    And I select "comp1" of the competency tree
+    And I click on "Add" "button" in the "Competency picker" "dialogue"
+    And I should see "Learning plans"
+    And I should see "Science plan Year-manage"
index 868fe6a..38aa6be 100644 (file)
@@ -72,26 +72,26 @@ class tool_uploadcourse_base_form extends moodleform {
         );
         $mform->addElement('select', 'options[updatemode]', get_string('updatemode', 'tool_uploadcourse'), $choices);
         $mform->setDefault('options[updatemode]', tool_uploadcourse_processor::UPDATE_NOTHING);
-        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[updatemode]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[updatemode]', 'updatemode', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowdeletes]', get_string('allowdeletes', 'tool_uploadcourse'));
         $mform->setDefault('options[allowdeletes]', 0);
-        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowdeletes]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowdeletes]', 'allowdeletes', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowrenames]', get_string('allowrenames', 'tool_uploadcourse'));
         $mform->setDefault('options[allowrenames]', 0);
-        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowrenames]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowrenames]', 'allowrenames', 'tool_uploadcourse');
 
         $mform->addElement('selectyesno', 'options[allowresets]', get_string('allowresets', 'tool_uploadcourse'));
         $mform->setDefault('options[allowresets]', 0);
-        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[allowresets]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->addHelpButton('options[allowresets]', 'allowresets', 'tool_uploadcourse');
     }
 
index 89ee15d..58c39d4 100644 (file)
@@ -57,8 +57,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
             'maxlength="100" size="20"');
         $mform->setType('options[shortnametemplate]', PARAM_RAW);
         $mform->addHelpButton('options[shortnametemplate]', 'shortnametemplate', 'tool_uploadcourse');
-        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
-        $mform->disabledIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
+        $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_OR_UPDATE);
+        $mform->hideIf('options[shortnametemplate]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_UPDATE_ONLY);
 
         // Restore file is not in the array options on purpose, because formslib can't handle it!
         $contextid = $this->_customdata['contextid'];
@@ -73,8 +73,8 @@ class tool_uploadcourse_step2_form extends tool_uploadcourse_base_form {
 
         $mform->addElement('selectyesno', 'options[reset]', get_string('reset', 'tool_uploadcourse'));
         $mform->setDefault('options[reset]', 0);
-        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
-        $mform->disabledIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
+        $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_NEW);
+        $mform->hideIf('options[reset]', 'options[mode]', 'eq', tool_uploadcourse_processor::MODE_CREATE_ALL);
         $mform->disabledIf('options[reset]', 'options[allowresets]', 'eq', 0);
         $mform->addHelpButton('options[reset]', 'reset', 'tool_uploadcourse');
 
index c0eb764..558f324 100644 (file)
@@ -95,7 +95,7 @@ class admin_uploaduser_form2 extends moodleform {
         $choices = array(0 => get_string('infilefield', 'auth'), 1 => get_string('createpasswordifneeded', 'auth'));
         $mform->addElement('select', 'uupasswordnew', get_string('uupasswordnew', 'tool_uploaduser'), $choices);
         $mform->setDefault('uupasswordnew', 1);
-        $mform->disabledIf('uupasswordnew', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('uupasswordnew', 'uutype', 'eq', UU_USER_UPDATE);
 
         $choices = array(UU_UPDATE_NOCHANGES    => get_string('nochanges', 'tool_uploaduser'),
                          UU_UPDATE_FILEOVERRIDE => get_string('uuupdatefromfile', 'tool_uploaduser'),
@@ -103,16 +103,16 @@ class admin_uploaduser_form2 extends moodleform {
                          UU_UPDATE_MISSING      => get_string('uuupdatemissing', 'tool_uploaduser'));
         $mform->addElement('select', 'uuupdatetype', get_string('uuupdatetype', 'tool_uploaduser'), $choices);
         $mform->setDefault('uuupdatetype', UU_UPDATE_NOCHANGES);
-        $mform->disabledIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuupdatetype', 'uutype', 'eq', UU_USER_ADDINC);
 
         $choices = array(0 => get_string('nochanges', 'tool_uploaduser'), 1 => get_string('update'));
         $mform->addElement('select', 'uupasswordold', get_string('uupasswordold', 'tool_uploaduser'), $choices);
         $mform->setDefault('uupasswordold', 0);
-        $mform->disabledIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDINC);
-        $mform->disabledIf('uupasswordold', 'uuupdatetype', 'eq', 0);
-        $mform->disabledIf('uupasswordold', 'uuupdatetype', 'eq', 3);
+        $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uupasswordold', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 0);
+        $mform->hideIf('uupasswordold', 'uuupdatetype', 'eq', 3);
 
         $choices = array(UU_PWRESET_WEAK => get_string('usersweakpassword', 'tool_uploaduser'),
                          UU_PWRESET_NONE => get_string('none'),
@@ -125,18 +125,18 @@ class admin_uploaduser_form2 extends moodleform {
 
         $mform->addElement('selectyesno', 'uuallowrenames', get_string('allowrenames', 'tool_uploaduser'));
         $mform->setDefault('uuallowrenames', 0);
-        $mform->disabledIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowrenames', 'uutype', 'eq', UU_USER_ADDINC);
 
         $mform->addElement('selectyesno', 'uuallowdeletes', get_string('allowdeletes', 'tool_uploaduser'));
         $mform->setDefault('uuallowdeletes', 0);
-        $mform->disabledIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowdeletes', 'uutype', 'eq', UU_USER_ADDINC);
 
         $mform->addElement('selectyesno', 'uuallowsuspends', get_string('allowsuspends', 'tool_uploaduser'));
         $mform->setDefault('uuallowsuspends', 1);
-        $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
-        $mform->disabledIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
+        $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDNEW);
+        $mform->hideIf('uuallowsuspends', 'uutype', 'eq', UU_USER_ADDINC);
 
         if (!empty($CFG->allowaccountssameemail)) {
             $mform->addElement('selectyesno', 'uunoemailduplicates', get_string('uunoemailduplicates', 'tool_uploaduser'));
@@ -209,14 +209,14 @@ class admin_uploaduser_form2 extends moodleform {
         $mform->addElement('text', 'username', get_string('uuusernametemplate', 'tool_uploaduser'), 'size="20"');
         $mform->setType('username', PARAM_RAW); // No cleaning here. The process verifies it later.
         $mform->addRule('username', get_string('requiredtemplate', 'tool_uploaduser'), 'required', null, 'client');
-        $mform->disabledIf('username', 'uutype', 'eq', UU_USER_ADD_UPDATE);
-        $mform->disabledIf('username', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('username', 'uutype', 'eq', UU_USER_ADD_UPDATE);
+        $mform->hideIf('username', 'uutype', 'eq', UU_USER_UPDATE);
         $mform->setForceLtr('username');
 
         $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
         $mform->setType('email', PARAM_RAW); // No cleaning here. The process verifies it later.
-        $mform->disabledIf('email', 'uutype', 'eq', UU_USER_ADD_UPDATE);
-        $mform->disabledIf('email', 'uutype', 'eq', UU_USER_UPDATE);
+        $mform->hideIf('email', 'uutype', 'eq', UU_USER_ADD_UPDATE);
+        $mform->hideIf('email', 'uutype', 'eq', UU_USER_UPDATE);
         $mform->setForceLtr('email');
 
         // only enabled and known to work plugins
index ffbd787..3f38f0a 100644 (file)
@@ -101,7 +101,7 @@ class block extends base {
      * @param   MoodleQuickForm $mform      The form to add configuration to.
      */
     public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) {
-        $mform->disabledIf('targetvalue_block', 'targettype', 'noteq',
+        $mform->hideIf('targetvalue_block', 'targettype', 'noteq',
                 \tool_usertours\target::get_target_constant_for_class(get_class()));
     }
 
index ae6f2fc..3e3fd58 100644 (file)
@@ -91,7 +91,7 @@ class selector extends base {
      * @param   MoodleQuickForm $mform      The form to add configuration to.
      */
     public static function add_disabled_constraints_to_form(\MoodleQuickForm $mform) {
-        $mform->disabledIf('targetvalue_selector', 'targettype', 'noteq',
+        $mform->hideIf('targetvalue_selector', 'targettype', 'noteq',
                 \tool_usertours\target::get_target_constant_for_class(get_class()));
     }
 
index 6a4b078..696efe5 100644 (file)
@@ -84,7 +84,7 @@ class unattached extends base {
         $myvalue = \tool_usertours\target::get_target_constant_for_class(get_class());
 
         foreach (array_keys(self::$forcedsettings) as $settingname) {
-            $mform->disabledIf($settingname, 'targettype', 'eq', $myvalue);
+            $mform->hideIf($settingname, 'targettype', 'eq', $myvalue);
         }
     }
 
diff --git a/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php b/admin/tool/xmldb/actions/add_persistent_mandatory/add_persistent_mandatory.class.php
new file mode 100644 (file)
index 0000000..a323c1e
--- /dev/null
@@ -0,0 +1,154 @@
+<?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/>.
+
+/**
+ * @package    tool_xmldb
+ * @copyright  2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Add the mandatory fields for persistent to the table.
+ *
+ * @package    tool_xmldb
+ * @copyright  2019 Michael Aherne
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class add_persistent_mandatory extends XMLDBAction {
+
+    function init() {
+
+        parent::init();
+
+        // Get needed strings.
+        $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
+            'persistentfieldsconfirm' => 'tool_xmldb',
+            'persistentfieldscomplete' => 'tool_xmldb',
+            'persistentfieldsexist' => 'tool_xmldb',
+            'back' => 'core'
+        ));
+
+    }
+
+    function getTitle() {
+        return $this->str['addpersistent'];
+    }
+
+    function invoke() {
+
+        parent::invoke();
+
+        $this->does_generate = ACTION_GENERATE_HTML;
+
+        global $CFG, $XMLDB, $OUTPUT;
+
+        $dir = required_param('dir', PARAM_PATH);
+        $dirpath = $CFG->dirroot . $dir;
+
+        if (empty($XMLDB->dbdirs)) {
+            return false;
+        }
+
+        if (!empty($XMLDB->editeddirs)) {
+            $editeddir = $XMLDB->editeddirs[$dirpath];
+            $structure = $editeddir->xml_file->getStructure();
+        }
+
+        $tableparam = required_param('table', PARAM_ALPHANUMEXT);
+
+        /** @var xmldb_table $table */
+        $table = $structure->getTable($tableparam);
+
+        $result = true;
+        // Launch postaction if exists (leave this here!)
+        if ($this->getPostAction() && $result) {
+            return $this->launch($this->getPostAction());
+        }
+
+        $confirm = optional_param('confirm', false, PARAM_BOOL);
+
+        $fields = ['usermodified', 'timecreated', 'timemodified'];
+        $existing = [];
+        foreach ($fields as $field) {
+            if ($table->getField($field)) {
+                $existing[] = $field;
+            }
+        }
+
+        $returnurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'table' => $tableparam,
+            'dir' => $dir,
+            'action' => 'edit_table'
+        ]);
+
+        $backbutton = html_writer::link($returnurl, '[' . $this->str['back'] . ']');
+        $actionbuttons = html_writer::tag('p', $backbutton, ['class' => 'centerpara buttons']);
+
+        if (!$confirm) {
+
+            if (!empty($existing)) {
+
+                $message = html_writer::span($this->str['persistentfieldsexist']);
+                $message .= html_writer::alist($existing);
+                $this->output .= $OUTPUT->notification($message);
+
+                if (count($existing) == count($fields)) {
+                    $this->output .= $actionbuttons;
+                    return true;
+                }
+            }
+
+            $confirmurl = new \moodle_url('/admin/tool/xmldb/index.php', [
+                'table' => $tableparam,
+                'dir' => $dir,
+                'action' => 'add_persistent_mandatory',
+                'sesskey' => sesskey(),
+                'confirm' => '1'
+            ]);
+
+            $message = html_writer::span($this->str['persistentfieldsconfirm']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->confirm($message, $confirmurl, $returnurl);
+
+        } else {
+
+            $fieldsadded = [];
+            foreach ($fields as $field) {
+                if (!in_array($field, $existing)) {
+                    $fieldsadded[] = $field;
+                    $table->add_field($field, XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, 0);
+                }
+            }
+
+            if (!$table->getKey('usermodified')) {
+                $table->add_key('usermodified', XMLDB_KEY_FOREIGN, ['usermodified'], 'user', ['id']);
+            }
+
+            $structure->setVersion(userdate(time(), '%Y%m%d', 99, false));
+            $structure->setChanged(true);
+
+            $message = html_writer::span($this->str['persistentfieldscomplete']);
+            $message .= html_writer::alist(array_diff($fields, $existing));
+            $this->output .= $OUTPUT->notification($message, 'success');
+
+            $this->output .= $actionbuttons;
+        }
+
+        return $result;
+    }
+
+}
index 5cd4bcd..aa82f99 100644 (file)
@@ -44,6 +44,7 @@ class edit_table extends XMLDBAction {
 
         // Get needed strings
         $this->loadStrings(array(
+            'addpersistent' => 'tool_xmldb',
             'change' => 'tool_xmldb',
             'vieworiginal' => 'tool_xmldb',
             'viewedited' => 'tool_xmldb',
@@ -177,6 +178,15 @@ class edit_table extends XMLDBAction {
         $b .= '<a href="index.php?action=view_table_sql&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' .$this->str['viewsqlcode'] . ']</a>';
         // The view php code button
         $b .= '&nbsp;<a href="index.php?action=view_table_php&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['viewphpcode'] . ']</a>';
+        // The add persistent fields button.
+        $url = new \moodle_url('/admin/tool/xmldb/index.php', [
+            'action' => 'add_persistent_mandatory',
+            'sesskey' => sesskey(),
+            'table' => $tableparam,
+            'dir'=> str_replace($CFG->dirroot, '', $dirpath)
+        ]);
+        $b .= '&nbsp;' . \html_writer::link($url, '[' . $this->str['addpersistent'] . ']');
+
         // The save button (if possible)
         if ($cansavenow) {
             $b .= '&nbsp;<a href="index.php?action=save_xml_file&amp;sesskey=' . sesskey() . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '&amp;time=' . time() . '&amp;unload=false&amp;postaction=edit_table&amp;table=' . $tableparam . '&amp;dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['save'] . ']</a>';
index d531a86..d2efeab 100644 (file)
@@ -23,6 +23,7 @@
  */
 
 $string['actual'] = 'Actual';
+$string['addpersistent'] = 'Add mandatory persistent fields';
 $string['aftertable'] = 'After table:';
 $string['back'] = 'Back';
 $string['backtomainview'] = 'Back to main';
@@ -169,6 +170,9 @@ $string['numberincorrectwholepart'] = 'Too big whole number part for number fiel
 $string['pendingchanges'] = 'Note: You have performed changes to this file. They can be saved at any moment.';
 $string['pendingchangescannotbesaved'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server.';
 $string['pendingchangescannotbesavedreload'] = 'There are changes in this file but they cannot be saved! Please verify that both the directory and the "install.xml" within it have write permissions for the web server. Then reload this page and you should be able to save those changes.';
+$string['persistentfieldsconfirm'] = 'Do you want to add the following fields: ';
+$string['persistentfieldscomplete'] = 'The following fields have been added: ';
+$string['persistentfieldsexist'] = 'The following fields already exist: ';
 $string['pluginname'] = 'XMLDB editor';
 $string['primarykeyonlyallownotnullfields'] = 'Primary keys cannot be null';
 $string['reserved'] = 'Reserved';
index 5eb8434..56e5a7d 100644 (file)
@@ -130,21 +130,10 @@ class auth_plugin_cas extends auth_plugin_ldap {
             }
 
             $authCAS = optional_param('authCAS', '', PARAM_RAW);
-            if ($authCAS == 'NOCAS') {
+            if ($authCAS != 'CAS') {
                 return;
             }
-            // Show authentication form for multi-authentication.
-            // Test pgtIou parameter for proxy mode (https connection in background from CAS server to the php server).
-            if ($authCAS != 'CAS' && !isset($_GET['pgtIou'])) {
-                $PAGE->set_url('/login/index.php');
-                $PAGE->navbar->add($CASform);
-                $PAGE->set_title("$site->fullname: $CASform");
-                $PAGE->set_heading($site->fullname);
-                echo $OUTPUT->header();
-                include($CFG->dirroot.'/auth/cas/cas_form.html');
-                echo $OUTPUT->footer();
-                exit();
-            }
+
         }
 
         // Connection to CAS server
@@ -363,4 +352,35 @@ class auth_plugin_cas extends auth_plugin_ldap {
             phpCAS::logoutWithRedirectService($backurl);
         }
     }
+
+    /**
+     * Return a list of identity providers to display on the login page.
+     *
+     * @param string|moodle_url $wantsurl The requested URL.
+     * @return array List of arrays with keys url, iconurl and name.
+     */
+    public function loginpage_idp_list($wantsurl) {
+        if (empty($this->config->hostname)) {
+            // CAS is not configured.
+            return [];
+        }
+
+        $iconurl = moodle_url::make_pluginfile_url(
+            context_system::instance()->id,
+            'auth_cas',
+            'logo',
+            null,
+            '/',
+            $this->config->auth_logo);
+
+        return [
+            [
+                'url' => new moodle_url(get_login_url(), [
+                        'authCAS' => 'CAS',
+                    ]),
+                'iconurl' => $iconurl,
+                'name' => format_string($this->config->auth_name),
+            ],
+        ];
+    }
 }
diff --git a/auth/cas/cas_form.html b/auth/cas/cas_form.html
deleted file mode 100644 (file)
index 52319a3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="loginbox clearfix">
-<div class="loginpanel">
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=CAS';?>"><?php print_string('accesCAS', 'auth_cas');?></a>
-</div>
-<br/>
-<div>
-<a href="<?php echo get_login_url() . '?authCAS=NOCAS';?>"><?php print_string('accesNOCAS', 'auth_cas');?></a>
-</div>
-</div>
-</div>
index a7c3662..3e465f9 100644 (file)
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['accesCAS'] = 'CAS users';
-$string['accesNOCAS'] = 'other users';
+$string['auth_cas_auth_name'] = 'Authentication method name';
+$string['auth_cas_auth_name_description'] = 'Provide a name for the CAS authentication method that is familiar to your users.';
+$string['auth_cas_auth_logo'] = 'Authentication method logo';
+$string['auth_cas_auth_logo_description'] = 'Provide a logo for the CAS authentication method that is familiar to your users.';
 $string['auth_cas_auth_user_create'] = 'Create users externally';
+$string['auth_cas_auth_service'] = 'CAS';
 $string['auth_cas_baseuri'] = 'URI of the server (nothing if no baseUri)<br />For example, if the CAS server responds to host.domaine.fr/CAS/ then<br />cas_baseuri = CAS/';
 $string['auth_cas_baseuri_key'] = 'Base URI';
 $string['auth_cas_broken_password'] = 'You cannot proceed without changing your password, however there is no available page for changing it. Please contact your Moodle Administrator.';
@@ -75,3 +78,7 @@ $string['noldapserver'] = 'No LDAP server configured for CAS! Syncing disabled.'
 $string['pluginname'] = 'CAS server (SSO)';
 $string['synctask'] = 'CAS users sync job';
 $string['privacy:metadata'] = 'The CAS server (SSO) authentication plugin does not store any personal data.';
+
+// Deprecated since Moodle 3.7.
+$string['accesCAS'] = 'CAS users';
+$string['accesNOCAS'] = 'other users';
diff --git a/auth/cas/lang/en/deprecated.txt b/auth/cas/lang/en/deprecated.txt
new file mode 100644 (file)
index 0000000..6854e8e
--- /dev/null
@@ -0,0 +1,2 @@
+accesCAS,auth_cas
+accesNOCAS,auth_cas
diff --git a/auth/cas/lib.php b/auth/cas/lib.php
new file mode 100644 (file)
index 0000000..7127556
--- /dev/null
@@ -0,0 +1,67 @@
+<?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/>.
+
+/**
+ * Authentication Plugin: CAS Authentication.
+ *
+ * Authentication using CAS (Central Authentication Server).
+ *
+ * @package     auth_cas
+ * @copyright   2018 Fabrice Ménard <menard.fabrice@gmail.com>
+ * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+/**
+ * Serves the logo file settings.
+ *
+ * @param   stdClass $course course object
+ * @param   stdClass $cm course module object
+ * @param   stdClass $context context object
+ * @param   string $filearea file area
+ * @param   array $args extra arguments
+ * @param   bool $forcedownload whether or not force download
+ * @param   array $options additional options affecting the file serving
+ * @return  bool false|void
+ */
+function auth_cas_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = []) {
+    if ($context->contextlevel != CONTEXT_SYSTEM) {
+        return false;
+    }
+
+    if ($filearea !== 'logo' ) {
+        return false;
+    }
+
+    // Extract the filename / filepath from the $args array.
+    $filename = array_pop($args);
+    if (!$args) {
+        $filepath = '/';
+    } else {
+        $filepath = '/' . implode('/', $args) . '/';
+    }
+
+    // Retrieve the file from the Files API.
+    $itemid = 0;
+    $fs = get_file_storage();
+    $file = $fs->get_file($context->id, 'auth_cas', $filearea, $itemid, $filepath, $filename);
+    if (!$file) {
+        return false; // The file does not exist.
+    }
+
+    send_stored_file($file, null, 0, $forcedownload, $options);
+}
index 2bd7434..5434984 100644 (file)
@@ -45,6 +45,20 @@ if ($ADMIN->fulltree) {
         $settings->add(new admin_setting_heading('auth_cas/casserversettings',
                 new lang_string('auth_cas_server_settings', 'auth_cas'), ''));
 
+        // Authentication method name.
+        $settings->add(new admin_setting_configtext('auth_cas/auth_name',
+                get_string('auth_cas_auth_name', 'auth_cas'),
+                get_string('auth_cas_auth_name_description', 'auth_cas'),
+                get_string('auth_cas_auth_service', 'auth_cas'),
+                PARAM_RAW_TRIMMED));
+
+        // Authentication method logo.
+        $opts = array('accepted_types' => array('.png', '.jpg', '.gif', '.webp', '.tiff', '.svg'));
+        $settings->add(new admin_setting_configstoredfile('auth_cas/auth_logo',
+                 get_string('auth_cas_auth_logo', 'auth_cas'),
+                 get_string('auth_cas_auth_logo_description', 'auth_cas'), 'logo', 0, $opts));
+
+
         // Hostname.
         $settings->add(new admin_setting_configtext('auth_cas/hostname',
                 get_string('auth_cas_hostname_key', 'auth_cas'),
index f735c62..7c47f6a 100644 (file)
@@ -26,7 +26,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2018120300;        // The current plugin version (Date: YYYYMMDDXX)
+$plugin->version   = 2018121400;        // The current plugin version (Date: YYYYMMDDXX)
 $plugin->requires  = 2018112800;        // Requires this Moodle version
 $plugin->component = 'auth_cas';        // Full name of the plugin (used for diagnostics)
 
index 6e6c7ff..1052f3a 100644 (file)
@@ -2089,6 +2089,31 @@ class auth_plugin_ldap extends auth_plugin_base {
         return (bool)$user->suspended;
     }
 
+    /**
+     * Test a DN
+     *
+     * @param resource $ldapconn
+     * @param string $dn The DN to check for existence
+     * @param string $message The identifier of a string as in get_string()
+     * @param string|object|array $a An object, string or number that can be used
+     *      within translation strings as in get_string()
+     * @return true or a message in case of error
+     */
+    private function test_dn($ldapconn, $dn, $message, $a = null) {
+        $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array());
+        if (!$ldapresult) {
+            if (ldap_errno($ldapconn) == 32) {
+                // No such object.
+                return get_string($message, 'auth_ldap', $a);
+            }
+
+            $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn));
+            return get_string('diag_genericerror', 'auth_ldap', $a);
+        }
+
+        return true;
+    }
+
     /**
      * Test if settings are correct, print info to output.
      */
@@ -2096,35 +2121,66 @@ class auth_plugin_ldap extends auth_plugin_base {
         global $OUTPUT;
 
         if (!function_exists('ldap_connect')) { // Is php-ldap really there?
-            echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'));
+            echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR);
             return;
         }
 
         // Check to see if this is actually configured.
-        if ((isset($this->config->host_url)) && ($this->config->host_url !== '')) {
+        if (empty($this->config->host_url)) {
+            // LDAP is not even configured.
+            echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR);
+            return;
+        }
+
+        if ($this->config->ldap_version != 3) {
+            echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING);
+        }
+
+        try {
+            $ldapconn = $this->ldap_connect();
+        } catch (Exception $e) {
+            echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR);
+            return;
+        }
 
-            try {
-                $ldapconn = $this->ldap_connect();
-                // Try to connect to the LDAP server.  See if the page size setting is supported on this server.
-                $pagedresultssupported = ldap_paged_results_supported($this->config->ldap_version, $ldapconn);
-            } catch (Exception $e) {
+        // Display paged file results.
+        if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) {
+            echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
+        }
 
-                // If we couldn't connect and get the supported options, we can only assume we don't support paged results.
-                $pagedresultssupported = false;
+        // Check contexts.
+        foreach (explode(';', $this->config->contexts) as $context) {
+            $context = trim($context);
+            if (empty($context)) {
+                echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING);
+                continue;
             }
 
-            // Display paged file results.
-            if ((!$pagedresultssupported)) {
-                echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
-            } else if ($ldapconn) {
-                // We were able to connect successfuly.
-                echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS);
+            $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context);
+            if ($message !== true) {
+                echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
             }
+        }
 
-        } else {
-            // LDAP is not even configured.
-            echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_INFO);
+        // Create system role mapping field for each assignable system role.
+        $roles = get_ldap_assignable_role_names();
+        foreach ($roles as $role) {
+            foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) {
+                if (empty($groupdn)) {
+                    continue;
+                }
+
+                $role['group'] = $groupdn;
+                $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role);
+                if ($message !== true) {
+                    echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
+                }
+            }
         }
+
+        $this->ldap_close(true);
+        // We were able to connect successfuly.
+        echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS);
     }
 
     /**
index 13f87ce..76a4317 100644 (file)
@@ -163,6 +163,12 @@ $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n";
 $string['usernotfound'] = 'User not found in LDAP';
 $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}';
 
+$string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.';
+$string['diag_toooldversion'] = 'Its is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.';
+$string['diag_emptycontext'] = 'Empty context found.';
+$string['diag_contextnotfound'] = 'Context {$a} does not  exists or cannot be read by bind DN.';
+$string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} does not exists or cannot be read by bind DN.';
+
 // Deprecated since Moodle 3.4.
 $string['auth_ldap_creators'] = 'List of groups or contexts whose members are allowed to create new courses. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\'';
 $string['auth_ldap_creators_key'] = 'Creators';
index f2f536d..c774de1 100644 (file)
@@ -161,18 +161,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {mnet_log} ml
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ml.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {mnet_log}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
@@ -273,13 +265,15 @@ class provider implements
             return;
         }
 
+        $userid = $contextlist->get_user()->id;
         foreach ($contextlist->get_contexts() as $context) {
             if ($context->contextlevel != CONTEXT_USER) {
-                return;
+                continue;
+            }
+            if ($context->instanceid == $userid) {
+                // Because we only use user contexts the instance ID is the user ID.
+                $DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
             }
-
-            // Because we only use user contexts the instance ID is the user ID.
-            $DB->delete_records('mnet_log', ['userid' => $context->instanceid]);
         }
     }
 }
index d0dfa65..f5130ae 100644 (file)
@@ -99,18 +99,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextuser' => CONTEXT_USER,
-            'contextid' => $context->id
-        ];
-
-        $sql = "SELECT ctx.instanceid as userid
-                  FROM {auth_oauth2_linked_login} ao
-                  JOIN {context} ctx
-                       ON ctx.instanceid = ao.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {auth_oauth2_linked_login}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
@@ -178,12 +170,15 @@ class provider implements
         if (empty($contextlist->count())) {
             return;
         }
+        $userid = $contextlist->get_user()->id;
         foreach ($contextlist->get_contexts() as $context) {
             if ($context->contextlevel != CONTEXT_USER) {
-                return;
+                continue;
+            }
+            if ($context->instanceid == $userid) {
+                // Because we only use user contexts the instance ID is the user ID.
+                static::delete_user_data($context->instanceid);
             }
-            // Because we only use user contexts the instance ID is the user ID.
-            static::delete_user_data($context->instanceid);
         }
     }
 
index 39ca032..c52ad12 100644 (file)
@@ -308,9 +308,9 @@ class auth_plugin_shibboleth extends auth_plugin_base {
 
     /**
      * Sets the standard SAML domain cookie that is also used to preselect
-     * the right entry on the local wayf
+     * the right entry on the local way
      *
-     * @param IdP identifiere
+     * @param string $selectedIDP IDP identifier
      */
     function set_saml_cookie($selectedIDP) {
         if (isset($_COOKIE['_saml_idp']))
@@ -325,41 +325,12 @@ class auth_plugin_shibboleth extends auth_plugin_base {
         setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600));
     }
 
-     /**
-     * Prints the option elements for the select element of the drop down list
-     *
-     */
-    function print_idp_list(){
-        $config = get_config('auth_shibboleth');
-
-        $IdPs = get_idp_list($config->organization_selection);
-        if (isset($_COOKIE['_saml_idp'])){
-            $idp_cookie = generate_cookie_array($_COOKIE['_saml_idp']);
-            do {
-                $selectedIdP = array_pop($idp_cookie);
-            } while (!isset($IdPs[$selectedIdP]) && count($idp_cookie) > 0);
-
-        } else {
-            $selectedIdP = '-';
-        }
-
-        foreach($IdPs as $IdP => $data){
-            if ($IdP == $selectedIdP){
-                echo '<option value="'.$IdP.'" selected="selected">'.$data[0].'</option>';
-            } else {
-                echo '<option value="'.$IdP.'">'.$data[0].'</option>';
-            }
-        }
-    }
-
-
-     /**
+    /**
      * Generate array of IdPs from Moodle Shibboleth settings
      *
      * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator
      * @return array Identifier of IdPs and their name/session initiator
      */
-
     function get_idp_list($organization_selection) {
         $idp_list = array();
 
diff --git a/auth/shibboleth/index_form.html b/auth/shibboleth/index_form.html
deleted file mode 100644 (file)
index 9f1e23e..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-$config = get_config('auth_shibboleth');
-
-if ($show_instructions) {
-    $columns = 'twocolumns';
-} else {
-    $columns = 'onecolumn';
-}
-?>
-<div class="loginbox clearfix <?php echo $columns ?>">
-  <div class="loginpanel">
-    <!--<h2><?php print_string("returningtosite") ?></h2>-->
-
-    <h2><?php
-        if (isset($config->login_name) && !empty($config->login_name)){
-            echo $config->login_name;
-        } else {
-            print_string("auth_shibboleth_login_long", "auth_shibboleth");
-        }
-    ?></h2>
-      <div class="subcontent loginsub">
-        <div class="desc">
-        <?php
-          if (!empty($errormsg)) {
-              echo '<div class="loginerrors">';
-              echo $OUTPUT->error_text($errormsg);
-              echo '</div>';
-          }
-
-        ?>
-          <div class="guestsub">
-          <p><label for="idp"><?php print_string("auth_shibboleth_select_organization", "auth_shibboleth"); ?></label></p>
-            <form action="login.php" method="post" id="guestlogin">
-            <select id="idp" name="idp">
-                <option value="-" ><?php print_string("auth_shibboleth_select_member", "auth_shibboleth"); ?></option>
-                <?php
-                    print_idp_list();
-                ?>
-            </select><p><input type="submit" value="<?php print_string("select"); ?>" accesskey="s" /></p>
-            </form>
-            <p>
-            <?php
-                print_string("auth_shib_contact_administrator", "auth_shibboleth", get_admin()->email);
-            ?>
-            </p>
-          </div>
-         </div>
-      </div>
-
-<?php if ($CFG->guestloginbutton) {  ?>
-      <div class="subcontent guestsub">
-        <div class="desc">
-          <?php print_string("someallowguest") ?>
-        </div>
-        <form action="../../login/index.php" method="post" id="guestlogin">
-          <div class="guestform">
-            <input type="hidden" name="logintoken" value="<?php echo s(\core\session\manager::get_login_token()); ?>" />
-            <input type="hidden" name="username" value="guest" />
-            <input type="hidden" name="password" value="guest" />
-            <input type="submit" value="<?php print_string("loginguest") ?>" />
-          </div>
-        </form>
-      </div>
-<?php } ?>
-     </div>
-
-
-<?php if ($show_instructions) { ?>
-    <div class="signuppanel">
-      <h2><?php print_string("firsttime") ?></h2>
-      <div class="subcontent">
-<?php     if (is_enabled_auth('none')) { // instructions override the rest for security reasons
-              print_string("loginstepsnone");
-          } else if ($CFG->registerauth == 'email') {
-              if (!empty($config->auth_instructions)) {
-                  echo format_text($config->auth_instructions);
-              } else {
-                  print_string("loginsteps", "", "signup.php");
-              } ?>
-                 <div class="signupform">
-                   <form action="../../login/signup.php" method="get" id="signup">
-                   <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                   </form>
-                 </div>
-<?php     } else if (!empty($CFG->registerauth)) {
-              echo format_text($config->auth_instructions); ?>
-              <div class="signupform">
-                <form action="../../login/signup.php" method="get" id="signup">
-                <div><input type="submit" value="<?php print_string("startsignup") ?>" /></div>
-                </form>
-              </div>
-<?php     } else {
-              echo format_text($config->auth_instructions);
-          } ?>
-      </div>
-    </div>
-<?php } ?>
-</div>
index 8ef9ec1..fd8b747 100644 (file)
@@ -41,7 +41,7 @@ $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not r
 $string['auth_shib_changepasswordurl'] = 'Password-change URL';
 $string['auth_shib_idp_list'] = 'Identity providers';
 $string['auth_shib_idp_list_description'] = 'Provide a list of Identity Provider entityIDs to let the user choose from on the login page.<br />On each line there must be a comma-separated tuple for entityID of the IdP (see the Shibboleth metadata file) and Name of IdP as it shall be displayed in the drop-down list.<br />As an optional third parameter you can add the location of a Shibboleth session initiator that shall be used in case your Moodle installation is part of a multi federation setup.';
-$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it.<br />Otherwise, use the normal login form shown here.';
+$string['auth_shib_instructions'] = 'Use the <a href="{$a}">Shibboleth login</a> to get access via Shibboleth, if your institution supports it. Otherwise, use the normal login form shown here.';
 $string['auth_shib_instructions_help'] = 'Here you should provide custom instructions for your users to explain Shibboleth.  It will be shown on the login page in the instructions section. The instructions must include a link to "<b>{$a}</b>" that users click when they want to log in.';
 $string['auth_shib_instructions_key'] = 'Login instructions';
 $string['auth_shib_integrated_wayf'] = 'Moodle WAYF service';
index 6877fcb..d4fc639 100644 (file)
@@ -3,10 +3,9 @@
     require_once("../../config.php");
     require_once($CFG->dirroot."/auth/shibboleth/auth.php");
 
-    //initialize variables
-    $errormsg = '';
+    $idp = optional_param('idp', null, PARAM_RAW);
 
-/// Check for timed out sessions
+    // Check for timed out sessions.
     if (!empty($SESSION->has_timed_out)) {
         $session_has_timed_out = true;
         $SESSION->has_timed_out = false;
@@ -14,8 +13,8 @@
         $session_has_timed_out = false;
     }
 
-
-/// Define variables used in page
+    // Define variables used in page.
+    $isvalid = true;
     $site = get_site();
 
     $loginsite = get_string("loginsite");
 
     $config = get_config('auth_shibboleth');
     if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) {
-        $show_instructions = true;
+        $showinstructions = true;
     } else {
-        $show_instructions = false;
+        $showinstructions = false;
     }
 
-    $IdPs = get_idp_list($config->organization_selection);
-    if (isset($_POST['idp']) && isset($IdPs[$_POST['idp']])){
-        $selectedIdP = $_POST['idp'];
-        set_saml_cookie($selectedIdP);
+    $idplist = get_idp_list($config->organization_selection);
+    if (isset($idp)) {
+        if (isset($idplist[$idp])) {
+            set_saml_cookie($idp);
 
-        // Redirect to SessionInitiator with entityID as argument
-        if (isset($IdPs[$selectedIdP][1]) && !empty($IdPs[$selectedIdP][1])) {
-            // For Shibbolet 1.x Service Providers
-            header('Location: '.$IdPs[$selectedIdP][1].'?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $targeturl = new moodle_url('/auth/shibboleth/index.php');
+            $idpinfo = $idplist[$idp];
 
-            // For Shibbolet 2.x Service Providers
-            // header('Location: '.$IdPs[$selectedIdP][1].'?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            // Redirect to SessionInitiator with entityID as argument.
+            if (isset($idpinfo[1]) && !empty($idpinfo[1])) {
+                $sso = $idpinfo[1];
+            } else {
+                $sso = '/Shibboleth.sso';
+            }
+            // For Shibboleth 1.x Service Providers.
+            header('Location: ' . $sso . '?providerId=' . urlencode($idp) . '&target=' . urlencode($targeturl->out()));
 
         } else {
-            // For Shibbolet 1.x Service Providers
-            header('Location: /Shibboleth.sso?providerId='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
-
-            // For Shibboleth 2.x Service Providers
-            // header('Location: /Shibboleth.sso/DS?entityID='. urlencode($selectedIdP) .'&target='. urlencode($CFG->wwwroot.'/auth/shibboleth/index.php'));
+            $isvalid = false;
         }
-    } elseif (isset($_POST['idp']) && !isset($IdPs[$_POST['idp']]))  {
-        $errormsg = get_string('auth_shibboleth_errormsg', 'auth_shibboleth');
     }
 
     $loginsite = get_string("loginsite");
@@ -60,6 +57,7 @@
     $PAGE->navbar->add($loginsite);
     $PAGE->set_title("$site->fullname: $loginsite");
     $PAGE->set_heading($site->fullname);
+    $PAGE->set_pagelayout('login');
 
     echo $OUTPUT->header();
 
         echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue);
         echo $OUTPUT->box_end();
     } else {
-        include("index_form.html");
-    }
+        // Print login page.
+        $selectedidp = '-';
+        if (isset($_COOKIE['_saml_idp'])) {
+            $idpcookie = generate_cookie_array($_COOKIE['_saml_idp']);
+            do {
+                $selectedidp = array_pop($idpcookie);
+            } while (!isset($idplist[$selectedidp]) && count($idpcookie) > 0);
+        }
 
-    echo $OUTPUT->footer();
+        $idps = [];
+        foreach ($idplist as $value => $data) {
+            $name = reset($data);
+            $selected = $value === $selectedidp;
+            $idps[] = (object)[
+                'name' => $name,
+                'value' => $value,
+                'selected' => $selected
+            ];
+        }
 
+        // Whether the user can sign up.
+        $cansignup = !empty($CFG->registerauth);
+        // Default instructions.
+        $instructions = format_text($config->auth_instructions);
+        if (is_enabled_auth('none')) {
+            $instructions = get_string('loginstepsnone');
+        } else if ($cansignup) {
+            if ($CFG->registerauth === 'email' && empty($instructions)) {
+                $instructions = get_string('loginsteps');
+            }
+        }
 
+        // Build the template context data.
+        $templatedata = (object)[
+            'adminemail' => get_admin()->email,
+            'cansignup' => $cansignup,
+            'guestlogin' => $CFG->guestloginbutton,
+            'guestloginurl' => new moodle_url('/login/index.php'),
+            'idps' => $idps,
+            'instructions' => $instructions,
+            'loginname' => $config->login_name ?? null,
+            'logintoken' => \core\session\manager::get_login_token(),
+            'loginurl' => new moodle_url('/auth/shibboleth/login.php'),
+            'showinstructions' => $showinstructions,
+            'signupurl' => new moodle_url('/login/signup.php'),
+            'isvalid' => $isvalid
+        ];
+
+        // Render the login form.
+        echo $OUTPUT->render_from_template('auth_shibboleth/login_form', $templatedata);
+    }
+
+    echo $OUTPUT->footer();
diff --git a/auth/shibboleth/templates/login_form.mustache b/auth/shibboleth/templates/login_form.mustache
new file mode 100644 (file)
index 0000000..230f615
--- /dev/null
@@ -0,0 +1,129 @@
+{{!
+    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/>.
+}}
+{{!
+    @template auth_shibboleth/login_form
+
+    Template for the Shibboleth authentication plugin's login form.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * adminemail String The Administrator's email address.
+    * cansignup Boolean Whether a new user can sign up for an account.
+    * guestlogin Boolean Whether to show the guest login section.
+    * guestloginurl String The URL for guest login.
+    * idps Array The list of identity providers for the Shibboleth authentication plugin in value-name pairs per IDP.
+    * instructions String Signup instructions.
+    * isvalid Boolean Whether form validation passes.
+    * loginname String The custom login name.
+    * logintoken String The login token.
+    * loginurl String The login URL.
+    * showinstructions Boolean Whether to show additional login instructions.
+    * signupurl String The signup URL.
+
+    Example context (json):
+    {
+        "loginurl": "#",
+        "guestloginurl": "#",
+        "guestlogin": true,
+        "idps": [
+            { "value": 1, "name": "IDP 1" },
+            { "value": 2, "name": "IDP 2", "selected": true },
+            { "value": 3, "name": "IDP 3" }
+        ],
+        "showinstructions": true,
+        "logintoken": "abcde",
+        "adminemail": "admin@example.com",
+        "loginname": "Shib auth",
+        "cansignup": true,
+        "signupurl": "#",
+        "instructions": "Sign up here",
+        "isvalid": false
+    }
+}}
+
+<div class="my-1 my-sm-5"></div>
+<div class="container">
+    <div class="card">
+        <h2 class="card-header">
+            {{#loginname}}{{.}}{{/loginname}}
+            {{^loginname}}{{#str}}auth_shibboleth_login_long, auth_shibboleth{{/str}}{{/loginname}}
+        </h2>
+        <div class="card-body">
+            <div class="row justify-content-center m-l-1 m-r-1 m-b-1">
+                <div class="col-md-5">
+                    <form action="{{loginurl}}" method="post" id="login">
+                        <div class="form-group">
+                            <label for="idp">{{#str}}auth_shibboleth_select_organization, auth_shibboleth{{/str}}</label>
+                            <select id="idp" name="idp" class="form-control input-block-level {{^isvalid}}is-invalid{{/isvalid}}">
+                                <option value="-">{{#str}}auth_shibboleth_select_member, auth_shibboleth{{/str}}</option>
+                                {{#idps}}
+                                    <option value="{{value}}" {{#selected}}selected{{/selected}}>{{name}}</option>
+                                {{/idps}}
+                            </select>
+                            <div class="invalid-feedback text-danger m-b-1" {{#isvalid}}hidden{{/isvalid}}>
+                                {{#str}}auth_shibboleth_errormsg, auth_shibboleth{{/str}}
+                            </div>
+                        </div>
+                        <button type="submit" class="btn btn-primary btn-block m-b-1" accesskey="s">
+                            {{#str}}select, moodle{{/str}}
+                        </button>
+                        <p class="form-text text-muted m-t-1 m-b-1">
+                            {{#str}}auth_shib_contact_administrator, auth_shibboleth, {{adminemail}}{{/str}}
+                        </p>
+                    </form>
+                </div>
+                {{#guestlogin}}
+                <div class="col-md-5">
+                    <p>
+                        {{#str}}someallowguest, moodle{{/str}}
+                    </p>
+                    <form action="{{guestloginurl}}" method="post" id="guestlogin">
+                        <div class="guestform">
+                            <input type="hidden" name="logintoken" value="{{logintoken}}">
+                            <input type="hidden" name="username" value="guest">
+                            <input type="hidden" name="password" value="guest">
+                            <button type="submit" class="btn btn-secondary btn-block">
+                                {{#str}}loginguest, moodle{{/str}}
+                            </button>
+                        </div>
+                    </form>
+                </div>
+                {{/guestlogin}}
+            </div>
+        </div>
+    </div>
+    {{#showinstructions}}
+    <div class="card m-t-1">
+        <div class="card-body m-l-1 m-r-1 m-b-1">
+            <h2 class="card-title">{{#str}}firsttime, moodle{{/str}}</h2>
+            <p>
+                {{{instructions}}}
+            </p>
+            {{#cansignup}}
+            <form action="{{signupurl}}" method="get" id="signup">
+                <button type="submit" class="btn btn-secondary">{{#str}}startsignup, moodle{{/str}}</button>
+            </form>
+            {{/cansignup}}
+        </div>
+    </div>
+    {{/showinstructions}}
+</div>
index 27ebd41..30d1691 100644 (file)
@@ -44,6 +44,12 @@ class behat_auth extends behat_base {
      * @Given /^I log in as "(?P<username_string>(?:[^"]|\\")*)"$/
      */
     public function i_log_in_as($username) {
+        // In the mobile app the required tasks are different.
+        if ($this->is_in_app()) {
+            $this->execute('behat_app::login', [$username]);
+            return;
+        }
+
         // Visit login page.
         $this->getSession()->visit($this->locate_path('login/index.php'));
 
index 8e9ed2e..f547d22 100644 (file)
@@ -188,7 +188,7 @@ class award_criteria_activity extends award_criteria {
      * @return bool Whether criteria is complete
      */
     public function review($userid, $filtered = false) {
-        $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS);
+        $completionstates = array(COMPLETION_COMPLETE, COMPLETION_COMPLETE_PASS, COMPLETION_COMPLETE_FAIL);
 
         if ($this->course->startdate > time()) {
             return false;
diff --git a/badges/tests/behat/criteria_activity.feature b/badges/tests/behat/criteria_activity.feature
new file mode 100644 (file)
index 0000000..b71fcea
--- /dev/null
@@ -0,0 +1,69 @@
+@mod @mod_quiz @core @core_badges @_file_upload @javascript
+Feature: Award badges based on activity completion
+  In order to ensure a student has learned the material before being marked complete
+  As a teacher
+  I need to set a quiz to award a badge when upon completion when the student receives a passing grade, or completed_fail if they use all attempts without passing
+
+  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 | enablecompletion |
+      | Course 1 | C1        | 0        | 1                |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+      | student1 | C1     | student        |
+    And the following config values are set as admin:
+      | grade_item_advanced | hiddenuntil |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name           | questiontext              |
+      | Test questions   | truefalse | First question | Answer the first question |
+    And the following "activities" exist:
+      | activity   | name           | course | idnumber | attempts | gradepass | completion | completionattemptsexhausted | completionpass | completionusegrade |
+      | quiz       | Test quiz name | C1     | quiz1    | 2        | 5.00      | 2          | 1                           | 1              | 1                  |
+    And quiz "Test quiz name" contains the following questions:
+      | question       | page |
+      | First question | 1    |
+    And user "student1" has attempted "Test quiz name" with responses:
+      | slot | response |
+      |   1  | False    |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Add a new badge" in current page administration
+    And I follow "Add a new badge"
+    And I set the following fields to these values:
+      | Name | Course Badge |
+      | Description | Course badge 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 "Activity completion"
+    And I set the field "Quiz - Test quiz name" to "1"
+    And I press "Save"
+    And I press "Enable access"
+    And I press "Continue"
+    And I should see "Recipients (0)"
+    And I log out
+
+  Scenario: Student earns a badge using activity completion, but does not get passing grade
+    When I log in as "student1"
+    And I am on "Course 1" course homepage
+    And the "Test quiz name" "quiz" activity with "auto" completion should be marked as not complete
+    And I follow "Test quiz name"
+    And I press "Re-attempt quiz"
+    And I set the field "False" to "1"
+    And I press "Finish attempt ..."
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+    And I log out
+    Then I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Badges > Manage badges" in current page administration
+    And I follow "Course Badge"
+    And I should see "Recipients (1)"
index 9ed8f32..dd32b8f 100644 (file)
@@ -105,18 +105,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextuser' => CONTEXT_USER,
-        ];
-
-        $sql = "SELECT bc.userid as userid
-                  FROM {block_community} bc
-                  JOIN {context} ctx
-                       ON ctx.instanceid = bc.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {block_community}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index e7a080f..aa32e78 100644 (file)
@@ -98,23 +98,26 @@ class provider implements
      * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
      */
     public static function get_users_in_context(userlist $userlist) {
+        // This block doesn't know who information is stored against unless it
+        // is at the user context.
         $context = $userlist->get_context();
 
-        if (!is_a($context, \context_block::class)) {
+        if (!$context instanceof \context_block) {
             return;
         }
 
+        $sql = "SELECT bpc.instanceid AS userid
+                  FROM {block_instances} bi
+                  JOIN {context} bpc ON bpc.id = bi.parentcontextid
+                 WHERE bi.blockname = 'html'
+                   AND bpc.contextlevel = :contextuser
+                   AND bi.id = :blockinstanceid";
+
         $params = [
-            'contextid'    => $context->id,
             'contextuser' => CONTEXT_USER,
+            'blockinstanceid' => $context->instanceid
         ];
 
-        $sql = "SELECT bpc.instanceid AS userid
-                  FROM {context} c
-                  JOIN {block_instances} bi ON bi.id = c.instanceid AND bi.blockname = 'html'
-                  JOIN {context} bpc ON bpc.id = bi.parentcontextid AND bpc.contextlevel = :contextuser
-                 WHERE c.id = :contextid";
-
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 67d0d03..8247355 100644 (file)
@@ -66,12 +66,16 @@ class block_login extends block_base {
 
             $this->content->text .= "\n".'<form class="loginform" id="login" method="post" action="'.get_login_url().'">';
 
-            $this->content->text .= '<div class="form-group"><label for="login_username">'.$strusername.'</label>';
-            $this->content->text .= '<input type="text" name="username" id="login_username" class="form-control" value="'.s($username).'" /></div>';
+            $this->content->text .= '<div class="form-group">';
+            $this->content->text .= '<label for="login_username">'.$strusername.'</label>';
+            $this->content->text .= '<input type="text" name="username" id="login_username" ';
+            $this->content->text .= ' class="form-control" value="'.s($username).'" autocomplete="username"/></div>';
 
             $this->content->text .= '<div class="form-group"><label for="login_password">'.get_string('password').'</label>';
 
-            $this->content->text .= '<input type="password" name="password" id="login_password" class="form-control" value="" /></div>';
+            $this->content->text .= '<input type="password" name="password" id="login_password" ';
+            $this->content->text .= ' class="form-control" value="" autocomplete="current-password"/>';
+            $this->content->text .= '</div>';
 
             if (isset($CFG->rememberusername) and $CFG->rememberusername == 2) {
                 $checked = $username ? 'checked="checked"' : '';
index dbce038..99b59c4 100644 (file)
Binary files a/blocks/myoverview/amd/build/view.min.js and b/blocks/myoverview/amd/build/view.min.js differ
index f9d0e1d..85c81e1 100644 (file)
@@ -64,7 +64,7 @@ function(
         COURSES_CARDS: 'block_myoverview/view-cards',
         COURSES_LIST: 'block_myoverview/view-list',
         COURSES_SUMMARY: 'block_myoverview/view-summary',
-        NOCOURSES: 'block_myoverview/no-courses'
+        NOCOURSES: 'core_course/no-courses'
     };
 
     var NUMCOURSES_PERPAGE = [12, 24, 48];
index 75c38d3..22c49e3 100644 (file)
@@ -54,7 +54,6 @@ $string['lastaccessed'] = 'Last accessed';
 $string['list'] = 'List';
 $string['myoverview:addinstance'] = 'Add a new course overview block';
 $string['myoverview:myaddinstance'] = 'Add a new course overview block to Dashboard';
-$string['nocourses'] = 'No courses';
 $string['past'] = 'Past';
 $string['pluginname'] = 'Course overview';
 $string['privacy:metadata:overviewsortpreference'] = 'The Course overview block sort preference.';
@@ -90,3 +89,6 @@ $string['timeline'] = 'Timeline';
 $string['viewcoursename'] = 'View course {$a}';
 $string['privacy:metadata:overviewlasttab'] = 'This stores the last tab selected by the user on the overview block.';
 $string['viewcourse'] = 'View course';
+
+// Deprecated since Moodle 3.7.
+$string['nocourses'] = 'No courses';
\ No newline at end of file
index 7927bba..4e22e76 100644 (file)
@@ -12,4 +12,5 @@ sortbycourses,block_myoverview
 sortbydates,block_myoverview
 timeline,block_myoverview
 viewcoursename,block_myoverview
-privacy:metadata:overviewlasttab,block_myoverview
\ No newline at end of file
+privacy:metadata:overviewlasttab,block_myoverview
+nocourses,block_myoverview
\ No newline at end of file
index d61c984..5bbe7ef 100644 (file)
 <div data-region="loading-placeholder-content" aria-hidden="true">
     {{#cards}}
     <div class="card-deck dashboard-card-deck one-row" style="height: 13rem">
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
-        <div class="card dashboard-card border-0">
-            <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-            </div>
-            <div class="card-body course-info-container">
-                <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-            </div>
-        </div>
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
+        {{> core_course/placeholder-course }}
     </div>
     {{/cards}}
     {{#list}}
index 73692b5..594bbde 100644 (file)
Binary files a/blocks/recentlyaccessedcourses/amd/build/main.min.js and b/blocks/recentlyaccessedcourses/amd/build/main.min.js differ
index 70e679e..2fdd54c 100644 (file)
@@ -69,13 +69,13 @@ define(
          */
         var renderCourses = function(root, courses) {
             if (courses.length > 0) {
-                return Templates.render('block_recentlyaccessedcourses/view-cards', {
+                return Templates.render('core_course/view-cards', {
                     courses: courses
                 });
             } else {
-                var nocoursesimgurl = root.attr('data-nocoursesimgurl');
+                var nocoursesimgurl = root.attr('data-nocoursesimg');
                 return Templates.render('block_recentlyaccessedcourses/no-courses', {
-                    nocoursesimgurl: nocoursesimgurl
+                    nocoursesimg: nocoursesimgurl
                 });
             }
         };
index 09bbe03..06eb282 100644 (file)
@@ -49,7 +49,7 @@ class main implements renderable, templatable {
 
         return [
             'userid' => $USER->id,
-            'nocoursesimgurl' => $nocoursesurl
+            'nocoursesimg' => $nocoursesurl
         ];
     }
 }
index 9730dcd..cdf71be 100644 (file)
@@ -20,8 +20,8 @@
  * @copyright  2018 Victor Deniz <victor@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-$string['nocourses'] = 'No recent courses';
 $string['pluginname'] = 'Recently accessed courses';
 $string['privacy:metadata'] = 'The Recently accessed courses block does not store any personal data.';
 $string['recentlyaccessedcourses:addinstance'] = 'Add a new Recently accessed courses block';
 $string['recentlyaccessedcourses:myaddinstance'] = 'Add a new recently accessed courses block to Dashboard';
+$string['nocourses'] = 'No recent courses';
\ No newline at end of file
index 22a8648..84e64d5 100644 (file)
@@ -22,7 +22,7 @@
     Example context (json):
     {
         "userid": 2,
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
 
index 27ab207..cf1b8b0 100644 (file)
     You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-{{!
-    @template block_recentlyaccessedcourses/no-courses
-
-    This template renders the no courses message.
-
-    Example context (json):
-    {
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
-    }
-}}
-<div class="text-xs-center text-center m-t-3" data-region="empty-message">
-    <img class="empty-placeholder-image-lg m-t-1"
-         src="{{nocoursesimgurl}}"
-         alt="{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}"
-         role="presentation">
-    <p class="text-muted mt-3">{{#str}} nocourses, block_recentlyaccessedcourses {{/str}}</p>
-</div>
\ No newline at end of file
+{{< core_course/no-courses}}
+    {{$nocoursestring}}
+        {{#str}} nocourses, block_recentlyaccessedcourses {{/str}}
+    {{/nocoursestring}}
+{{/ core_course/no-courses}}
index b277154..ec44dae 100644 (file)
 
     Example context (json):
     {
-        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
+        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
 <div id="recentlyaccessedcourses-view-{{uniqid}}"
      data-region="recentlyaccessedcourses-view"
-     data-nocoursesimgurl="{{nocoursesimgurl}}">
+     data-nocoursesimg="{{nocoursesimg}}">
     <div data-region="recentlyaccessedcourses-view-content">
         <div data-region="recentlyaccessedcourses-loading-placeholder">
             <div class="card-deck dashboard-card-deck one-row" style="height: 11.1rem">
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
-                {{> block_recentlyaccessedcourses/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
             </div>
         </div>
     </div>
index 214148e..e2b71e6 100644 (file)
@@ -98,18 +98,10 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextuser' => CONTEXT_USER,
-        ];
-
-        $sql = "SELECT brc.userid as userid
-                  FROM {block_rss_client} brc
-                  JOIN {context} ctx
-                       ON ctx.instanceid = brc.userid
-                       AND ctx.contextlevel = :contextuser
-                 WHERE ctx.id = :contextid";
-
+        $sql = "SELECT userid
+                  FROM {block_rss_client}
+                 WHERE userid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 8ebbc68..9b9beb9 100644 (file)
Binary files a/blocks/starredcourses/amd/build/main.min.js and b/blocks/starredcourses/amd/build/main.min.js differ
index 81d4130..0c6d9c0 100644 (file)
@@ -53,7 +53,7 @@ function(
      */
     var renderCourses = function(root, courses) {
         if (courses.length > 0) {
-            return Templates.render('block_starredcourses/view-cards', {
+            return Templates.render('core_course/view-cards', {
                 courses: courses
             });
         } else {
index 72040e2..37d51a6 100644 (file)
@@ -22,9 +22,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-$string['nocourses'] = 'No starred courses';
 $string['pluginname'] = 'Starred courses';
 $string['privacy:metadata'] = 'The starred courses block does not store any personal data.';
 $string['starredcourses:addinstance'] = 'Add a new starred courses block';
 $string['starredcourses:myaddinstance'] = 'Add a new starred courses block to Dashboard';
-
+$string['nocourses'] = 'No starred courses';
\ No newline at end of file
index a00de4e..7333fc7 100644 (file)
@@ -1,28 +1,21 @@
 {{!
     This file is part of Moodle - http://moodle.org/
-     Moodle is free software: you can redistribute it and/or modify
+
+    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,
+
+    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
+
+    You should have received a copy of the GNU General Public License
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
-{{!
-    @template block_starredcourses/no-courses
-     This template renders the no courses message.
-     Example context (json):
-    {
-        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_recentcourses/1535727318/courses"
-    }
-}}
-<div class="text-xs-center text-center m-t-3" data-region="empty-message">
-    <img class="empty-placeholder-image-lg m-t-1"
-         src="{{nocoursesimg}}"
-         alt="{{#str}} nocourses, block_starredcourses {{/str}}"
-         role="presentation">
-    <p class="text-muted mt-3">{{#str}} nocourses, block_starredcourses {{/str}}</p>
-</div>
+{{< core_course/no-courses}}
+    {{$nocoursestring}}
+        {{#str}} nocourses, block_starredcourses {{/str}}
+    {{/nocoursestring}}
+{{/ core_course/no-courses}}
diff --git a/blocks/starredcourses/templates/placeholder-course.mustache b/blocks/starredcourses/templates/placeholder-course.mustache
deleted file mode 100644 (file)
index 3ce061e..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-{{!
-    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/>.
-}}
-{{!
-    @template block_starredcourses/placeholder-course
-     This template renders an course card item loading placeholder for the starred courses block.
-     Example context (json):
-    {}
-}}
-<div class="card dashboard-card border-0">
-    <div class="card-img-top bg-pulse-grey w-100" style="height: 7rem">
-    </div>
-    <div class="card-body recent-course-info-container">
-        <div class="bg-pulse-grey w-100" style="height: 1rem"></div>
-    </div>
-</div>
diff --git a/blocks/starredcourses/templates/view-cards.mustache b/blocks/starredcourses/templates/view-cards.mustache
deleted file mode 100644 (file)
index b84d24e..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-{{!
-    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 Licensebllsdsadfasfd
-    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
-}}
-{{!
-    @template block_starredcourses/view-cards
-     This template renders the carousel for the starredcourses block.
-     Example context (json):
-    {
-        "courses": [
-            {
-                "name": "Assignment due 1",
-                "viewurl": "https://moodlesite/course/view.php?id=2",
-                "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg",
-                "fullname": "course 3"
-            }
-        ]
-    }
-}}
-
-{{< core_course/coursecards }}
-    {{$classes}}one-row{{/classes}}
-    {{$coursename}} {{fullname}} {{/coursename}}
-{{/ core_course/coursecards }}
index 7046b96..fd2f548 100644 (file)
@@ -30,9 +30,9 @@
     <div data-region="starred-courses-view-content">
         <div data-region="starred-courses-loading-placeholder">
             <div class="card-deck dashboard-card-deck one-row" style="height: 11.1rem">
-                {{> block_starredcourses/placeholder-course }}
-                {{> block_starredcourses/placeholder-course }}
-                {{> block_starredcourses/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
+                {{> core_course/placeholder-course }}
             </div>
         </div>
     </div>
index 691d2fb..6b1337f 100644 (file)
@@ -1712,6 +1712,7 @@ class cache_session extends cache {
     public function __construct(cache_definition $definition, cache_store $store, $loader = null) {
         // First up copy the loadeduserid to the current user id.
         $this->currentuserid = self::$loadeduserid;
+        $this->set_session_id();
         parent::__construct($definition, $store, $loader);
 
         // This will trigger check tracked user. If this gets removed a call to that will need to be added here in its place.
@@ -1771,8 +1772,6 @@ class cache_session extends cache {
                 // Purge the data we have for the old user.
                 // This way we don't bloat the session.
                 $this->purge();
-                // Update the session id just in case!
-                $this->set_session_id();
             }
             self::$loadeduserid = $new;
             $this->currentuserid = $new;
@@ -1780,8 +1779,6 @@ class cache_session extends cache {
             // The current user matches the loaded user but not the user last used by this cache.
             $this->purge_current_user();
             $this->currentuserid = $new;
-            // Update the session id just in case!
-            $this->set_session_id();
         }
     }
 
index 6aa268f..9c0f1d9 100644 (file)
@@ -2327,4 +2327,51 @@ class core_cache_testcase extends advanced_testcase {
         $this->assertEquals('test data 2', $cache->get('testkey1'));
     }
 
+    /**
+     * Test that values set in different sessions are stored with different key prefixes.
+     */
+    public function test_session_distinct_storage_key() {
+        $this->resetAfterTest();
+
+        // Prepare a dummy session cache configuration.
+        $config = cache_config_testing::instance();
+        $config->phpunit_add_definition('phpunit/test_session_distinct_storage_key', array(
+            'mode' => cache_store::MODE_SESSION,
+            'component' => 'phpunit',
+            'area' => 'test_session_distinct_storage_key'
+        ));
+
+        // First anonymous user's session cache.
+        cache_phpunit_session::phpunit_mockup_session_id('foo');
+        $this->setUser(0);
+        $cache1 = cache::make('phpunit', 'test_session_distinct_storage_key');
+
+        // Reset cache instances to emulate a new request.
+        cache_factory::instance()->reset_cache_instances();
+
+        // Another anonymous user's session cache.
+        cache_phpunit_session::phpunit_mockup_session_id('bar');
+        $this->setUser(0);
+        $cache2 = cache::make('phpunit', 'test_session_distinct_storage_key');
+
+        cache_factory::instance()->reset_cache_instances();
+
+        // Guest user's session cache.
+        cache_phpunit_session::phpunit_mockup_session_id('baz');
+        $this->setGuestUser();
+        $cache3 = cache::make('phpunit', 'test_session_distinct_storage_key');
+
+        cache_factory::instance()->reset_cache_instances();
+
+        // Same guest user's session cache but in another browser window.
+        cache_phpunit_session::phpunit_mockup_session_id('baz');
+        $this->setGuestUser();
+        $cache4 = cache::make('phpunit', 'test_session_distinct_storage_key');
+
+        // Assert that different PHP session implies different key prefix for storing values.
+        $this->assertNotEquals($cache1->phpunit_get_key_prefix(), $cache2->phpunit_get_key_prefix());
+
+        // Assert that same PHP session implies same key prefix for storing values.
+        $this->assertEquals($cache3->phpunit_get_key_prefix(), $cache4->phpunit_get_key_prefix());
+    }
 }
index 6c42c39..bee6609 100644 (file)
@@ -465,6 +465,9 @@ class cache_phpunit_application extends cache_application {
  */
 class cache_phpunit_session extends cache_session {
 
+    /** @var Static member used for emulating the behaviour of session_id() during the tests. */
+    protected static $sessionidmockup = 'phpunitmockupsessionid';
+
     /**
      * Returns the class of the store immediately associated with this cache.
      * @return string
@@ -480,6 +483,31 @@ class cache_phpunit_session extends cache_session {
     public function phpunit_get_store_implements() {
         return class_implements($this->get_store());
     }
+
+    /**
+     * Provide access to the {@link cache_session::get_key_prefix()} method.
+     *
+     * @return string
+     */
+    public function phpunit_get_key_prefix() {
+        return $this->get_key_prefix();
+    }
+
+    /**
+     * Allows to inject the session identifier.
+     *
+     * @param string $sessionid
+     */
+    public static function phpunit_mockup_session_id($sessionid) {
+        static::$sessionidmockup = $sessionid;
+    }
+
+    /**
+     * Override the parent behaviour so that it does not need the actual session_id() call.
+     */
+    protected function set_session_id() {
+        $this->sessionid = static::$sessionidmockup;
+    }
 }
 
 /**
index 9d04083..97edf63 100644 (file)
@@ -3191,6 +3191,34 @@ class api {
         return $plancompetency;
     }
 
+    /**
+     * List the plans with a competency.
+     *
+     * @param  int $userid The user id we want the plans for.
+     * @param  int $competencyorid The competency, or its ID.
+     * @return array[plan] Array of learning plans.
+     */
+    public static function list_plans_with_competency($userid, $competencyorid) {
+        global $USER;
+
+        static::require_enabled();
+        $competencyid = $competencyorid;
+        $competency = null;
+        if (is_object($competencyid)) {
+            $competency = $competencyid;
+            $competencyid = $competency->get('id');
+        }
+
+        $plans = plan::get_by_user_and_competency($userid, $competencyid);
+        foreach ($plans as $index => $plan) {
+            // Filter plans we cannot read.
+            if (!$plan->can_read()) {
+                unset($plans[$index]);
+            }
+        }
+        return $plans;
+    }
+
     /**
      * List the competencies in a user plan.
      *
index 705a32a..3ecf3e9 100644 (file)
@@ -4631,4 +4631,38 @@ class core_competency_api_testcase extends advanced_testcase {
         $this->assertEquals($uc1b->get('id'), $result['competencies'][0]->usercompetency->get('id'));
         $this->assertEquals($uc1c->get('id'), $result['competencies'][1]->usercompetency->get('id'));
     }
+
+    /**
+     * Test we can get all of a users plans with a competency.
+     */
+    public function test_list_plans_with_competency() {
+        $this->resetAfterTest(true);
+        $this->setAdminUser();
+        $lpg = $this->getDataGenerator()->get_plugin_generator('core_competency');
+
+        $u1 = $this->getDataGenerator()->create_user();
+        $tpl = $this->getDataGenerator()->get_plugin_generator('core_competency')->create_template();
+
+        // Create a framework and assign competencies.
+        $framework = $lpg->create_framework();
+        $c1 = $lpg->create_competency(array('competencyframeworkid' => $framework->get('id')));
+
+        // Create two plans and assign the competency to each.
+        $plan1 = $lpg->create_plan(array('userid' => $u1->id));
+        $plan2 = $lpg->create_plan(array('userid' => $u1->id));
+
+        $lpg->create_plan_competency(array('planid' => $plan1->get('id'), 'competencyid' => $c1->get('id')));
+        $lpg->create_plan_competency(array('planid' => $plan2->get('id'), 'competencyid' => $c1->get('id')));
+
+        // Create one more plan without the competency.
+        $plan3 = $lpg->create_plan(array('userid' => $u1->id));
+
+        $plans = api::list_plans_with_competency($u1->id, $c1);
+
+        $this->assertEquals(2, count($plans));
+
+        $this->assertEquals(reset($plans)->get('id'), $plan1->get('id'));
+        $this->assertEquals(end($plans)->get('id'), $plan2->get('id'));
+    }
+
 }
index a1ec90b..0861a0e 100644 (file)
@@ -161,47 +161,55 @@ class provider implements
         $completioninfo = new \completion_info($course);
         $completion = $completioninfo->is_enabled();
 
-        if ($completion == COMPLETION_ENABLED) {
+        if ($completion != COMPLETION_ENABLED) {
+            return [];
+        }
+
+        $coursecomplete = $completioninfo->is_course_complete($user->id);
 
-            $coursecomplete = $completioninfo->is_course_complete($user->id);
+        if ($coursecomplete) {
+            $status = get_string('complete');
+        } else {
             $criteriacomplete = $completioninfo->count_course_user_data($user->id);
             $ccompletion = new \completion_completion(['userid' => $user->id, 'course' => $course->id]);
 
-            $status = ($coursecomplete) ? get_string('complete') : '';
-            $status = (!$criteriacomplete && !$ccompletion->timestarted) ? get_string('notyetstarted', 'completion') :
-                    get_string('inprogress', 'completion');
-
-            $completions = $completioninfo->get_completions($user->id);
-            $overall = get_string('nocriteriaset', 'completion');
-            if (!empty($completions)) {
-                if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
-                    $overall = get_string('criteriarequiredall', 'completion');
-                } else {
-                    $overall = get_string('criteriarequiredany', 'completion');
-                }
+            if (!$criteriacomplete && !$ccompletion->timestarted) {
+                $status = get_string('notyetstarted', 'completion');
+            } else {
+                $status = get_string('inprogress', 'completion');
             }
+        }
 
-            $coursecompletiondata = [
-                'status' => $status,
-                'required' => $overall,
-            ];
-
-            $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
-                $criteria = $completion->get_criteria();
-                $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
-                $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
-                        get_string('any', 'completion');
-                $data = [
-                    'required' => $required,
-                    'completed' => transform::yesno($completion->is_complete()),
-                    'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
-                ];
-                $details = $criteria->get_details($completion);
-                $data = array_merge($data, $details);
-                return $data;
-            }, $completions);
-            return $coursecompletiondata;
+        $completions = $completioninfo->get_completions($user->id);
+        $overall = get_string('nocriteriaset', 'completion');
+        if (!empty($completions)) {
+            if ($completioninfo->get_aggregation_method() == COMPLETION_AGGREGATION_ALL) {
+                $overall = get_string('criteriarequiredall', 'completion');
+            } else {
+                $overall = get_string('criteriarequiredany', 'completion');
+            }
         }
+
+        $coursecompletiondata = [
+            'status' => $status,
+            'required' => $overall,
+        ];
+
+        $coursecompletiondata['criteria'] = array_map(function($completion) use ($completioninfo) {
+            $criteria = $completion->get_criteria();
+            $aggregation = $completioninfo->get_aggregation_method($criteria->criteriatype);
+            $required = ($aggregation == COMPLETION_AGGREGATION_ALL) ? get_string('all', 'completion') :
+                    get_string('any', 'completion');
+            $data = [
+                'required' => $required,
+                'completed' => transform::yesno($completion->is_complete()),
+                'timecompleted' => isset($completion->timecompleted) ? transform::datetime($completion->timecompleted) : ''
+            ];
+            $details = $criteria->get_details($completion);
+            $data = array_merge($data, $details);
+            return $data;
+        }, $completions);
+        return $coursecompletiondata;
     }
 
     /**
index ffff66a..7bab239 100644 (file)
@@ -193,13 +193,31 @@ class core_completion_privacy_test extends \core_privacy\tests\provider_testcase
         $hasno = array_search('No', $coursecompletion1['criteria'], true);
         $this->assertFalse($hasno);
         $coursecompletion2 = \core_completion\privacy\provider::get_course_completion_info($user2, $this->course);
-        $hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
+        $hasyes = array_search('Yes', $coursecompletion2['criteria'], true);
         $this->assertFalse($hasyes);
         $coursecompletion3 = \core_completion\privacy\provider::get_course_completion_info($user3, $this->course);
-        $hasno = array_search('No', $coursecompletion1['criteria'], true);
+        $hasno = array_search('No', $coursecompletion3['criteria'], true);
         $this->assertFalse($hasno);
         $coursecompletion4 = \core_completion\privacy\provider::get_course_completion_info($user4, $this->course);
-        $hasyes = array_search('Yes', $coursecompletion1['criteria'], true);
+        $hasyes = array_search('Yes', $coursecompletion4['criteria'], true);
         $this->assertFalse($hasyes);
     }
+
+    /**
+     * Test getting course completion information with completion disabled.
+     */
+    public function test_get_course_completion_info_completion_disabled() {
+        $this->resetAfterTest();
+
+        $user = $this->getDataGenerator()->create_user();
+
+        $course = $this->getDataGenerator()->create_course(['enablecompletion' => 0]);
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+        $coursecompletion = \core_completion\privacy\provider::get_course_completion_info($user, $course);
+
+        $this->assertTrue(is_array($coursecompletion));
+        $this->assertEmpty($coursecompletion);
+    }
 }
index c5659ea..006768a 100644 (file)
@@ -6,8 +6,8 @@
     "homepage": "https://moodle.org",
     "require-dev": {
         "phpunit/phpunit": "6.5.*",
-        "phpunit/dbUnit": "3.0.*",
+        "phpunit/dbunit": "3.0.*",
         "moodlehq/behat-extension": "3.37.0",
-        "mikey179/vfsStream": "^1.6"
+        "mikey179/vfsstream": "^1.6"
     }
 }
index 29f9b79..baff6db 100644 (file)
@@ -608,6 +608,12 @@ $CFG->admin = 'admin';
 //
 //      $CFG->disablelogintoken = true;
 //
+// Moodle 3.7+ checks that cron is running frequently. If the time between cron runs
+// is greater than this value (in seconds), you get a warning on the admin page. (This
+// setting only controls whether or not the warning appears, it has no other effect.)
+//
+//      $CFG->expectedcronfrequency = 200;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
@@ -862,6 +868,13 @@ $CFG->admin = 'admin';
 // Example:
 //   define('BEHAT_DISABLE_HISTOGRAM', true);
 //
+// Mobile app Behat testing requires this option, pointing to a developer Moodle Mobile directory:
+//   $CFG->behat_ionic_dirroot = '/where/I/keep/my/git/checkouts/moodlemobile2';
+//
+// The following option can be used to indicate a running Ionic server (otherwise Behat will start
+// one automatically for each test run, which is convenient but takes ages):
+//   $CFG->behat_ionic_wwwroot = 'http://localhost:8100';
+//
 //=========================================================================
 // 12. DEVELOPER DATA GENERATOR
 //=========================================================================
index 89fc598..b75f19f 100644 (file)
@@ -104,7 +104,7 @@ class core_course_deletecategory_form extends moodleform {
             if (in_array($this->coursecat->parent, $displaylist)) {
                 $mform->setDefault('newparent', $this->coursecat->parent);
             }
-            $mform->disabledIf('newparent', 'fulldelete', 'eq', '1');
+            $mform->hideIf('newparent', 'fulldelete', 'eq', '1');
         }
 
         $mform->addElement('hidden', 'categoryid', $this->coursecat->id);
index 8e74142..a5b5c8e 100644 (file)
@@ -23,7 +23,6 @@ M.core_completion.init = function(Y) {
                 iconkey,
                 button = args.image.get('parentNode');
 
-
             if (current == 1) {
                 altstr = M.util.get_string('completion-alt-manual-y', 'completion', modulename);
                 iconkey = 'i/completion-manual-y';
@@ -33,11 +32,13 @@ M.core_completion.init = function(Y) {
                 iconkey = 'i/completion-manual-n';
                 args.state.set('value', 1);
             }
-            button.set('title', altstr);
 
             require(['core/templates', 'core/notification'], function(Templates, Notification) {
                 Templates.renderPix(iconkey, 'core', altstr).then(function(html) {
-                    Templates.replaceNode(args.image.getDOMNode(), html, '');
+                    var id = button.get('id'),
+                        postFocus = '$(document.getElementById("' + id + '")).focus();';
+
+                    Templates.replaceNode(args.image.getDOMNode(), html, postFocus);
                 }).catch(Notification.exception);
             });
         }
index 55cc640..36e7bd0 100644 (file)
@@ -3433,6 +3433,13 @@ function duplicate_module($course, $cm) {
     $rc = new restore_controller($backupid, $course->id,
             backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
 
+    // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
+    $plan = $rc->get_plan();
+    $groupsetting = $plan->get_setting('groups');
+    if (empty($groupsetting->get_value())) {
+        $groupsetting->set_value(true);
+    }
+
     $cmcontext = context_module::instance($cm->id);
     if (!$rc->execute_precheck()) {
         $precheckresults = $rc->get_precheck_results();
index 4d2348e..caf3868 100644 (file)
@@ -61,9 +61,7 @@ function add_moduleinfo($moduleinfo, $course, $mform = null) {
     $newcm->instance         = 0; // Not known yet, will be updated later (this is similar to restore code).
     $newcm->visible          = $moduleinfo->visible;
     $newcm->visibleold       = $moduleinfo->visible;
-    if (isset($moduleinfo->visibleoncoursepage)) {
-        $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage;
-    }
+    $newcm->visibleoncoursepage = $moduleinfo->visibleoncoursepage;
     if (isset($moduleinfo->cmidnumber)) {
         $newcm->idnumber         = $moduleinfo->cmidnumber;
     }
@@ -410,6 +408,9 @@ function set_moduleinfo_defaults($moduleinfo) {
     if (!isset($moduleinfo->conditionfieldgroup)) {
         $moduleinfo->conditionfieldgroup = array();
     }
+    if (!isset($moduleinfo->visibleoncoursepage)) {
+        $moduleinfo->visibleoncoursepage = 1;
+    }
 
     return $moduleinfo;
 }
index 5b47c51..c977a14 100644 (file)
@@ -283,7 +283,7 @@ abstract class moodleform_mod extends moodleform {
         // option (MDL-30764)
         if (empty($this->_cm) || !$this->_cm->groupingid) {
             if ($mform->elementExists('groupmode') && empty($COURSE->groupmodeforce)) {
-                $mform->disabledIf('groupingid', 'groupmode', 'eq', NOGROUPS);
+                $mform->hideIf('groupingid', 'groupmode', 'eq', NOGROUPS);
 
             } else if (!$mform->elementExists('groupmode')) {
                 // Groupings have no use without groupmode.
@@ -545,19 +545,19 @@ abstract class moodleform_mod extends moodleform {
                 }
             }
             $mform->addElement('modgrade', 'scale', get_string('scale'), $gradeoptions);
-            $mform->disabledIf('scale', 'assessed', 'eq', 0);
+            $mform->hideIf('scale', 'assessed', 'eq', 0);
             $mform->addHelpButton('scale', 'modgrade', 'grades');
             $mform->setDefault('scale', $CFG->gradepointdefault);
 
             $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating'));
-            $mform->disabledIf('ratingtime', 'assessed', 'eq', 0);
+            $mform->hideIf('ratingtime', 'assessed', 'eq', 0);
 
             $mform->addElement('date_time_selector', 'assesstimestart', get_string('from'));
-            $mform->disabledIf('assesstimestart', 'assessed', 'eq', 0);
+            $mform->hideIf('assesstimestart', 'assessed', 'eq', 0);
             $mform->disabledIf('assesstimestart', 'ratingtime');
 
             $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to'));
-            $mform->disabledIf('assesstimefinish', 'assessed', 'eq', 0);
+            $mform->hideIf('assesstimefinish', 'assessed', 'eq', 0);
             $mform->disabledIf('assesstimefinish', 'ratingtime');
         }
 
@@ -677,7 +677,7 @@ abstract class moodleform_mod extends moodleform {
             if (plugin_supports('mod', $this->_modname, FEATURE_COMPLETION_TRACKS_VIEWS, false)) {
                 $mform->addElement('checkbox', 'completionview', get_string('completionview', 'completion'),
                     get_string('completionview_desc', 'completion'));
-                $mform->disabledIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+                $mform->hideIf('completionview', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
                 // Check by default if automatic completion tracking is set.
                 if ($trackingdefault == COMPLETION_TRACKING_AUTOMATIC) {
                     $mform->setDefault('completionview', 1);
@@ -689,7 +689,7 @@ abstract class moodleform_mod extends moodleform {
             if (plugin_supports('mod', $this->_modname, FEATURE_GRADE_HAS_GRADE, false)) {
                 $mform->addElement('checkbox', 'completionusegrade', get_string('completionusegrade', 'completion'),
                     get_string('completionusegrade_desc', 'completion'));
-                $mform->disabledIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+                $mform->hideIf('completionusegrade', 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
                 $mform->addHelpButton('completionusegrade', 'completionusegrade', 'completion');
                 $gotcompletionoptions = true;
 
@@ -702,7 +702,7 @@ abstract class moodleform_mod extends moodleform {
             // Automatic completion according to module-specific rules
             $this->_customcompletionelements = $this->add_completion_rules();
             foreach ($this->_customcompletionelements as $element) {
-                $mform->disabledIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
+                $mform->hideIf($element, 'completion', 'ne', COMPLETION_TRACKING_AUTOMATIC);
             }
 
             $gotcompletionoptions = $gotcompletionoptions ||
@@ -719,7 +719,7 @@ abstract class moodleform_mod extends moodleform {
             $mform->addElement('date_time_selector', 'completionexpected', get_string('completionexpected', 'completion'),
                     array('optional' => true));
             $mform->addHelpButton('completionexpected', 'completionexpected', 'completion');
-            $mform->disabledIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
+            $mform->hideIf('completionexpected', 'completion', 'eq', COMPLETION_TRACKING_NONE);
         }
 
         // Populate module tags.
@@ -859,7 +859,7 @@ abstract class moodleform_mod extends moodleform {
                         get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
                     $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
                     if (!$this->_features->rating) {
-                        $mform->disabledIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
+                        $mform->hideIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
                     }
 
                 } else {
@@ -882,7 +882,7 @@ abstract class moodleform_mod extends moodleform {
                         grade_get_categories_menu($COURSE->id, $this->_outcomesused));
                 $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades');
                 if (!$this->_features->rating) {
-                    $mform->disabledIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
+                    $mform->hideIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
                 }
             }
 
@@ -892,9 +892,9 @@ abstract class moodleform_mod extends moodleform {
             $mform->setDefault('gradepass', '');
             $mform->setType('gradepass', PARAM_RAW);
             if (!$this->_features->rating) {
-                $mform->disabledIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
+                $mform->hideIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
             } else {
-                $mform->disabledIf('gradepass', 'assessed', 'eq', '0');
+                $mform->hideIf('gradepass', 'assessed', 'eq', '0');
             }
         }
     }
index d27e10f..3bba431 100644 (file)
@@ -503,7 +503,7 @@ class core_course_renderer extends plugin_renderer_base {
                     'type' => 'hidden', 'name' => 'completionstate', 'value' => $newstate));
                 $output .= html_writer::tag('button',
                     $this->output->pix_icon('i/completion-' . $completionicon, $imgalt),
-                        array('class' => 'btn btn-link', 'title' => $imgalt));
+                        array('class' => 'btn btn-link', 'aria-live' => 'assertive'));
                 $output .= html_writer::end_tag('div');
                 $output .= html_writer::end_tag('form');
             } else {
similarity index 68%
rename from blocks/myoverview/templates/no-courses.mustache
rename to course/templates/no-courses.mustache
index ba52e4a..48c962c 100644 (file)
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_myoverview/no-courses
+    @template core_course/no-courses
 
     This template renders the no courses message.
 
     Example context (json):
     {
-        "nocoursesimg": "https://moodlesite/theme/image.php/boost/block_myoverview/1535727318/courses"
+        "nocoursesimgurl": "https://moodlesite/theme/image.php/boost/block_recentlyaccessedcourses/1535727318/courses"
     }
 }}
-<div class="text-xs-center text-center m-t-1" data-region="empty-message">
+<div class="text-xs-center text-center m-t-3" data-region="empty-message">
     <img class="empty-placeholder-image-lg m-t-1"
          src="{{nocoursesimg}}"
-         alt="{{#str}} nocourses, block_myoverview {{/str}}"
+         alt="{{$nocoursestring}}{{#str}} nocourses, core {{/str}}{{/nocoursestring}}"
          role="presentation">
-    <p class="text-muted mt-3">{{#str}} nocourses, block_myoverview {{/str}}</p>
-</div>
+    <p class="text-muted mt-3">{{$nocoursestring}}{{#str}} nocourses, core {{/str}}{{/nocoursestring}}</p>
+</div>
\ No newline at end of file
@@ -15,9 +15,9 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_recentlyaccessedcourses/placeholder-course
+    @template core_course/placeholder-course
 
-    This template renders an course card item loading placeholder for the recentlyaccessedcourses block.
+    This template renders an course card item loading placeholder for multiple blocks.
 
     Example context (json):
     {}
@@ -15,9 +15,9 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template block_recentlyaccessedcourses/view-cards
+    @template core_course/view-cards
 
-    This template renders the carousel for the recentlyaccessedcourses block.
+    This template renders the carousel for the recentlyaccessedcourses & starredcourses blocks.
 
     Example context (json):
     {
diff --git a/course/tests/behat/app_courselist.feature b/course/tests/behat/app_courselist.feature
new file mode 100644 (file)
index 0000000..ea68c05
--- /dev/null
@@ -0,0 +1,120 @@
+@core @core_course @app @javascript
+Feature: Test course list shown on app start tab
+  In order to select a course
+  As a student
+  I need to see the correct list of courses
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname |
+      | Course 1 | C1        |
+      | Course 2 | C2        |
+    And the following "users" exist:
+      | username |
+      | student1 |
+      | student2 |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student1 | C1     | student |
+      | student2 | C1     | student |
+      | student2 | C2     | student |
+
+  Scenario: Student is registered on one course
+    When I enter the app
+    And I log in as "student1"
+    Then I should see "Course 1"
+    And I should not see "Course 2"
+
+  Scenario: Student is registered on two courses (shortnames not displayed)
+    When I enter the app
+    And I log in as "student2"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I should not see "C1"
+    And I should not see "C2"
+
+  Scenario: Student is registered on two courses (shortnames displayed)
+    Given the following config values are set as admin:
+      | courselistshortnames | 1 |
+    When I enter the app
+    And I log in as "student2"
+    Then I should see "Course 1"
+    And I should see "Course 2"
+    And I should see "C1"
+    And I should see "C2"
+
+  Scenario: Student uses course list to enter course, then leaves it again
+    When I enter the app
+    And I log in as "student2"
+    And I press "Course 2" near "Course overview" in the app
+    Then the header should be "Course 2" in the app
+    And I press the back button in the app
+    Then the header should be "Acceptance test site" in the app
+
+  Scenario: Student uses filter feature to reduce course list
+    Given the following config values are set as admin:
+      | courselistshortnames | 1 |
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Frog 3   | C3        |
+      | Frog 4   | C4        |
+      | Course 5 | C5        |
+      | Toad 6   | C6        |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student2 | C3     | student |
+      | student2 | C4     | student |
+      | student2 | C5     | student |
+      | student2 | C6     | student |
+    # Create bogus courses so that the main ones aren't shown in the 'recently accessed' part.
+    # Because these come later in alphabetical order, they may not be displayed in the lower part
+    # which is OK.
+    And the following "courses" exist:
+      | fullname | shortname |
+      | Zogus 1  | Z1        |
+      | Zogus 2  | Z2        |
+      | Zogus 3  | Z3        |
+      | Zogus 4  | Z4        |
+      | Zogus 5  | Z5        |
+      | Zogus 6  | Z6        |
+      | Zogus 7  | Z7        |
+      | Zogus 8  | Z8        |
+      | Zogus 9  | Z9        |
+      | Zogus 10 | Z10       |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student2 | Z1     | student |
+      | student2 | Z2     | student |
+      | student2 | Z3     | student |
+      | student2 | Z4     | student |
+      | student2 | Z5     | student |
+      | student2 | Z6     | student |
+      | student2 | Z7     | student |
+      | student2 | Z8     | student |
+      | student2 | Z9     | student |
+      | student2 | Z10    | student |
+    When I enter the app
+    And I log in as "student2"
+    Then I should see "C1"
+    And I should see "C2"
+    And I should see "C3"
+    And I should see "C4"
+    And I should see "C5"
+    And I should see "C6"
+    And I press "more" near "Course overview" in the app
+    And I press "Filter my courses" in the app
+    And I set the field "Filter my courses" to "fr" in the app
+    Then I should not see "C1"
+    And I should not see "C2"
+    And I should see "C3"
+    And I should see "C4"
+    And I should not see "C5"
+    And I should not see "C6"
+    And I press "more" near "Course overview" in the app
+    And I press "Filter my courses" in the app
+    Then I should see "C1"
+    And I should see "C2"
+    And I should see "C3"
+    And I should see "C4"
+    And I should see "C5"
+    And I should see "C6"
index d447b2b..b620e70 100644 (file)
@@ -19,6 +19,7 @@ Feature: View subfolders in a course in-line
     And I add a "Folder" to section "3" and I fill the form with:
       | Name | Test folder |
       | Display folder contents | On a separate page |
+      | Show subfolders expanded | |
     And I should see "Test folder"
     And I follow "Test folder"
     And I press "Edit"
@@ -42,6 +43,12 @@ Feature: View subfolders in a course in-line
     Then I should not see "Test subfolder 2"
     And I follow "Test folder"
     And I should see "Test subfolder 2"
+    Given I navigate to "Edit settings" in current page administration
+    And I set the field "Show subfolders expanded" to "1"
+    When I am on "Course 1" course homepage
+    Then I should not see "Test subfolder 2"
+    And I follow "Test folder"
+    And I should see "Test subfolder 2"
 
   @javascript
   Scenario: Make the subfolders viewable inline on the course page
@@ -51,14 +58,12 @@ Feature: View subfolders in a course in-line
     And I set the field "New folder name" to "Test sub subfolder"
     And I click on "button.fp-dlg-butcreate" "css_element" in the "div.fp-mkdir-dlg" "css_element"
     And I press "Save changes"
-    And I should see "Test sub subfolder"
     And I navigate to "Edit settings" in current page administration
-    And I set the field "Display folder contents" to "Inline on a course page"
-    And I set the field "Show subfolders expanded" to ""
+    When I set the field "Display folder contents" to "Inline on a course page"
     And I press "Save and return to course"
-    And I should see "Test subfolder 1"
+    Then I should see "Test subfolder 1"
     And I should not see "Test sub subfolder"
-    And I open "Test folder" actions menu
+    Given I open "Test folder" actions menu
     When I click on "Edit settings" "link" in the "Test folder" activity
     And I set the field "Show subfolders expanded" to "1"
     And I press "Save and return to course"
index dd34b53..a7d4a96 100644 (file)
@@ -105,20 +105,11 @@ class provider implements
             return;
         }
 
-        $params = [
-            'contextid' => $context->id,
-            'contextcourse' => CONTEXT_COURSE,
-        ];
-
         $sql = "SELECT ue.userid as userid
                   FROM {user_enrolments} ue
-                  JOIN {enrol} e
-                       ON e.id = ue.enrolid
-                  JOIN {context} ctx
-                       ON ctx.instanceid = e.courseid
-                       AND ctx.contextlevel = :contextcourse
-                 WHERE ctx.id = :contextid";
-
+                  JOIN {enrol} e ON e.id = ue.enrolid
+                 WHERE e.courseid = ?";
+        $params = [$context->instanceid];
         $userlist->add_from_sql('userid', $sql, $params);
     }
 
index 2ced5d6..bf66147 100644 (file)
Binary files a/enrol/manual/amd/build/form-potential-user-selector.min.js and b/enrol/manual/amd/build/form-potential-user-selector.min.js differ
index 066f59d..7fac602 100644 (file)
@@ -49,6 +49,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str'], function($, Ajax,
         transport: function(selector, query, success, failure) {
             var promise;
             var courseid = $(selector).attr('courseid');
+            var userfields = $(selector).attr('userfields').split(',');
             if (typeof courseid === "undefined") {
                 courseid = '1';
             }
@@ -78,7 +79,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str'], function($, Ajax,
                     $.each(results, function(index, user) {
                         var ctx = user,
                             identity = [];
-                        $.each(['idnumber', 'email', 'phone1', 'phone2', 'department', 'institution'], function(i, k) {
+                        $.each(userfields, function(i, k) {
                             if (typeof user[k] !== 'undefined' && user[k] !== '') {
                                 ctx.hasidentity = true;
                                 identity.push(user[k]);
index da4cd68..165a6e9 100644 (file)
@@ -98,7 +98,8 @@ class enrol_manual_enrol_users_form extends moodleform {
             'ajax' => 'enrol_manual/form-potential-user-selector',
             'multiple' => true,
             'courseid' => $course->id,
-            'enrolid' => $instance->id
+            'enrolid' => $instance->id,
+            'userfields' => implode(',', get_extra_user_fields($context))
         );
         $mform->addElement('autocomplete', 'userlist', get_string('selectusers', 'enrol_manual'), array(), $options);
 
index c5e3549..2afa506 100644 (file)
@@ -153,3 +153,28 @@ Feature: Teacher can search and enrol users one by one into the course
     When I set the field "Select users" to "example.com"
     And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
     Then I should see "Too many users (>100) to show"
+
+  @javascript
+  Scenario: Change the Show user identity setting affects the enrolment pop-up.
+    Given I log out
+    When I log in as "admin"
+    Then the following "users" exist:
+      | username    | firstname | lastname | email                   | phone1     | phone2     | department | institution | city    | country  |
+      | student100  | Student   | 100      | student100@example.com  | 1234567892 | 1234567893 | ABC1       | ABC2        | CITY1   | UK       |
+    And the following config values are set as admin:
+      | showuseridentity | idnumber,email,city,country,phone1,phone2,department,institution |
+    When I am on "Course 001" course homepage
+    Then I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "student100@example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "student100@example.com, CITY1, UK, 1234567892, 1234567893, ABC1, ABC2"
+    # Remove identity field in setting User policies
+    And the following config values are set as admin:
+      | showuseridentity | idnumber,email,phone1,phone2,department,institution |
+    When I am on "Course 001" course homepage
+    And I navigate to course participants
+    And I press "Enrol users"
+    When I set the field "Select users" to "student100@example.com"
+    And I click on ".form-autocomplete-downarrow" "css_element" in the "Select users" "form_row"
+    Then I should see "student100@example.com, 1234567892, 1234567893, ABC1, ABC2"
index 76fea61..1e984be 100644 (file)
@@ -139,7 +139,7 @@ echo '
         </div>
     </fieldset>
     <div class="fitem" style="text-align: center;">
-        <input id="id_addidnumbers" type="submit" value="'.get_string('addidnumbers', 'grades').'" name="addidnumbers" />
+        <input id="id_addidnumbers" type="submit" class="btn btn-secondary" value="' . get_string('addidnumbers', 'grades') . '" name="addidnumbers" />
     </div>
 </form>';
 
index ccd917a..bebede4 100644 (file)
@@ -462,7 +462,7 @@ class core_group_external extends external_api {
             $groupid = $member['groupid'];
             $userid = $member['userid'];
 
-            $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
+            $group = groups_get_group($groupid, '*', MUST_EXIST);
             $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
 
             // now security checks
@@ -540,7 +540,7 @@ class core_group_external extends external_api {
             $groupid = $member['groupid'];
             $userid = $member['userid'];
 
-            $group = groups_get_group($groupid, 'id, courseid', MUST_EXIST);
+            $group = groups_get_group($groupid, '*', MUST_EXIST);
             $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0, 'mnethostid'=>$CFG->mnet_localhost_id), '*', MUST_EXIST);
 
             // now security checks
@@ -986,7 +986,7 @@ class core_group_external extends external_api {
 
         foreach ($params['groupingids'] as $groupingid) {
 
-            if (!$grouping = groups_get_grouping($groupingid, 'id, courseid', IGNORE_MISSING)) {
+            if (!$grouping = groups_get_grouping($groupingid)) {
                 // Silently ignore attempts to delete nonexisting groupings.
                 continue;
             }
index e1af7ca..b04853c 100644 (file)
@@ -36,8 +36,6 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
 
     /**
      * Test create_groups
-     *
-     * @expectedException required_capability_exception
      */
     public function test_create_groups() {
         global $DB;
@@ -112,13 +110,13 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
 
         // Call without required capability
         $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
+
+        $this->expectException(\required_capability_exception::class);
         $froups = core_group_external::create_groups(array($group4));
     }
 
     /**
      * Test update_groups
-     *
-     * @expectedException required_capability_exception
      */
     public function test_update_groups() {
         global $DB;
@@ -191,13 +189,13 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         // Call without required capability.
         $group1data['idnumber'] = 'TEST1';
         $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
+
+        $this->expectException(\required_capability_exception::class);
         $groups = core_group_external::update_groups(array($group1data));
     }
 
     /**
      * Test get_groups
-     *
-     * @expectedException required_capability_exception
      */
     public function test_get_groups() {
         global $DB;
@@ -256,13 +254,13 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
 
         // Call without required capability
         $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
+
+        $this->expectException(\required_capability_exception::class);
         $groups = core_group_external::get_groups(array($group1->id, $group2->id));
     }
 
     /**
      * Test delete_groups
-     *
-     * @expectedException required_capability_exception
      */
     public function test_delete_groups() {
         global $DB;
@@ -305,6 +303,8 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
 
         // Call without required capability
         $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
+
+        $this->expectException(\required_capability_exception::class);
         $froups = core_group_external::delete_groups(array($group3->id));
     }
 
@@ -444,6 +444,59 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test delete_groupings.
+     */
+    public function test_delete_groupings() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $course = self::getDataGenerator()->create_course();
+
+        $groupingdata1 = array();
+        $groupingdata1['courseid'] = $course->id;
+        $groupingdata1['name'] = 'Grouping Test';
+        $groupingdata1['description'] = 'Grouping Test description';
+        $groupingdata1['descriptionformat'] = FORMAT_MOODLE;
+        $groupingdata2 = array();
+        $groupingdata2['courseid'] = $course->id;
+        $groupingdata2['name'] = 'Grouping Test';
+        $groupingdata2['description'] = 'Grouping Test description';
+        $groupingdata2['descriptionformat'] = FORMAT_MOODLE;
+        $groupingdata3 = array();
+        $groupingdata3['courseid'] = $course->id;
+        $groupingdata3['name'] = 'Grouping Test';
+        $groupingdata3['description'] = 'Grouping Test description';
+        $groupingdata3['descriptionformat'] = FORMAT_MOODLE;
+
+        $grouping1 = self::getDataGenerator()->create_grouping($groupingdata1);
+        $grouping2 = self::getDataGenerator()->create_grouping($groupingdata2);
+        $grouping3 = self::getDataGenerator()->create_grouping($groupingdata3);
+
+        // Set the required capabilities by the external function.
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id);
+        $this->assignUserCapability('moodle/course:view', $context->id, $roleid);
+
+        // Checks against DB values.
+        $groupingstotal = $DB->count_records('groupings', array());
+        $this->assertEquals(3, $groupingstotal);
+
+        // Call the external function.
+        core_group_external::delete_groupings(array($grouping1->id, $grouping2->id));
+
+        // Checks against DB values.
+        $groupingstotal = $DB->count_records('groupings', array());
+        $this->assertEquals(1, $groupingstotal);
+
+        // Call without required capability.
+        $this->unassignUserCapability('moodle/course:managegroups', $context->id, $roleid);
+
+        $this->expectException(\required_capability_exception::class);
+        core_group_external::delete_groupings(array($grouping3->id));
+    }
+
     /**
      * Test get_groups
      */
@@ -729,4 +782,114 @@ class core_group_externallib_testcase extends externallib_advanced_testcase {
         }
 
     }
+
+    /**
+     * Test add_group_members.
+     */
+    public function test_add_group_members() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $student3 = self::getDataGenerator()->create_user();
+
+        $course = self::getDataGenerator()->create_course();
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id);
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Group Test 1';
+        $group1data['description'] = 'Group Test 1 description';
+        $group1data['idnumber'] = 'TEST1';
+        $group1 = self::getDataGenerator()->create_group($group1data);
+
+        // Checks against DB values.
+        $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]);
+        $this->assertEquals(0, $memberstotal);
+
+        // Set the required capabilities by the external function.
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id);
+        $this->assignUserCapability('moodle/course:view', $context->id, $roleid);
+
+        core_group_external::add_group_members([
+            'members' => [
+                'groupid' => $group1->id,
+                'userid' => $student1->id,
+            ]
+        ]);
+        core_group_external::add_group_members([
+            'members' => [
+                'groupid' => $group1->id,
+                'userid' => $student2->id,
+            ]
+        ]);
+        core_group_external::add_group_members([
+            'members' => [
+                'groupid' => $group1->id,
+                'userid' => $student3->id,
+            ]
+        ]);
+
+        // Checks against DB values.
+        $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]);
+        $this->assertEquals(3, $memberstotal);
+    }
+
+    /**
+     * Test delete_group_members.
+     */
+    public function test_delete_group_members() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $student1 = self::getDataGenerator()->create_user();
+        $student2 = self::getDataGenerator()->create_user();
+        $student3 = self::getDataGenerator()->create_user();
+
+        $course = self::getDataGenerator()->create_course();
+
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id);
+        $this->getDataGenerator()->enrol_user($student3->id, $course->id, $studentrole->id);
+
+        $group1data = array();
+        $group1data['courseid'] = $course->id;
+        $group1data['name'] = 'Group Test 1';
+        $group1data['description'] = 'Group Test 1 description';
+        $group1data['idnumber'] = 'TEST1';
+        $group1 = self::getDataGenerator()->create_group($group1data);
+
+        groups_add_member($group1->id, $student1->id);
+        groups_add_member($group1->id, $student2->id);
+        groups_add_member($group1->id, $student3->id);
+
+        // Checks against DB values.
+        $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]);
+        $this->assertEquals(3, $memberstotal);
+
+        // Set the required capabilities by the external function.
+        $context = context_course::instance($course->id);
+        $roleid = $this->assignUserCapability('moodle/course:managegroups', $context->id);
+        $this->assignUserCapability('moodle/course:view', $context->id, $roleid);
+
+        core_group_external::delete_group_members([
+            'members' => [
+                'groupid' => $group1->id,
+                'userid' => $student2->id,
+            ]
+        ]);
+
+        // Checks against DB values.
+        $memberstotal = $DB->count_records('groups_members', ['groupid' => $group1->id]);
+        $this->assertEquals(2, $memberstotal);
+    }
 }
index 0e18336..3286cd5 100644 (file)
@@ -40,3 +40,4 @@ $string['cliunknowoption'] = 'Nepoznate opcije: {$a} Molimo koristite --help opc
 $string['cliyesnoprompt'] = 'unesite y (znači da) ili n (znači ne)';
 $string['environmentrequireinstall'] = 'je neophodno instalirati/omogućiti';
 $string['environmentrequireversion'] = 'neophodna inačica je {$a->needed}, a vi trenutačno koristite inačicu {$a->current}';
+$string['upgradekeyset'] = 'Ključ za ažuriranje (ostavite prazno kako ga ne bi zadali)';
index 578ef82..a535604 100644 (file)
@@ -73,9 +73,9 @@ $string['pathssubdataroot'] = '<p>ユーザによってアップロードされ
 <p>ウェブからは直接アクセスできないようにしてください。</p>
 <p>現在ディレクトリが存在しない場合、インストレーションプロセスは作成を試みます。</p';
 $string['pathssubdirroot'] = '<p>Moodleコードを含むディレクトリに関するフルパスです。</p>';
-$string['pathssubwwwroot'] = '<p>Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\93ã\81¨ã\81®ã\81§ã\81\8dã\82\8bã\83\95ã\83«ã\82¦ã\82§ã\83\96ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82ä¾\8bã\81\88ã\81°ã\83¦ã\83¼ã\82¶ã\81\8cã\83\96ã\83©ã\82¦ã\82¶ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\83\90ã\83¼ã\81«å\85¥å\8a\9bã\81\97ã\81¦Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\9fã\82\81ã\81®ã\82¢ã\83\89ã\83¬ã\82¹ã\81§ã\81\99ã\80\82</p>
+$string['pathssubwwwroot'] = '<p>Moodleにアクセスできるフルウェブアドレスです。例えばユーザがブラウザのアドレスバーに入力してMoodleにアクセスするためのアドレスです。</p>
 
-<p>è¤\87æ\95°ã\82¢ã\83\89ã\83¬ã\82¹ã\82\92使ç\94¨ã\81\97ã\81¦Moodleã\81«ã\82¢ã\82¯ã\82»ã\82¹ã\81\99ã\82\8bã\81\93ã\81¨はできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
+<p>è¤\87æ\95°ã\82¢ã\83\89ã\83¬ã\82¹ã\82\92使ç\94¨ã\81\97ã\81\9fMoodleã\81¸ã\81®ã\82¢ã\82¯ã\82»ã\82¹はできません。あなたのサイトに複数アドレスからアクセスできる場合、最も簡単なアドレスを選択して、すべてのアドレスにパーマネントリダイレクトを設定してください。</p>
 
 <p>あなたのサイトにインターネットおよび内部ネットワーク (イントラネットと呼ばれます) からアクセスできる場合、ここではパブリックアドレスを使用してください。</p>
 
index 1b3b845..707818c 100644 (file)
@@ -417,6 +417,7 @@ $string['cron_link'] = 'admin/cron';
 $string['cronclionly'] = 'Cron execution via command line only';
 $string['cronerrorclionly'] = 'Sorry, internet access to this page has been disabled by the administrator.';
 $string['cronerrorpassword'] = 'Sorry, you have not provided a valid password to access this page';
+$string['croninfrequent'] = 'The time between the last two runs of the cron maintenance script was over {$a} seconds. We recommend configuring it to run more frequently.';
 $string['cronremotepassword'] = 'Cron password for remote access';
 $string['cronwarning'] = 'The <a href="{$a}">cron.php maintenance script</a> has not been run for at least 24 hours.';
 $string['cronwarningcli'] = 'The cli/cron.php maintenance script has not been run for at least 24 hours.';
index 42377c7..4047987 100644 (file)
@@ -107,6 +107,7 @@ $string['invalidpersistenterror'] = 'Error: {$a}';
 $string['invalidplan'] = 'Invalid learning plan';
 $string['invalidtaxonomy'] = 'Invalid taxonomy: {$a}';
 $string['invalidurl'] = 'The URL is not valid. Make sure it starts with \'http://\' or \'https://\'.';
+$string['nouserplanswithcompetency'] = 'No learning plans contain this competency.';
 $string['planstatusactive'] = 'Active';
 $string['planstatuscomplete'] = 'Complete';
 $string['planstatusdraft'] = 'Draft';
index 43131c2..279943f 100644 (file)
@@ -3542,19 +3542,21 @@ function get_users_by_capability(context $context, $capability, $fields = '', $s
     if ($groups) {
         $groups = (array)$groups;
         list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
-        $grouptest = "u.id IN (SELECT userid FROM {groups_members} gm WHERE gm.groupid $grouptest)";
+        $joins[] = "LEFT OUTER JOIN (SELECT DISTINCT userid
+                                       FROM {groups_members}
+                                      WHERE groupid $grouptest
+                                    ) gm ON gm.userid = u.id";
+
         $params = array_merge($params, $grpparams);
 
+        $grouptest = 'gm.userid IS NOT NULL';
         if ($useviewallgroups) {
             $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
             if (!empty($viewallgroupsusers)) {
-                $wherecond[] =  "($grouptest OR u.id IN (" . implode(',', array_keys($viewallgroupsusers)) . '))';
-            } else {
-                $wherecond[] =  "($grouptest)";
+                $grouptest .= ' OR u.id IN (' . implode(',', array_keys($viewallgroupsusers)) . ')';
             }
-        } else {
-            $wherecond[] =  "($grouptest)";
         }
+        $wherecond[] = "($grouptest)";
     }
 
     // User exceptions
diff --git a/lib/amd/build/checkbox-toggleall.min.js b/lib/amd/build/checkbox-toggleall.min.js
new file mode 100644 (file)
index 0000000..90e5ebc
Binary files /dev/null and b/lib/amd/build/checkbox-toggleall.min.js differ
index 7b10704..2a6fd40 100644 (file)
Binary files a/lib/amd/build/icon_system_fontawesome.min.js and b/lib/amd/build/icon_system_fontawesome.min.js differ
index 01fd0c6..a82abd9 100644 (file)
Binary files a/lib/amd/build/storagewrapper.min.js and b/lib/amd/build/storagewrapper.min.js differ
diff --git a/lib/amd/src/checkbox-toggleall.js b/lib/amd/src/checkbox-toggleall.js
new file mode 100644 (file)
index 0000000..c6d4103
--- /dev/null
@@ -0,0 +1,126 @@
+// 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/>.
+
+/**
+ * A module to help with toggle select/deselect all.
+ *
+ * @module     core/checkbox-toggleall
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/pubsub'], function($, PubSub) {
+
+    var registered = false;
+
+    var events = {
+        checkboxToggled: 'core/checkbox-toggleall:checkboxToggled',
+    };
+
+    var getAllCheckboxes = function(root, toggleGroup) {
+        return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
+    };
+
+    var getAllSlaveCheckboxes = function(root, toggleGroup) {
+        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="slave"]');
+    };
+
+    var getControlCheckboxes = function(root, toggleGroup) {
+        return getAllCheckboxes(root, toggleGroup).filter('[data-toggle="master"]');
+    };
+
+    var toggleSlavesFromMasters = function(e) {
+        var root = e.data.root;
+        var target = $(e.target);
+
+        var toggleGroupName = target.data('togglegroup');
+        var targetState = target.is(':checked');
+
+        var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
+        var checkedSlaves = slaves.filter(':checked');
+
+        setMasterStates(root, toggleGroupName, targetState);
+
+        // Set the slave checkboxes from the masters.
+        slaves.prop('checked', targetState);
+
+        PubSub.publish(events.checkboxToggled, {
+            root: root,
+            toggleGroupName: toggleGroupName,
+            slaves: slaves,
+            checkedSlaves: checkedSlaves,
+            anyChecked: targetState,
+        });
+    };
+
+    var toggleMastersFromSlaves = function(e) {
+        var root = e.data.root;
+        var target = $(e.target);
+
+        var toggleGroupName = target.data('togglegroup');
+
+        var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
+        var checkedSlaves = slaves.filter(':checked');
+        var targetState = (slaves.length === checkedSlaves.length);
+
+        setMasterStates(root, toggleGroupName, targetState);
+
+        PubSub.publish(events.checkboxToggled, {
+            root: root,
+            toggleGroupName: toggleGroupName,
+            slaves: slaves,
+            checkedSlaves: checkedSlaves,
+            anyChecked: !!checkedSlaves.length,
+        });
+    };
+
+    var setMasterStates = function(root, toggleGroupName, targetState) {
+        // Set the master checkboxes value and ARIA labels..
+        var masters = getControlCheckboxes(root, toggleGroupName);
+        masters.prop('checked', targetState);
+        masters.each(function(i, masterCheckbox) {
+            masterCheckbox = $(masterCheckbox);
+            var masterLabel = root.find('[for="' + masterCheckbox.attr('id') + '"]');
+            var targetString;
+            if (masterLabel.length) {
+                if (targetState) {
+                    targetString = masterCheckbox.data('toggle-deselectall');
+                } else {
+                    targetString = masterCheckbox.data('toggle-selectall');
+                }
+
+                if (masterLabel.html() !== targetString) {
+                    masterLabel.html(targetString);
+                }
+            }
+        });
+    };
+
+    var registerListeners = function() {
+        if (!registered) {
+            registered = true;
+
+            var root = $(document.body);
+            root.on('change', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
+            root.on('change', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
+        }
+    };
+
+    return {
+        init: function() {
+            registerListeners();
+        },
+        events: events,
+    };
+});
index c17f551..970b0b9 100644 (file)
@@ -105,7 +105,8 @@ define(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/locals
             unmappedIcon: unmappedIcon
         };
 
-        return Mustache.render(template, context);
+        var result = Mustache.render(template, context);
+        return result.trim();
     };
 
     /**
index 7f7f3e6..ce92e39 100644 (file)
@@ -35,7 +35,8 @@ define(['core/config'], function(config) {
         this.hashSource = config.wwwroot + '/' + config.jsrev;
         this.hash = this.hashString(this.hashSource);
         this.prefix = this.hash + '/';
-        this.jsrevPrefix = this.hash + '/jsrev';
+        this.jsrevPrefix = this.hashString(config.wwwroot) + '/jsrev';
+        this.validateCache();
     };
 
     /**
@@ -89,8 +90,8 @@ define(['core/config'], function(config) {
             this.storage.setItem(this.jsrevPrefix, config.jsrev);
             return;
         }
-        var moodleVersion = config.jsrev;
 
+        var moodleVersion = config.jsrev;
         if (moodleVersion != cacheVersion) {
             this.storage.clear();
             this.storage.setItem(this.jsrevPrefix, config.jsrev);
@@ -132,7 +133,6 @@ define(['core/config'], function(config) {
         if (!this.supported) {
             return false;
         }
-        this.validateCache();
         key = this.prefixKey(key);
 
         return this.storage.getItem(key);
@@ -150,7 +150,6 @@ define(['core/config'], function(config) {
         if (!this.supported) {
             return false;
         }
-        this.validateCache();
         key = this.prefixKey(key);
         // This can throw exceptions when the storage limit is reached.
         try {
index a6cca8a..cd8f0dc 100644 (file)
@@ -474,6 +474,21 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
     }
 
+    /**
+     * Checks if the current page is part of the mobile app.
+     *
+     * @return bool True if it's in the app
+     */
+    protected function is_in_app() : bool {
+        // Cannot be in the app if there's no @app tag on scenario.
+        if (!$this->has_tag('app')) {
+            return false;
+        }
+
+        // Check on page to see if it's an app page. Safest way is to look for added JavaScript.
+        return $this->getSession()->evaluateScript('typeof window.behat') === 'object';
+    }
+
     /**
      * Spins around an element until it exists
      *
@@ -647,6 +662,16 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext {
         return;
     }
 
+    /**
+     * Checks if the current scenario, or its feature, has a specified tag.
+     *
+     * @param string $tag Tag to check
+     * @return bool True if the tag exists in scenario or feature
+     */
+    public function has_tag(string $tag) : bool {
+        return array_key_exists($tag, behat_hooks::get_tags_for_scenario());
+    }
+
     /**
      * Change browser window size.
      *   - small: 640x480
index 0070634..2d23185 100644 (file)
@@ -219,6 +219,12 @@ class behat_command {
             return BEHAT_EXITCODE_CONFIG;
         }
 
+        // If app config is supplied, check the value is correct.
+        if (!empty($CFG->behat_ionic_dirroot) && !file_exists($CFG->behat_ionic_dirroot . '/ionic.config.json')) {
+            self::output_msg(get_string('errorapproot', 'tool_behat'));
+            return BEHAT_EXITCODE_CONFIG;
+        }
+
         return 0;
     }
 
index e393580..6738ef0 100644 (file)
@@ -620,6 +620,42 @@ class behat_config_util {
 
         // Check suite values.
         $behatprofilesuites = array();
+
+        // Automatically set tags information to skip app testing if necessary. We skip app testing
+        // if the browser is not Chrome. (Note: We also skip if it's not configured, but that is
+        // done on the theme/suite level.)
+        if (empty($values['browser']) || $values['browser'] !== 'chrome') {
+            if (!empty($values['tags'])) {
+                $values['tags'] .= ' && ~@app';
+            } else {
+                $values['tags'] = '~@app';
+            }
+        }
+
+        // Automatically add Chrome command line option to skip the prompt about allowing file
+        // storage - needed for mobile app testing (won't hurt for everything else either).
+        if (!empty($values['browser']) && $values['browser'] === 'chrome') {
+            if (!isset($values['capabilities'])) {
+                $values['capabilities'] = [];
+            }
+            if (!isset($values['capabilities']['chrome'])) {
+                $values['capabilities']['chrome'] = [];
+            }
+            if (!isset($values['capabilities']['chrome']['switches'])) {
+                $values['capabilities']['chrome']['switches'] = [];
+            }
+            $values['capabilities']['chrome']['switches'][] = '--unlimited-storage';
+
+            // If the mobile app is enabled, check its version and add appropriate tags.
+            if ($mobiletags = $this->get_mobile_version_tags()) {
+                if (!empty($values['tags'])) {
+                    $values['tags'] .= ' && ' . $mobiletags;
+                } else {
+                    $values['tags'] = $mobiletags;
+                }
+            }
+        }
+
         // Fill tags information.
         if (isset($values['tags'])) {
             $behatprofilesuites = array(
@@ -658,6 +694,102 @@ class behat_config_util {
         return array($profile => array_merge($behatprofilesuites, $behatprofileextension));
     }
 
+    /**
+     * Gets version tags to use for the mobile app.
+     *
+     * This is based on the current mobile app version (from its package.json) and all known
+     * mobile app versions (based on the list appversions.json in the lib/behat directory).
+     *
+     * @param bool $verbose If true, outputs information about installed app version
+     * @return string List of tags or '' if not supporting mobile
+     */
+    protected function get_mobile_version_tags($verbose = true) : string {
+        global $CFG;
+
+        if (!empty($CFG->behat_ionic_dirroot)) {
+            // Get app version from package.json.
+            $jsonpath = $CFG->behat_ionic_dirroot . '/package.json';
+            $json = @file_get_contents($jsonpath);
+            if (!$json) {
+                throw new coding_exception('Unable to load app version from ' . $jsonpath);
+            }
+            $package = json_decode($json);
+            if ($package === null || empty($package->version)) {
+                throw new coding_exception('Invalid app package data in ' . $jsonpath);
+            }
+            $installedversion = $package->version;
+        } else if (!empty($CFG->behat_ionic_wwwroot)) {
+            // Get app version from config.json inside wwwroot.
+            $jsonurl = $CFG->behat_ionic_wwwroot . '/config.json';
+            $json = @download_file_content($jsonurl);
+            if (!$json) {
+                throw new coding_exception('Unable to load app version from ' . $jsonurl);
+            }
+            $config = json_decode($json);
+            if ($config === null || empty($config->versionname)) {
+                throw new coding_exception('Invalid app config data in ' . $jsonurl);
+            }
+            $installedversion = str_replace('-dev', '', $config->versionname);
+        } else {
+            return '';
+        }
+
+        // Read all feature files to check which mobile tags are used. (Note: This could be cached
+        // but ideally, it is the sort of thing that really ought to be refreshed by doing a new
+        // Behat init. Also, at time of coding it only takes 0.3 seconds and only if app enabled.)
+        $usedtags = [];
+        foreach ($this->features as $filepath) {
+            $feature = file_get_contents($filepath);
+            // This may incorrectly detect versions used e.g. in a comment or something, but it
+            // doesn't do much harm if we have extra ones.
+            if (preg_match_all('~@app_(?:from|upto)(?:[0-9]+(?:\.[0-9]+)*)~', $feature, $matches)) {
+                foreach ($matches[0] as $tag) {
+                    // Store as key in array so we don't get duplicates.
+                    $usedtags[$tag] = true;
+                }
+            }
+        }
+
+        // Set up relevant tags for each version.
+        $tags = [];
+        foreach ($usedtags as $usedtag => $ignored) {
+            if (!preg_match('~^@app_(from|upto)([0-9]+(?:\.[0-9]+)*)$~', $usedtag, $matches)) {
+                throw new coding_exception('Unexpected tag format');
+            }
+            $direction = $matches[1];
+            $version = $matches[2];
+
+            switch (version_compare($installedversion, $version)) {
+                case -1:
+                    // Installed version OLDER than the one being considered, so do not
+                    // include any scenarios that only run from the considered version up.
+                    if ($direction === 'from') {
+                        $tags[] = '~@app_from' . $version;
+                    }
+                    break;
+
+                case 0:
+                    // Installed version EQUAL to the one being considered - no tags need
+                    // excluding.
+                    break;
+
+                case 1:
+                    // Installed version NEWER than the one being considered, so do not
+                    // include any scenarios that only run up to that version.
+                    if ($direction === 'upto') {
+                        $tags[] = '~@app_upto' . $version;
+                    }
+                    break;
+            }
+        }
+
+        if ($verbose) {
+            mtrace('Configured app tests for version ' . $installedversion);
+        }
+
+        return join(' && ', $tags);
+    }
+
     /**
      * Attempt to split feature list into fairish buckets using timing information, if available.
      * Simply add each one to lightest buckets until all files allocated.
@@ -1237,12 +1369,20 @@ class behat_config_util {
      * @return array ($blacklistfeatures, $blacklisttags, $features)
      */
     protected function get_behat_features_for_theme($theme) {
+        global $CFG;
 
         // Get list of features defined by theme.
         $themefeatures = $this->get_tests_for_theme($theme, 'features');
         $themeblacklistfeatures = $this->get_blacklisted_tests_for_theme($theme, 'features');
         $themeblacklisttags = $this->get_blacklisted_tests_for_theme($theme, 'tags');
 
+        // Mobile app tests are not theme-specific, so run only for the default theme (and if
+        // configured).
+        if ((empty($CFG->behat_ionic_dirroot) && empty($CFG->behat_ionic_wwwroot)) ||
+                $theme !== $this->get_default_theme()) {
+            $themeblacklisttags[] = '@app';
+        }
+
         // Clean feature key and path.
         $features = array();
         $blacklistfeatures = array();
index d6da75c..bb011e0 100644 (file)
@@ -136,6 +136,16 @@ class behat_form_field {
         return $instance->matches($expectedvalue);
     }
 
+    /**
+     * Get the value of an attribute set on this field.
+     *
+     * @param string $name The attribute name
+     * @return string The attribute value
+     */
+    public function get_attribute($name) {
+        return $this->field->getAttribute($name);
+    }
+
     /**
      * Guesses the element type we are dealing with in case is not a text-based element.
      *
index 46184a0..5b87b68 100644 (file)
@@ -122,7 +122,8 @@ class courses extends \core_analytics\local\analyser\by_course {
      * @return array array(string, \renderable)
      */
     public function sample_description($sampleid, $contextid, $sampledata) {
-        $description = format_string($sampledata['course']->fullname, true, array('context' => $sampledata['context']));
+        $description = format_string(
+            get_course_display_name_for_list($sampledata['course']), true, array('context' => $sampledata['context']));
         $courseimage = new \pix_icon('i/course', get_string('course'));
         return array($description, $courseimage);
     }
index b2fdc0f..aa3b50e 100644 (file)
@@ -131,7 +131,8 @@ class site_courses extends \core_analytics\local\analyser\sitewide {
      * @return array array(string, \renderable)
      */
     public function sample_description($sampleid, $contextid, $sampledata) {
-        $description = format_string($sampledata['course']->fullname, true, array('context' => $sampledata['context']));
+        $description = format_string(
+            get_course_display_name_for_list($sampledata['course']), true, array('context' => $sampledata['context']));
         $courseimage = new \pix_icon('i/course', get_string('course'));
         return array($description, $courseimage);
     }
index 0ed5355..cb46366 100644 (file)
@@ -446,6 +446,9 @@ class icon_system_fontawesome extends icon_system_font {
         if (!$subpix->is_mapped()) {
             $data['unmappedIcon'] = $icon->export_for_template($output);
         }
+        if (isset($icon->attributes['aria-hidden'])) {
+            $data['aria-hidden'] = $icon->attributes['aria-hidden'];
+        }
         return $output->render_from_template('core/pix_icon_fontawesome', $data);
     }
 
index 7341ad1..8de7602 100644 (file)
@@ -61,6 +61,14 @@ function cron_run() {
     $timenow  = time();
     mtrace("Server Time: ".date('r', $timenow)."\n\n");
 
+    // Record start time and interval between the last cron runs.
+    $laststart = get_config('tool_task', 'lastcronstart');
+    set_config('lastcronstart', $timenow, 'tool_task');
+    if ($laststart) {
+        // Record the interval between last two runs (always store at least 1 second).
+        set_config('lastcroninterval', max(1, $timenow - $laststart), 'tool_task');
+    }
+
     // Run all scheduled tasks.
     cron_run_scheduled_tasks($timenow);
 
index 7a08841..9c0593c 100644 (file)
@@ -104,7 +104,10 @@ $messageproviders = array (
 
     // User insights.
     'insights' => array (
-         'capability'  => 'moodle/analytics:listinsights'
+        'defaults' => [
+            'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDIN + MESSAGE_DEFAULT_LOGGEDOFF,
+            'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_LOGGEDOFF,
+        ]
     ),
 
     // Message contact requests.
index cf8ad44..e9bbae5 100644 (file)
@@ -2719,5 +2719,14 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019011801.03);
     }
 
+    if ($oldversion < 2019021500.01) {
+        $insights = $DB->get_record('message_providers', ['component' => 'moodle', 'name' => 'insights']);
+        if (!empty($insights)) {
+            $insights->capability = null;
+            $DB->update_record('message_providers', $insights);
+        }
+        upgrade_main_savepoint(true, 2019021500.01);
+    }
+
     return true;
 }
diff --git a/lib/form/amd/build/showadvanced.min.js b/lib/form/amd/build/showadvanced.min.js
new file mode 100644 (file)
index 0000000..fb4ccde
Binary files /dev/null and b/lib/form/amd/build/showadvanced.min.js differ
diff --git a/lib/form/amd/src/showadvanced.js b/lib/form/amd/src/showadvanced.js
new file mode 100644 (file)
index 0000000..a9f9645
--- /dev/null
@@ -0,0 +1,219 @@
+// 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/>.
+
+/**
+ * A class to help show and hide advanced form content.
+ *
+ * @module     core_form/showadvanced
+ * @class      showadvanced
+ * @package    core_form
+ * @copyright  2016 Damyon Wiese <damyon@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/log', 'core/str', 'core/notification'], function($, Log, Strings, Notification) {
+
+    var SELECTORS = {
+            FIELDSETCONTAINSADVANCED: 'fieldset.containsadvancedelements',
+            DIVFITEMADVANCED: 'div.fitem.advanced',
+            DIVFCONTAINER: 'div.fcontainer',
+            MORELESSLINK: 'fieldset.containsadvancedelements .moreless-toggler'
+        },
+        CSS = {
+            SHOW: 'show',
+            MORELESSACTIONS: 'moreless-actions',
+            MORELESSTOGGLER: 'moreless-toggler',
+            SHOWLESS: 'moreless-less'
+        },
+        WRAPPERS = {
+            FITEM: '<div class="fitem"></div>',
+            FELEMENT: '<div class="felement"></div>'
+        },
+        IDPREFIX = 'showadvancedid-';
+
+    /** @type {Integer} uniqIdSeed Auto incrementing number used to generate ids. */
+    var uniqIdSeed = 0;
+
+    /**
+     * ShowAdvanced behaviour class.
+     * @param {String} id The id of the form.
+     */
+    var ShowAdvanced = function(id) {
+        this.id = id;
+
+        var form = $(document.getElementById(id));
+        this.enhanceForm(form);
+    };
+
+    /** @type {String} id The form id to enhance. */
+    ShowAdvanced.prototype.id = '';
+
+    /**
+     * @method enhanceForm
+     * @param {JQuery} form JQuery selector representing the form
+     * @return {ShowAdvanced}
+     */
+    ShowAdvanced.prototype.enhanceForm = function(form) {
+        var fieldsets = form.find(SELECTORS.FIELDSETCONTAINSADVANCED);
+
+        // Enhance each fieldset in the form matching the selector.
+        fieldsets.each(function(index, item) {
+            this.enhanceFieldset($(item));
+        }.bind(this));
+
+        // Attach some event listeners.
+        // Subscribe more/less links to click event.
+        form.on('click', SELECTORS.MORELESSLINK, this.switchState);
+
+        // Subscribe to key events but filter for space or enter.
+        form.on('keydown', SELECTORS.MORELESSLINK, function(e) {
+            // Enter or space.
+            if (e.which == 13 || e.which == 32) {
+                return this.switchState(e);
+            }
+            return true;
+        }.bind(this));
+        return this;
+    };
+
+
+    /**
+     * Generates a uniq id for the dom element it's called on unless the element already has an id.
+     * The id is set on the dom node before being returned.
+     *
+     * @method generateId
+     * @param {JQuery} node JQuery selector representing a single DOM Node.
+     * @return {String}
+     */
+    ShowAdvanced.prototype.generateId = function(node) {
+        var id = node.prop('id');
+        if (typeof id === 'undefined') {
+            id = IDPREFIX + (uniqIdSeed++);
+            node.prop('id', id);
+        }
+        return id;
+    };
+
+    /**
+     * @method enhanceFieldset
+     * @param {JQuery} fieldset JQuery selector representing a fieldset
+     * @return {ShowAdvanced}
+     */
+    ShowAdvanced.prototype.enhanceFieldset = function(fieldset) {
+        var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
+        if (!statuselement.length) {
+            Log.debug("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '" +
+                fieldset.prop('id') + "'");
+            return this;
+        }
+
+        // Fetch some strings.
+        Strings.get_strings([{
+            key: 'showmore',
+            component: 'core_form'
+        }, {
+            key: 'showless',
+            component: 'core_form'
+        }]).then(function(results) {
+            var showmore = results[0],
+                showless = results[1];
+
+            // Generate more/less links.
+            var morelesslink = $('<a href="#"></a>');
+            morelesslink.addClass(CSS.MORELESSTOGGLER);
+            if (statuselement.val() === '0') {
+                morelesslink.html(showmore);
+            } else {
+                morelesslink.html(showless);
+                morelesslink.addClass(CSS.SHOWLESS);
+                fieldset.find(SELECTORS.DIVFITEMADVANCED).addClass(CSS.SHOW);
+            }
+            // Build a list of advanced fieldsets.
+            var idlist = [];
+            fieldset.find(SELECTORS.DIVFITEMADVANCED).each(function(index, node) {
+                idlist[idlist.length] = this.generateId($(node));
+            }.bind(this));
+
+            // Set aria attributes.
+            morelesslink.attr('role', 'button');
+            morelesslink.attr('aria-controls', idlist.join(' '));
+
+            // Add elements to the DOM.
+            var fitem = $(WRAPPERS.FITEM);
+            fitem.addClass(CSS.MORELESSACTIONS);
+            var felement = $(WRAPPERS.FELEMENT);
+            felement.append(morelesslink);
+            fitem.append(felement);
+
+            fieldset.find(SELECTORS.DIVFCONTAINER).append(fitem);
+            return true;
+        }.bind(this)).fail(Notification.exception);
+
+        return this;
+    };
+
+    /**
+     * @method switchState
+     * @param {Event} e Event that triggered this action.
+     * @return {Boolean}
+     */
+    ShowAdvanced.prototype.switchState = function(e) {
+        e.preventDefault();
+
+        // Fetch some strings.
+        Strings.get_strings([{
+            key: 'showmore',
+            component: 'core_form'
+        }, {
+            key: 'showless',
+            component: 'core_form'
+        }]).then(function(results) {
+            var showmore = results[0],
+                showless = results[1],
+                fieldset = $(e.target).closest(SELECTORS.FIELDSETCONTAINSADVANCED);
+
+            // Toggle collapsed class.
+            fieldset.find(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.SHOW);
+
+            // Get corresponding hidden variable.
+            var statuselement = $('input[name=mform_showmore_' + fieldset.prop('id') + ']');
+
+            // Invert it and change the link text.
+            if (statuselement.val() === '0') {
+                statuselement.val(1);
+                $(e.target).addClass(CSS.SHOWLESS);
+                $(e.target).html(showless);
+            } else {
+                statuselement.val(0);
+                $(e.target).removeClass(CSS.SHOWLESS);
+                $(e.target).html(showmore);
+            }
+            return true;
+        }).fail(Notification.exception);
+
+        return this;
+    };
+
+    return {
+        /**
+         * Initialise this module.
+         * @method init
+         * @param {String} formid
+         * @return {ShowAdvanced}
+         */
+        init: function(formid) {
+            return new ShowAdvanced(formid);
+        }
+    };
+});