Merge branch 'MDL-63938-master' of https://github.com/HuongNV13/moodle
authorEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Feb 2019 18:07:14 +0000 (19:07 +0100)
committerEloy Lafuente (stronk7) <stronk7@moodle.org>
Tue, 19 Feb 2019 18:07:14 +0000 (19:07 +0100)
89 files changed:
admin/tool/analytics/classes/output/invalid_analysables.php
admin/tool/analytics/lang/en/tool_analytics.php
admin/tool/analytics/templates/invalid_analysables.mustache
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/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
blocks/login/block_login.php
cache/classes/loaders.php
cache/tests/cache_test.php
cache/tests/fixtures/lib.php
competency/classes/api.php
competency/tests/api_test.php
course/modlib.php
lang/en/competency.php
lib/amd/build/checkbox-toggleall.min.js [new file with mode: 0644]
lib/amd/src/checkbox-toggleall.js [new file with mode: 0644]
lib/behat/form_field/behat_form_field.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/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/templates/loginform.mustache
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/templates/preferences_processor.mustache
message/tests/privacy_provider_test.php
mod/assign/classes/event/base.php
mod/assign/locallib.php
mod/assign/submission/file/locallib.php
mod/assign/submission/onlinetext/locallib.php
mod/assign/tests/events_test.php
question/amd/build/qbankmanager.min.js [new file with mode: 0644]
question/amd/src/qbankmanager.js [new file with mode: 0644]
question/classes/bank/checkbox_column.php
question/classes/bank/view.php
question/tests/behat/select_questions.feature [new file with mode: 0644]
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]
theme/boost/layout/columns2.php
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/core/help_icon.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/editadvanced_form.php
user/editlib.php
user/language_form.php
user/lib.php
user/tests/behat/behat_user.php
user/tests/behat/input-purpose.feature [new file with mode: 0644]
version.php

index a5bda09..0bb3902 100644 (file)
@@ -153,6 +153,12 @@ class invalid_analysables implements \renderable, \templatable {
             $data->analysables[] = $obj;
         }
 
+        if (empty($data->analysables)) {
+            $data->noanalysables = [
+                'message' => get_string('noinvalidanalysables', 'tool_analytics'),
+                'announce' => true,
+            ];
+        }
         return $data;
     }
 }
index 8df2cc3..1c3bba2 100644 (file)
@@ -75,6 +75,7 @@ $string['nextpage'] = 'Next page';
 $string['nodatatoevaluate'] = 'There is no data to evaluate the model';
 $string['nodatatopredict'] = 'No new elements to get predictions for';
 $string['nodatatotrain'] = 'There is no new data that can be used for training';
+$string['noinvalidanalysables'] = 'This site does not contain any invalid analysable element.';
 $string['notdefined'] = 'Not yet defined';
 $string['pluginname'] = 'Analytic models';
 $string['predictionresults'] = 'Prediction results';
index c97dd6b..b7c6cbb 100644 (file)
 <div class="box">
     <h3>{{#str}}modelinvalidanalysables, tool_analytics, {{modelname}}{{/str}}</h3>
     <div>{{#str}}invalidanalysablesinfo, tool_analytics{{/str}}</div>
-    <div class="m-t-2 m-b-1">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
-    <table class="generaltable fullwidth">
-        <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
-        <thead>
-            <tr>
-                <th scope="col">{{#str}}name{{/str}}</th>
-                <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
-                <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
-            </tr>
-        </thead>
-        <tbody>
-        {{#analysables}}
-            <tr>
-                <td>{{{url}}}</td>
-                <td>{{validtraining}}</td>
-                <td>{{validprediction}}</td>
-            </tr>
-        {{/analysables}}
-        </tbody>
-    </table>
-    <div class="m-t-1 m-b-2">
-        <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
-        <span>{{#next}}{{> core/single_button}}{{/next}}</span>
-    </div>
+    {{#noanalysables}}
+        <div class="m-t-2 m-b-1">
+            {{> core/notification_info}}
+        </div>
+    {{/noanalysables}}
+    {{#analysables}}
+        <div class="m-t-2 m-b-1">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+        <table class="generaltable fullwidth">
+            <caption class="accesshide">{{#str}}invalidanalysablestable, tool_analytics{{/str}}</caption>
+            <thead>
+                <tr>
+                    <th scope="col">{{#str}}name{{/str}}</th>
+                    <th scope="col">{{#str}}invalidtraining, tool_analytics{{/str}}</th>
+                    <th scope="col">{{#str}}invalidprediction, tool_analytics{{/str}}</th>
+                </tr>
+            </thead>
+            <tbody>
+            {{#analysables}}
+                <tr>
+                    <td>{{{url}}}</td>
+                    <td>{{validtraining}}</td>
+                    <td>{{validprediction}}</td>
+                </tr>
+            {{/analysables}}
+            </tbody>
+        </table>
+        <div class="m-t-1 m-b-2">
+            <span>{{#prev}}{{> core/single_button}}{{/prev}}</span>
+            <span>{{#next}}{{> core/single_button}}{{/next}}</span>
+        </div>
+    {{/analysables}}
 </div>
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"
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 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 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 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 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';
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
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 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 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);
+        }
+    };
+});
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js
deleted file mode 100644 (file)
index 4a032e0..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-debug.js and /dev/null differ
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js
deleted file mode 100644 (file)
index 8cfbaa1..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced-min.js and /dev/null differ
diff --git a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js b/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js
deleted file mode 100644 (file)
index 6c2ee05..0000000
Binary files a/lib/form/yui/build/moodle-form-showadvanced/moodle-form-showadvanced.js and /dev/null differ
diff --git a/lib/form/yui/src/showadvanced/build.json b/lib/form/yui/src/showadvanced/build.json
deleted file mode 100644 (file)
index 2f81737..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-form-showadvanced",
-    "builds": {
-        "moodle-form-showadvanced": {
-            "jsfiles": [
-                "showadvanced.js"
-            ]
-        }
-    }
-}
diff --git a/lib/form/yui/src/showadvanced/js/showadvanced.js b/lib/form/yui/src/showadvanced/js/showadvanced.js
deleted file mode 100644 (file)
index 564f86f..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-/**
- * Provides the form showadvanced class.
- *
- * @module moodle-form-showadvanced
- */
-
-/**
- * A class to help show and hide advanced form content.
- *
- * @class M.form.showadvanced
- * @constructor
- * @extends Base
- */
-function SHOWADVANCED() {
-    SHOWADVANCED.superclass.constructor.apply(this, arguments);
-}
-
-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>'
-    },
-    ATTRS = {};
-
-/**
- * The form ID attribute definition.
- *
- * @attribute formid
- * @type String
- * @default null
- * @writeOnce
- */
-ATTRS.formid = {
-    value: null
-};
-
-Y.extend(SHOWADVANCED, Y.Base, {
-    /**
-     * The initializer for the showadvanced instance.
-     *
-     * @method initializer
-     * @protected
-     */
-    initializer: function() {
-        var form = Y.one('#' + this.get('formid')),
-            fieldlist = form.all(SELECTORS.FIELDSETCONTAINSADVANCED);
-
-        // Look through fieldset divs that contain advanced elements.
-        fieldlist.each(this.processFieldset, this);
-
-        // Subscribe more/less links to click event.
-        form.delegate('click', this.switchState, SELECTORS.MORELESSLINK);
-        form.delegate('key', this.switchState, 'down:enter,32', SELECTORS.MORELESSLINK);
-    },
-
-    /**
-     * Process the supplied fieldset to add appropriate links, and ARIA roles.
-     *
-     * @method processFieldset
-     * @param {Node} fieldset The Node relating to the fieldset to add collapsing to.
-     * @chainable
-     */
-    processFieldset: function(fieldset) {
-        var statuselement = Y.one('input[name=mform_showmore_' + fieldset.get('id') + ']');
-        if (!statuselement) {
-            Y.log("M.form.showadvanced::processFieldset was called on an fieldset without a status field: '" +
-                fieldset.get('id') + "'", 'debug', 'moodle-form-showadvanced');
-            return this;
-        }
-
-        var morelesslink = Y.Node.create('<a href="#"></a>');
-        morelesslink.addClass(CSS.MORELESSTOGGLER);
-        if (statuselement.get('value') === '0') {
-            morelesslink.setHTML(M.util.get_string('showmore', 'form'));
-        } else {
-            morelesslink.setHTML(M.util.get_string('showless', 'form'));
-            morelesslink.addClass(CSS.SHOWLESS);
-            fieldset.all(SELECTORS.DIVFITEMADVANCED).addClass(CSS.SHOW);
-        }
-
-        // Get list of IDs controlled by this button to set the aria-controls attribute.
-        var idlist = [];
-        fieldset.all(SELECTORS.DIVFITEMADVANCED).each(function(node) {
-            idlist[idlist.length] = node.generateID();
-        });
-        morelesslink.setAttribute('role', 'button');
-        morelesslink.setAttribute('aria-controls', idlist.join(' '));
-
-        var fitem = Y.Node.create(WRAPPERS.FITEM);
-        fitem.addClass(CSS.MORELESSACTIONS);
-        var felement = Y.Node.create(WRAPPERS.FELEMENT);
-        felement.append(morelesslink);
-        fitem.append(felement);
-
-        fieldset.one(SELECTORS.DIVFCONTAINER).append(fitem);
-
-        return this;
-    },
-
-    /**
-     * Toggle the state for the fieldset that was clicked.
-     *
-     * @method switchState
-     * @param {EventFacade} e
-     */
-    switchState: function(e) {
-        e.preventDefault();
-        var fieldset = this.ancestor(SELECTORS.FIELDSETCONTAINSADVANCED);
-
-        // Toggle collapsed class.
-        fieldset.all(SELECTORS.DIVFITEMADVANCED).toggleClass(CSS.SHOW);
-
-        // Get corresponding hidden variable.
-        var statuselement = Y.one('input[name=mform_showmore_' + fieldset.get('id') + ']');
-
-        // Invert it and change the link text.
-        if (statuselement.get('value') === '0') {
-            statuselement.set('value', 1);
-            this.addClass(CSS.SHOWLESS);
-            this.setHTML(M.util.get_string('showless', 'form'));
-        } else {
-            statuselement.set('value', 0);
-            this.removeClass(CSS.SHOWLESS);
-            this.setHTML(M.util.get_string('showmore', 'form'));
-        }
-    }
-}, {
-    NAME: 'moodle-form-showadvanced',
-    ATTRS: ATTRS
-});
-
-M.form = M.form || {};
-M.form.showadvanced = M.form.showadvanced || function(params) {
-    return new SHOWADVANCED(params);
-};
diff --git a/lib/form/yui/src/showadvanced/meta/showadvanced.json b/lib/form/yui/src/showadvanced/meta/showadvanced.json
deleted file mode 100644 (file)
index befdc20..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-    "moodle-form-showadvanced": {
-        "requires": [
-            "node",
-            "base",
-            "selector-css3"
-        ]
-    }
-}
index caa39ef..acd0b21 100644 (file)
@@ -2919,8 +2919,7 @@ class MoodleQuickForm_Renderer extends HTML_QuickForm_Renderer_Tableless{
             $PAGE->requires->yui_module('moodle-form-shortforms', 'M.form.shortforms', array(array('formid' => $formid)));
         }
         if (!empty($this->_advancedElements)){
-            $PAGE->requires->strings_for_js(array('showmore', 'showless'), 'form');
-            $PAGE->requires->yui_module('moodle-form-showadvanced', 'M.form.showadvanced', array(array('formid' => $formid)));
+            $PAGE->requires->js_call_amd('core_form/showadvanced', 'init', [$formid]);
         }
     }
 
index 5ebe7af..3c44836 100644 (file)
@@ -8010,6 +8010,28 @@ function moodle_major_version($fromdisk = false) {
 
 // MISCELLANEOUS.
 
+/**
+ * Gets the system locale
+ *
+ * @return string Retuns the current locale.
+ */
+function moodle_getlocale() {
+    global $CFG;
+
+    // Fetch the correct locale based on ostype.
+    if ($CFG->ostype == 'WINDOWS') {
+        $stringtofetch = 'localewin';
+    } else {
+        $stringtofetch = 'locale';
+    }
+
+    if (!empty($CFG->locale)) { // Override locale for all language packs.
+        return $CFG->locale;
+    }
+
+    return get_string($stringtofetch, 'langconfig');
+}
+
 /**
  * Sets the system locale
  *
@@ -8023,20 +8045,11 @@ function moodle_setlocale($locale='') {
 
     $oldlocale = $currentlocale;
 
-    // Fetch the correct locale based on ostype.
-    if ($CFG->ostype == 'WINDOWS') {
-        $stringtofetch = 'localewin';
-    } else {
-        $stringtofetch = 'locale';
-    }
-
     // The priority is the same as in get_string() - parameter, config, course, session, user, global language.
     if (!empty($locale)) {
         $currentlocale = $locale;
-    } else if (!empty($CFG->locale)) { // Override locale for all language packs.
-        $currentlocale = $CFG->locale;
     } else {
-        $currentlocale = get_string($stringtofetch, 'langconfig');
+        $currentlocale = moodle_getlocale();
     }
 
     // Do nothing if locale already set up.
index 93d32b5..1090652 100644 (file)
@@ -430,15 +430,16 @@ class navigation_node implements renderable {
      *
      * @param flat_navigation $nodes List of the found flat navigation nodes.
      * @param boolean $showdivider Show a divider before the first node.
+     * @param string $label A label for the collection of navigation links.
      */
-    public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false) {
+    public function build_flat_navigation_list(flat_navigation $nodes, $showdivider = false, $label = '') {
         if ($this->showinflatnavigation) {
             $indent = 0;
             if ($this->type == self::TYPE_COURSE || $this->key === self::COURSE_INDEX_PAGE) {
                 $indent = 1;
             }
             $flat = new flat_navigation_node($this, $indent);
-            $flat->set_showdivider($showdivider);
+            $flat->set_showdivider($showdivider, $label);
             $nodes->add($flat);
         }
         foreach ($this->children as $child) {
@@ -913,6 +914,12 @@ class navigation_node_collection implements IteratorAggregate, Countable {
      */
     protected $count = 0;
 
+    /**
+     * Label for collection of nodes.
+     * @var string
+     */
+    protected $collectionlabel = '';
+
     /**
      * Adds a navigation node to the collection
      *
@@ -988,6 +995,24 @@ class navigation_node_collection implements IteratorAggregate, Countable {
         return $keys;
     }
 
+    /**
+     * Set a label for this collection.
+     *
+     * @param string $label
+     */
+    public function set_collectionlabel($label) {
+        $this->collectionlabel = $label;
+    }
+
+    /**
+     * Return a label for this collection.
+     *
+     * @return string
+     */
+    public function get_collectionlabel() {
+        return $this->collectionlabel;
+    }
+
     /**
      * Fetches a node from this collection.
      *
@@ -3770,6 +3795,9 @@ class flat_navigation_node extends navigation_node {
     /** @var $showdivider bool Show a divider before this element */
     private $showdivider = false;
 
+    /** @var $collectionlabel string Label for a group of nodes */
+    private $collectionlabel = '';
+
     /**
      * A proxy constructor
      *
@@ -3791,6 +3819,31 @@ class flat_navigation_node extends navigation_node {
         $this->indent = $indent;
     }
 
+    /**
+     * Setter, a label is required for a flat navigation node that shows a divider.
+     *
+     * @param string $label
+     */
+    public function set_collectionlabel($label) {
+        $this->collectionlabel = $label;
+    }
+
+    /**
+     * Getter, get the label for this flat_navigation node, or it's parent if it doesn't have one.
+     *
+     * @return string
+     */
+    public function get_collectionlabel() {
+        if (!empty($this->collectionlabel)) {
+            return $this->collectionlabel;
+        }
+        if ($this->parent && ($this->parent instanceof flat_navigation_node || $this->parent instanceof flat_navigation)) {
+            return $this->parent->get_collectionlabel();
+        }
+        debugging('Navigation region requires a label', DEBUG_DEVELOPER);
+        return '';
+    }
+
     /**
      * Does this node represent a course section link.
      * @return boolean
@@ -3828,9 +3881,15 @@ class flat_navigation_node extends navigation_node {
     /**
      * Setter for "showdivider"
      * @param $val boolean
+     * @param $label string Label for the group of nodes
      */
-    public function set_showdivider($val) {
+    public function set_showdivider($val, $label = '') {
         $this->showdivider = $val;
+        if ($this->showdivider && empty($label)) {
+            debugging('Navigation region requires a label', DEBUG_DEVELOPER);
+        } else {
+            $this->set_collectionlabel($label);
+        }
     }
 
     /**
@@ -3848,7 +3907,6 @@ class flat_navigation_node extends navigation_node {
     public function set_indent($val) {
         $this->indent = $val;
     }
-
 }
 
 /**
@@ -3903,6 +3961,7 @@ class flat_navigation extends navigation_node_collection {
                 format_string($course->fullname, true, array('context' => $coursecontext));
 
             $flat = new flat_navigation_node(navigation_node::create($coursename, $url), 0);
+            $flat->set_collectionlabel($coursename);
             $flat->key = 'coursehome';
             $flat->icon = new pix_icon('i/course', '');
 
@@ -3930,9 +3989,9 @@ class flat_navigation extends navigation_node_collection {
                 }
             }
 
-            $this->page->navigation->build_flat_navigation_list($this, true);
+            $this->page->navigation->build_flat_navigation_list($this, true, get_string('site'));
         } else {
-            $this->page->navigation->build_flat_navigation_list($this, false);
+            $this->page->navigation->build_flat_navigation_list($this, false, get_string('site'));
         }
 
         $admin = $PAGE->settingsnav->find('siteadministration', navigation_node::TYPE_SITE_ADMIN);
@@ -3942,7 +4001,7 @@ class flat_navigation extends navigation_node_collection {
         }
         if ($admin) {
             $flat = new flat_navigation_node($admin, 0);
-            $flat->set_showdivider(true);
+            $flat->set_showdivider(true, get_string('sitesettings'));
             $flat->key = 'sitesettings';
             $flat->icon = new pix_icon('t/preferences', '');
             $this->add($flat);
@@ -3956,7 +4015,7 @@ class flat_navigation extends navigation_node_collection {
             $url = new moodle_url($PAGE->url, ['bui_addblock' => '', 'sesskey' => sesskey()]);
             $addablock = navigation_node::create(get_string('addblock'), $url);
             $flat = new flat_navigation_node($addablock, 0);
-            $flat->set_showdivider(true);
+            $flat->set_showdivider(true, get_string('blocksaddedit'));
             $flat->key = 'addblock';
             $flat->icon = new pix_icon('i/addblock', '');
             $this->add($flat);
@@ -3969,6 +4028,26 @@ class flat_navigation extends navigation_node_collection {
         }
     }
 
+
+    /**
+     * Override the parent so we can set a label for this collection if it has not been set yet.
+     *
+     * @param navigation_node $node Node to add
+     * @param string $beforekey If specified, adds before a node with this key,
+     *   otherwise adds at end
+     * @return navigation_node Added node
+     */
+    public function add(navigation_node $node, $beforekey=null) {
+        $result = parent::add($node, $beforekey);
+        // Extend the parent to get a name for the collection of nodes if required.
+        if (empty($this->collectionlabel)) {
+            if ($node instanceof flat_navigation_node) {
+                $this->set_collectionlabel($node->get_collectionlabel());
+            }
+        }
+
+        return $result;
+    }
 }
 
 /**
index 7aa51a6..9448fea 100644 (file)
                         </label>
                     </div>
                     <div class="form-input">
-                        <input type="text" name="username" id="username" size="15" value="{{username}}">
+                        <input type="text" name="username" id="username" size="15" value="{{username}}" autocomplete="username">
                     </div>
                     <div class="clearer"><!-- --></div>
                     <div class="form-label">
                         <label for="password">{{#str}} password {{/str}}</label>
                     </div>
                     <div class="form-input">
-                        <input type="password" name="password" id="password" size="15" value="">
+                        <input type="password" name="password" id="password" size="15" value="" autocomplete="current-password">
                     </div>
                 </div>
 
index 530bce0..9f6e4af 100644 (file)
@@ -3,6 +3,7 @@ information provided here is intended especially for developers.
 
 === 3.7 ===
 
+* Nodes in the navigation api can have labels for each group. See set/get_collectionlabel().
 * The method core_user::is_real_user() now returns false for userid = 0 parameter
 * 'mform1' dependencies (in themes, js...) will stop working because a randomly generated string has been added to the id
 attribute on forms to avoid collisions in forms loaded in AJAX requests.
index 5064690..4a25979 100644 (file)
@@ -26,7 +26,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once $CFG->libdir.'/formslib.php';
+require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 class login_change_password_form extends moodleform {
 
@@ -51,7 +52,8 @@ class login_change_password_form extends moodleform {
         if ($policies) {
             $mform->addElement('static', 'passwordpolicyinfo', '', implode('<br />', $policies));
         }
-        $mform->addElement('password', 'password', get_string('oldpassword'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'password');
+        $mform->addElement('password', 'password', get_string('oldpassword'), $purpose);
         $mform->addRule('password', get_string('required'), 'required', null, 'client');
         $mform->setType('password', PARAM_RAW);
 
index 32e5310..67a4d9f 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Reset forgotten password form definition.
@@ -40,12 +41,15 @@ class login_forgot_password_form extends moodleform {
      * Define the forgot password form.
      */
     function definition() {
+        global $USER;
+
         $mform    = $this->_form;
         $mform->setDisableShortforms(true);
 
         $mform->addElement('header', 'searchbyusername', get_string('searchbyusername'), '');
 
-        $mform->addElement('text', 'username', get_string('username'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'username');
+        $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose);
         $mform->setType('username', PARAM_RAW);
 
         $submitlabel = get_string('search');
@@ -53,7 +57,8 @@ class login_forgot_password_form extends moodleform {
 
         $mform->addElement('header', 'searchbyemail', get_string('searchbyemail'), '');
 
-        $mform->addElement('text', 'email', get_string('email'));
+        $purpose = user_edit_map_field_purpose($USER->id, 'email');
+        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"' . $purpose);
         $mform->setType('email', PARAM_RAW_TRIMMED);
 
         $submitlabel = get_string('search');
index 0bcc236..ff35374 100644 (file)
Binary files a/message/amd/build/notification_processor_settings.min.js and b/message/amd/build/notification_processor_settings.min.js differ
index d4347b0..3d1c9f6 100644 (file)
Binary files a/message/amd/build/preferences_notifications_list_controller.min.js and b/message/amd/build/preferences_notifications_list_controller.min.js differ
index 6964dfb..8ce9f05 100644 (file)
Binary files a/message/amd/build/preferences_processor_form.min.js and b/message/amd/build/preferences_processor_form.min.js differ
index bb3763b..1fb2132 100644 (file)
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templates', 'core/str', 'tool_lp/dialogue'],
-        function($, Ajax, Notification, Fragment, Templates, Str, Dialogue) {
+define([
+        'jquery',
+        'core/ajax',
+        'core/str',
+        'core/notification',
+        'core/custom_interaction_events',
+        'core/modal',
+        'core/modal_registry',
+        'core/fragment',
+        ],
+        function(
+            $,
+            Ajax,
+            Str,
+            Notification,
+            CustomEvents,
+            Modal,
+            ModalRegistry,
+            Fragment
+        ) {
 
+    var registered = false;
     var SELECTORS = {
+        SAVE_BUTTON: '[data-action="save"]',
+        CANCEL_BUTTON: '[data-action="cancel"]',
         PROCESSOR: '[data-processor-name]',
         PREFERENCE_ROW: '[data-region="preference-row"]',
     };
 
     /**
-     * Constructor for the notification processor settings.
+     * Constructor for the Modal.
      *
-     * @param {object} element jQuery object root element of the processor
+     * @param {object} root The root jQuery element for the modal.
      */
-    var NotificationProcessorSettings = function(element) {
-        this.root = $(element);
-        this.name = this.root.attr('data-name');
-        this.userId = this.root.attr('data-user-id');
-        this.contextId = this.root.attr('data-context-id');
+    var NotificationProcessorSettings = function(root) {
+        Modal.call(this, root);
+        this.name = null;
+        this.userId = null;
+        this.contextId = null;
+        this.element = null;
+        this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
+        this.cancelButton = this.getFooter().find(SELECTORS.CANCEL_BUTTON);
+    };
+
+    NotificationProcessorSettings.TYPE = 'core_message-notification_processor_settings';
+    NotificationProcessorSettings.prototype = Object.create(Modal.prototype);
+    NotificationProcessorSettings.prototype.constructor = NotificationProcessorSettings;
+
+    /**
+     * Set the userid to the given value.
+     *
+     * @method setUserId
+     * @param {int} id The notification userid
+     */
+    NotificationProcessorSettings.prototype.setUserId = function(id) {
+        this.userId = id;
+    };
+
+    /**
+     * Retrieve the current userid, if any.
+     *
+     * @method getUserId
+     * @return {int|null} The notification userid
+     */
+    NotificationProcessorSettings.prototype.getUserId = function() {
+        return this.userId;
+    };
+
+    /**
+     * Set the object to the given value.
+     *
+     * @method setElement
+     * @param {object} element The notification node element.
+     */
+    NotificationProcessorSettings.prototype.setElement = function(element) {
+        this.element = element;
+    };
+
+    /**
+     * Retrieve the current element, if any.
+     *
+     * @method getElement
+     * @return {object|null} The notification node element.
+     */
+    NotificationProcessorSettings.prototype.getElement = function() {
+        return this.element;
+    };
+
+    /**
+     * Set the name to the given value.
+     *
+     * @method setName
+     * @param {string} name The notification name.
+     */
+    NotificationProcessorSettings.prototype.setName = function(name) {
+        this.name = name;
+    };
+
+    /**
+     * Retrieve the current name, if any.
+     *
+     * @method getName
+     * @return {string|null} The notification name.
+     */
+    NotificationProcessorSettings.prototype.getName = function() {
+        return this.name;
+    };
+    /**
+     * Set the context id to the given value.
+     *
+     * @method setContextId
+     * @param {Number} id The notification context id
+     */
+    NotificationProcessorSettings.prototype.setContextId = function(id) {
+        this.contextId = id;
+    };
+
+    /**
+     * Retrieve the current context id, if any.
+     *
+     * @method getContextId
+     * @return {Number|null} The notification context id
+     */
+    NotificationProcessorSettings.prototype.getContextId = function() {
+        return this.contextId;
+    };
+
+    /**
+     * Get the form element from the modal.
+     *
+     * @method getForm
+     * @return {object}
+     */
+    NotificationProcessorSettings.prototype.getForm = function() {
+        return this.getBody().find('form');
+    };
+
+    /**
+     * Disable the buttons in the footer.
+     *
+     * @method disableButtons
+     */
+    NotificationProcessorSettings.prototype.disableButtons = function() {
+        this.saveButton.prop('disabled', true);
+        this.cancelButton.prop('disabled', true);
+    };
+
+    /**
+     * Enable the buttons in the footer.
+     *
+     * @method enableButtons
+     */
+    NotificationProcessorSettings.prototype.enableButtons = function() {
+        this.saveButton.prop('disabled', false);
+        this.cancelButton.prop('disabled', false);
+    };
+
+    /**
+     * Load the title for the modal to the appropriate value
+     * depending on message outputs.
+     *
+     * @method loadTitleContent
+     * @return {object} A promise resolved with the new title text.
+     */
+    NotificationProcessorSettings.prototype.loadTitleContent = function() {
+        this.titlePromise = Str.get_string('processorsettings', 'message');
+        this.setTitle(this.titlePromise);
+
+        return this.titlePromise;
+    };
+
+    /**
+     * Load the body for the modal to the appropriate value
+     * depending on message outputs.
+     *
+     * @method loadBodyContent
+     * @return {object} A promise resolved with the fragment html and js from
+     */
+    NotificationProcessorSettings.prototype.loadBodyContent = function() {
+        this.disableButtons();
+
+        var args = {
+            userid: this.getUserId(),
+            type: this.getName(),
+        };
+
+        this.bodyPromise = Fragment.loadFragment('message', 'processor_settings', this.getContextId(), args);
+        this.setBody(this.bodyPromise);
+
+        this.bodyPromise.then(function() {
+            this.enableButtons();
+            return;
+        }.bind(this))
+        .fail(Notification.exception);
+
+        return this.bodyPromise;
     };
 
     /**
-     * Show the notification processor settings dialogue.
+     * Load both the title and body content.
+     *
+     * @method loadAllContent
+     * @return {object} promise
+     */
+    NotificationProcessorSettings.prototype.loadAllContent = function() {
+        return $.when(this.loadTitleContent(), this.loadBodyContent());
+    };
+
+    /**
+     * Load the modal content before showing it. This
+     * is to allow us to re-use the same modal for creating and
+     * editing different message outputs within the page.
      *
      * @method show
      */
     NotificationProcessorSettings.prototype.show = function() {
-        Fragment.loadFragment('message', 'processor_settings', this.contextId, {
-            userid: this.userId,
-            type: this.name,
-        })
-        .done(function(html, js) {
-            Str.get_string('processorsettings', 'message').done(function(string) {
-                var dialogue = new Dialogue(
-                    string,
-                    html,
-                    function() {
-                        Templates.runTemplateJS(js);
-                    },
-                    function() {
-                        // Removed dialogue from the DOM after close.
-                        dialogue.close();
-                    }
-                );
-
-                $(document).on('mpp:formsubmitted', function() {
-                    dialogue.close();
-                    this.updateConfiguredStatus();
-                }.bind(this));
-
-                $(document).on('mpp:formcancelled', function() {
-                    dialogue.close();
-                });
-            }.bind(this));
-        }.bind(this));
+        this.loadAllContent();
+        Modal.prototype.show.call(this);
+    };
+
+    /**
+     * Clear the notification from the modal when it's closed so
+     * that it is loaded fresh next time it's displayed.
+     *
+     * @method hide
+     */
+    NotificationProcessorSettings.prototype.hide = function() {
+        Modal.prototype.hide.call(this);
+        this.setContextId(null);
+        this.setName(null);
+        this.setUserId(null);
     };
 
     /**
@@ -86,7 +263,7 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templ
      * @return {Promise|boolean}
      */
     NotificationProcessorSettings.prototype.updateConfiguredStatus = function() {
-        var processorHeader = this.root.closest(SELECTORS.PROCESSOR);
+        var processorHeader = $(this.getElement()).closest(SELECTORS.PROCESSOR);
 
         if (!processorHeader.hasClass('unconfigured')) {
             return false;
@@ -114,5 +291,44 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/fragment', 'core/templ
             });
     };
 
+    /**
+     * Set up all of the event handling for the modal.
+     *
+     * @method registerEventListeners
+     */
+    NotificationProcessorSettings.prototype.registerEventListeners = function() {
+        // Apply parent event listeners.
+        Modal.prototype.registerEventListeners.call(this);
+
+        // When the user clicks the save button we trigger the form submission.
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
+            this.getForm().submit();
+            data.originalEvent.preventDefault();
+        }.bind(this));
+
+        this.getModal().on('mpp:formsubmitted', function(e) {
+            this.hide();
+            this.updateConfiguredStatus();
+            e.stopPropagation();
+        }.bind(this));
+
+        this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, function(e, data) {
+            this.hide();
+            data.originalEvent.preventDefault();
+            e.stopPropagation();
+        }.bind(this));
+    };
+
+    // Automatically register with the modal registry the first time this module is imported
+    // so that you can create modals
+    // of this type using the modal factory.
+    if (!registered) {
+        ModalRegistry.register(
+                                NotificationProcessorSettings.TYPE,
+                                NotificationProcessorSettings,
+                                'core/modal_save_cancel');
+        registered = true;
+    }
+
     return NotificationProcessorSettings;
 });
index 14ab2e6..8b73113 100644 (file)
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_events', 'core_message/notification_preference',
-        'core_message/notification_processor_settings'],
-        function($, Ajax, Notification, CustomEvents, NotificationPreference, NotificationProcessorSettings) {
+define(['jquery',
+        'core/ajax',
+        'core/notification',
+        'core/custom_interaction_events',
+        'core_message/notification_preference',
+        'core_message/notification_processor_settings',
+        'core/modal_factory',
+        ],
+        function(
+          $,
+          Ajax,
+          Notification,
+          CustomEvents,
+          NotificationPreference,
+          NotificationProcessorSettings,
+          ModalFactory
+        ) {
 
     var SELECTORS = {
         DISABLE_NOTIFICATIONS: '[data-region="disable-notification-container"] [data-disable-notifications]',
@@ -143,11 +157,25 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_eve
             }
         }.bind(this));
 
-        this.root.on(CustomEvents.events.activate, SELECTORS.PROCESSOR_SETTING, function(e, data) {
+        var eventFormPromise = ModalFactory.create({
+            type: NotificationProcessorSettings.TYPE,
+        });
+
+        this.root.on(CustomEvents.events.activate, SELECTORS.PROCESSOR_SETTING, function(e) {
             var element = $(e.target).closest(SELECTORS.PROCESSOR_SETTING);
-            var processorSettings = new NotificationProcessorSettings(element);
-            processorSettings.show();
-            data.originalEvent.preventDefault();
+
+            e.preventDefault();
+            eventFormPromise.then(function(modal) {
+                // Configure modal with element settings.
+                modal.setUserId($(element).attr('data-user-id'));
+                modal.setName($(element).attr('data-name'));
+                modal.setContextId($(element).attr('data-context-id'));
+                modal.setElement(element);
+                modal.show();
+
+                e.stopImmediatePropagation();
+                return;
+            }).fail(Notification.exception);
         });
 
         CustomEvents.define(disabledNotificationsElement, [
index 53b3766..e458d8b 100644 (file)
@@ -22,8 +22,8 @@
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_events'],
-        function($, Ajax, Notification, CustomEvents) {
+define(['jquery', 'core/ajax', 'core/notification'],
+        function($, Ajax, Notification) {
     /**
      * Constructor for the ProcessorForm.
      *
@@ -37,18 +37,9 @@ define(['jquery', 'core/ajax', 'core/notification', 'core/custom_interaction_eve
         this.root.find('form').on('submit', function(e) {
             e.preventDefault();
             this.save().done(function() {
-                $(document).trigger('mpp:formsubmitted');
+                $(element).trigger('mpp:formsubmitted');
             });
         }.bind(this));
-
-        var cancelButton = this.root.find('[data-cancel-button]');
-        CustomEvents.define(cancelButton, [
-            CustomEvents.events.activate
-        ]);
-
-        cancelButton.on(CustomEvents.events.activate, function() {
-            $(document).trigger('mpp:formcancelled');
-        });
     };
 
     /**
index be8aea6..a53f6b7 100644 (file)
@@ -2497,6 +2497,31 @@ class api {
 
         $request->id = $DB->insert_record('message_contact_requests', $request);
 
+        // Send a notification.
+        $userfrom = \core_user::get_user($userid);
+        $userfromfullname = fullname($userfrom);
+        $userto = \core_user::get_user($requesteduserid);
+        $url = new \moodle_url('/message/pendingcontactrequests.php');
+
+        $subject = get_string('messagecontactrequestsnotificationsubject', 'core_message', $userfromfullname);
+        $fullmessage = get_string('messagecontactrequestsnotification', 'core_message', $userfromfullname);
+
+        $message = new \core\message\message();
+        $message->courseid = SITEID;
+        $message->component = 'moodle';
+        $message->name = 'messagecontactrequests';
+        $message->notification = 1;
+        $message->userfrom = $userfrom;
+        $message->userto = $userto;
+        $message->subject = $subject;
+        $message->fullmessage = text_to_html($fullmessage);
+        $message->fullmessageformat = FORMAT_HTML;
+        $message->fullmessagehtml = $fullmessage;
+        $message->smallmessage = '';
+        $message->contexturl = $url->out(false);
+
+        message_send($message);
+
         return $request;
     }
 
index 42be717..dd77931 100644 (file)
     </div>
     <form>
         {{{formhtml}}}
-        <div class="form-actions m-t-1">
-            <button type="submit" class="btn btn-primary">{{#str}} savechanges {{/str}}</button>
-            <button type="button" class="btn" data-cancel-button>{{#str}} cancel {{/str}}</button>
-        </div>
     </form>
 </div>
 {{#js}}
index 4d1971c..425c425 100644 (file)
@@ -904,8 +904,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 conversation members.
         $this->assertEquals(4, $DB->count_records('message_conversation_members'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         provider::delete_data_for_all_users_in_context($user1context);
 
@@ -943,8 +943,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // And user1 is not in any conversation.
         $this->assertEquals(0, $DB->count_records('message_conversation_members', ['userid' => $user1->id]));
 
-        // Confirm there is only 1 notification.
-        $this->assertEquals(1, $DB->count_records('notifications'));
+        // Confirm there are only 2 notifications.
+        $this->assertEquals(2, $DB->count_records('notifications'));
         // And it is not related to user1.
         $this->assertEquals(0,
                 $DB->count_records_select('notifications', 'useridfrom = ? OR useridto = ? ', [$user1->id, $user1->id]));
@@ -1011,8 +1011,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be two conversation members.
         $this->assertEquals(2, $DB->count_records('message_conversation_members'));
 
-        // There should be three notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         $user1context = context_user::instance($user1->id);
         $contextlist = new \core_privacy\local\request\approved_contextlist($user1, 'core_message',
@@ -1053,10 +1053,10 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $mcm = reset($mcms);
         $this->assertEquals($user2->id, $mcm->userid);
 
-        $this->assertCount(1, $notifications);
+        $this->assertCount(2, $notifications);
         ksort($notifications);
 
-        $notification = array_shift($notifications);
+        $notification = array_pop($notifications);
         $this->assertEquals($user2->id, $notification->useridfrom);
         $this->assertEquals($user3->id, $notification->useridto);
     }
@@ -1323,7 +1323,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_conversation_members'));
 
         // There should be three notifications + two for the contact requests.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         $user1context = context_user::instance($user1->id);
         $approveduserlist = new \core_privacy\local\request\approved_userlist($user1context, 'core_message',
@@ -1366,10 +1366,10 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $mcm = reset($mcms);
         $this->assertEquals($user2->id, $mcm->userid);
 
-        $this->assertCount(1, $notifications);
+        $this->assertCount(2, $notifications);
         ksort($notifications);
 
-        $notification = array_shift($notifications);
+        $notification = array_pop($notifications);
         $this->assertEquals($user2->id, $notification->useridfrom);
         $this->assertEquals($user3->id, $notification->useridto);
     }
@@ -1843,8 +1843,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group.
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 3 favourite conversations.
         $this->assertEquals(3, $DB->count_records('favourite'));
@@ -1861,8 +1861,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be still 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications.
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 5 messages - 3 individual - 2 group (course2).
         $this->assertEquals(5, $DB->count_records('messages'));
@@ -2016,8 +2016,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group.
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 3 favourite conversations.
         $this->assertEquals(3, $DB->count_records('favourite'));
@@ -2033,7 +2033,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(3, $DB->count_records('favourite'));
 
         // Delete individual conversations for all users in system context.
@@ -2047,7 +2047,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(3, $DB->count_records('favourite'));
     }
 
@@ -2175,8 +2175,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 9 conversation members - (2 + 2) individual + (3 + 2) group.
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 3 favourite conversations.
         $this->assertEquals(3, $DB->count_records('favourite'));
@@ -2192,7 +2192,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(3, $DB->count_records('favourite'));
 
         // Delete individual conversations for all users in user context.
@@ -2206,7 +2206,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(3, $DB->count_records('favourite'));
     }
 
@@ -2310,8 +2310,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications.
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 6 messages.
         $this->assertEquals(6, $DB->count_records('messages'));
@@ -2347,8 +2347,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be still 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications.
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 4 messages - 3 private + 1 group sent by user2.
         $this->assertEquals(4, $DB->count_records('messages'));
@@ -2493,8 +2493,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications (3 from create_notification and 2 from create_contact_request).
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 6 messages.
         $this->assertEquals(6, $DB->count_records('messages'));
@@ -2526,7 +2526,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_contacts'));
         $this->assertEquals(2, $DB->count_records('message_contact_requests'));
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(6, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
@@ -2542,7 +2542,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_contacts'));
         $this->assertEquals(2, $DB->count_records('message_contact_requests'));
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(6, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
@@ -2564,8 +2564,8 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be still 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 3 notifications.
-        $this->assertEquals(3, $DB->count_records('notifications'));
+        // There should be 5 notifications.
+        $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 4 messages - 3 private + 1 group sent by user3.
         $this->assertEquals(4, $DB->count_records('messages'));
index bb7d06b..c0b5e2d 100644 (file)
@@ -60,6 +60,9 @@ abstract class base extends \core\event\base {
         if ($assign->get_context()->id != $this->get_context()->id) {
             throw new \coding_exception('Invalid assign isntance supplied!');
         }
+        if ($assign->is_blind_marking()) {
+            $this->data['anonymous'] = 1;
+        }
         $this->assign = $assign;
     }
 
index cd9f4c1..cacc73a 100644 (file)
@@ -8987,10 +8987,15 @@ class assign {
 
         // Trigger the course module viewed event.
         $assigninstance = $this->get_instance();
-        $event = \mod_assign\event\course_module_viewed::create(array(
-                'objectid' => $assigninstance->id,
-                'context' => $this->get_context()
-        ));
+        $params = [
+            'objectid' => $assigninstance->id,
+            'context' => $this->get_context()
+        ];
+        if ($this->is_blind_marking()) {
+            $params['anonymous'] = 1;
+        }
+
+        $event = \mod_assign\event\course_module_viewed::create($params);
 
         $event->add_record_snapshot('assign', $assigninstance);
         $event->trigger();
index 38e2da0..a4153dd 100644 (file)
@@ -252,6 +252,9 @@ class assign_submission_file extends assign_submission_plugin {
         if (!empty($submission->userid) && ($submission->userid != $USER->id)) {
             $params['relateduserid'] = $submission->userid;
         }
+        if ($this->assignment->is_blind_marking()) {
+            $params['anonymous'] = 1;
+        }
         $event = \assignsubmission_file\event\assessable_uploaded::create($params);
         $event->set_legacy_files($files);
         $event->trigger();
index a67bd06..3013327 100644 (file)
@@ -251,6 +251,9 @@ class assign_submission_onlinetext extends assign_submission_plugin {
         if (!empty($submission->userid) && ($submission->userid != $USER->id)) {
             $params['relateduserid'] = $submission->userid;
         }
+        if ($this->assignment->is_blind_marking()) {
+            $params['anonymous'] = 1;
+        }
         $event = \assignsubmission_onlinetext\event\assessable_uploaded::create($params);
         $event->trigger();
 
index b2fc975..fe50250 100644 (file)
@@ -1353,4 +1353,43 @@ class assign_events_testcase extends advanced_testcase {
         $this->assertInstanceOf('\mod_assign\event\course_module_viewed', $event);
         $this->assertEquals($context, $event->get_context());
     }
+
+    /**
+     * Test that all events generated with blindmarking enabled are anonymous
+     */
+    public function test_anonymous_events() {
+        $this->resetAfterTest();
+
+        $course = $this->getDataGenerator()->create_course();
+        $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
+        $student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+        $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+        $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
+        $instance = $generator->create_instance(array('course' => $course->id, 'blindmarking' => 1));
+
+        $cm = get_coursemodule_from_instance('assign', $instance->id, $course->id);
+        $context = context_module::instance($cm->id);
+        $assign = new assign($context, $cm, $course);
+
+        $this->setUser($teacher);
+        $sink = $this->redirectEvents();
+
+        $assign->lock_submission($student1->id);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertTrue((bool)$event->anonymous);
+
+        $assign->reveal_identities();
+        $sink = $this->redirectEvents();
+        $assign->lock_submission($student2->id);
+
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        $this->assertFalse((bool)$event->anonymous);
+    }
+
 }
diff --git a/question/amd/build/qbankmanager.min.js b/question/amd/build/qbankmanager.min.js
new file mode 100644 (file)
index 0000000..50dd343
Binary files /dev/null and b/question/amd/build/qbankmanager.min.js differ
diff --git a/question/amd/src/qbankmanager.js b/question/amd/src/qbankmanager.js
new file mode 100644 (file)
index 0000000..78ff864
--- /dev/null
@@ -0,0 +1,55 @@
+// 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 javascript module to handle question ajax actions.
+ *
+ * @module     core_question/qbankmanager
+ * @class      qbankmanager
+ * @package    core_question
+ * @copyright 2018 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+define(['jquery', 'core/pubsub', 'core/checkbox-toggleall'], function($, PubSub, ToggleAll) {
+
+    var registerListeners = function() {
+        PubSub.subscribe(ToggleAll.events.checkboxToggled, toggleButtonStates);
+    };
+
+    var toggleButtonStates = function(data) {
+        if ('qbank' !== data.toggleGroupName) {
+            return;
+        }
+
+        setButtonState(data.anyChecked);
+    };
+
+    var setButtonState = function(state) {
+        var buttons = $('.modulespecificbuttonscontainer').find('input, select, link');
+        buttons.attr('disabled', !state);
+    };
+
+    return {
+        /**
+         * Set up the Question Bank Manager.
+         *
+         * @method init
+         */
+        init: function() {
+            setButtonState(false);
+            registerListeners();
+        },
+    };
+});
index 289aaba..cddbaf7 100644 (file)
@@ -26,7 +26,9 @@ class checkbox_column extends column_base {
     protected $strselect;
 
     public function init() {
-        $this->strselect = get_string('select');
+        global $PAGE;
+
+        $PAGE->requires->js_call_amd('core/checkbox-toggleall', 'init');
     }
 
     public function get_name() {
@@ -34,21 +36,41 @@ class checkbox_column extends column_base {
     }
 
     protected function get_title() {
-        return '<input type="checkbox" disabled="disabled" id="qbheadercheckbox" />';
+        $input = \html_writer::empty_tag('input', [
+            'id' => 'qbheadercheckbox',
+            'name' => 'qbheadercheckbox',
+            'type' => 'checkbox',
+            'value' => '1',
+            'data-action' => 'toggle',
+            'data-toggle' => 'master',
+            'data-togglegroup' => 'qbank',
+            'data-toggle-selectall' => get_string('selectall', 'moodle'),
+            'data-toggle-deselectall' => get_string('deselectall', 'moodle'),
+        ]);
+
+        $label = \html_writer::tag('label', get_string('selectall', 'moodle'), [
+            'class' => 'accesshide',
+            'for' => 'qbheadercheckbox',
+        ]);
+
+        return $input . $label;
     }
 
     protected function get_title_tip() {
-        global $PAGE;
-        $PAGE->requires->strings_for_js(array('selectall', 'deselectall'), 'moodle');
-        $PAGE->requires->yui_module('moodle-question-qbankmanager', 'M.question.qbankmanager.init');
         return get_string('selectquestionsforbulk', 'question');
-
     }
 
     protected function display_content($question, $rowclasses) {
-        global $PAGE;
-        echo '<input title="' . $this->strselect . '" type="checkbox" name="q' .
-                $question->id . '" id="checkq' . $question->id . '" value="1"/>';
+        echo \html_writer::empty_tag('input', [
+            'title' => get_string('select'),
+            'type' => 'checkbox',
+            'name' => "q{$question->id}",
+            'id' => "checkq{$question->id}",
+            'value' => '1',
+            'data-action' => 'toggle',
+            'data-toggle' => 'slave',
+            'data-togglegroup' => 'qbank',
+        ]);
     }
 
     public function get_required_fields() {
index ec5b81f..9225af8 100644 (file)
@@ -688,7 +688,7 @@ class view {
     protected function display_question_list($contexts, $pageurl, $categoryandcontext,
             $cm = null, $recurse=1, $page=0, $perpage=100, $showhidden=false,
             $showquestiontext = false, $addcontexts = array()) {
-        global $CFG, $DB, $OUTPUT;
+        global $CFG, $DB, $OUTPUT, $PAGE;
 
         // This function can be moderately slow with large question counts and may time out.
         // We probably do not want to raise it to unlimited, so randomly picking 5 minutes.
@@ -758,6 +758,8 @@ class view {
         }
         echo '</div>';
 
+        $PAGE->requires->js_call_amd('core_question/qbankmanager', 'init');
+
         $this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
 
         echo '</fieldset>';
diff --git a/question/tests/behat/select_questions.feature b/question/tests/behat/select_questions.feature
new file mode 100644 (file)
index 0000000..b4b766e
--- /dev/null
@@ -0,0 +1,56 @@
+@core @core_question
+Feature: The questions in the question bank can be selected in various ways
+  In selected to do something for questions
+  As a teacher
+  I want to choose them to move, delete it.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1        | weeks  |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And the following "questions" exist:
+      | questioncategory | qtype     | name              | user     | questiontext    |
+      | Test questions   | essay     | A question 1 name | admin    | Question 1 text |
+      | Test questions   | essay     | B question 2 name | teacher1 | Question 2 text |
+      | Test questions   | numerical | C question 3 name | teacher1 | Question 3 text |
+    And I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "Question bank > Questions" in current page administration
+
+  @javascript
+  Scenario: The question text can be chosen all in the list of questions
+    Given the field "Select all" matches value ""
+    When I click on "Select all" "checkbox"
+    And the field "A question 1 name" matches value "1"
+    And the field "B question 2 name" matches value "1"
+    And the field "C question 3 name" matches value "1"
+    Then I click on "Deselect all" "checkbox"
+    And the field "A question 1 name" matches value ""
+    And the field "B question 2 name" matches value ""
+    And the field "C question 3 name" matches value ""
+
+  @javascript
+  Scenario: The question text can be chosen in the list of questions
+    Given the field "Select all" matches value ""
+    When I click on "A question 1 name" "checkbox"
+    Then the field "Select all" matches value ""
+    And I click on "B question 2 name" "checkbox"
+    And I click on "C question 3 name" "checkbox"
+    And the field "Deselect all" matches value "1"
+
+  @javascript
+  Scenario: The action button can be disabled when the question not be chosen in the list of questions
+    Given the "Delete" "button" should be disabled
+    And the "Move to >>" "button" should be disabled
+    When I click on "Select all" "checkbox"
+    Then the "Delete" "button" should be enabled
+    And the "Move to >>" "button" should be enabled
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js
deleted file mode 100644 (file)
index fbe45fe..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-debug.js and /dev/null differ
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js
deleted file mode 100644 (file)
index 22b2432..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager-min.js and /dev/null differ
diff --git a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js b/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js
deleted file mode 100644 (file)
index fbe45fe..0000000
Binary files a/question/yui/build/moodle-question-qbankmanager/moodle-question-qbankmanager.js and /dev/null differ
diff --git a/question/yui/src/qbankmanager/build.json b/question/yui/src/qbankmanager/build.json
deleted file mode 100644 (file)
index 2c8d36e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-    "name": "moodle-question-qbankmanager",
-    "builds": {
-        "moodle-question-qbankmanager": {
-            "jsfiles": [
-                "qbankmanager.js"
-            ]
-        }
-    }
-}
diff --git a/question/yui/src/qbankmanager/js/qbankmanager.js b/question/yui/src/qbankmanager/js/qbankmanager.js
deleted file mode 100644 (file)
index cb6ea38..0000000
+++ /dev/null
@@ -1,137 +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/>.
-
-/*
- * Question Bank Management.
- *
- * @package    question
- * @copyright  2014 Andrew Nicols
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-/**
- * Questionbank Management.
- *
- * @module moodle-question-qbankmanager
- */
-
-/**
- * Question Bank Management.
- *
- * @class M.question.qbankmanager
- */
-
-var manager = {
-    /**
-     * A reference to the header checkbox.
-     *
-     * @property _header
-     * @type Node
-     * @private
-     */
-    _header: null,
-
-    /**
-     * A reference to the add to quiz button.
-     *
-     * @property _addbutton
-     * @type Node
-     * @private
-     */
-    _addbutton: null,
-
-    /**
-     * The ID of the first checkbox on the page.
-     *
-     * @property _firstCheckbox
-     * @type Node
-     * @private
-     */
-    _firstCheckbox: null,
-
-    /**
-     * Set up the Question Bank Manager.
-     *
-     * @method init
-     */
-    init: function() {
-        // Find the header checkbox, and set the initial values.
-        this._header = Y.one('#qbheadercheckbox');
-        if (!this._header) {
-            return;
-        }
-        this._header.setAttrs({
-            disabled: false,
-            title: M.util.get_string('selectall', 'moodle')
-        });
-
-        this._header.on('click', this._headerClick, this);
-
-        this._addbutton = Y.one('.modulespecificbuttonscontainer input[name="add"]');
-        // input[name="add"] is not always available.
-        if (this._addbutton) {
-            this._addbutton.setAttrs({
-                disabled: true
-            });
-
-            this._header.on('click', this._questionClick, this);
-            Y.one('.categoryquestionscontainer').delegate('change', this._questionClick,
-                'td.checkbox input[type="checkbox"]', this);
-        }
-
-        // Store the first checkbox details.
-        var table = this._header.ancestor('table');
-        this._firstCheckbox = table.one('tbody tr td.checkbox input');
-    },
-
-    /**
-     * Handle toggling of the header checkbox.
-     *
-     * @method _headerClick
-     * @private
-     */
-    _headerClick: function() {
-        // Get the list of questions we affect.
-        var categoryQuestions = Y.one('#categoryquestions')
-                .all('[type=checkbox],[type=radio]');
-
-        // We base the state of all of the questions on the state of the first.
-        if (this._firstCheckbox.get('checked')) {
-            categoryQuestions.set('checked', false);
-            this._header.setAttribute('title', M.util.get_string('selectall', 'moodle'));
-        } else {
-            categoryQuestions.set('checked', true);
-            this._header.setAttribute('title', M.util.get_string('deselectall', 'moodle'));
-        }
-
-        this._header.set('checked', false);
-    },
-
-    /**
-     * Handle toggling of a question checkbox.
-     *
-     * @method _questionClick
-     * @private
-     */
-    _questionClick: function() {
-        var areChecked = Y.all('td.checkbox input[type="checkbox"]:checked').size();
-        this._addbutton.setAttrs({
-            disabled: (areChecked === 0)
-        });
-    }
-};
-
-M.question = M.question || {};
-M.question.qbankmanager = M.question.qbankmanager || manager;
diff --git a/question/yui/src/qbankmanager/meta/qbankmanager.json b/question/yui/src/qbankmanager/meta/qbankmanager.json
deleted file mode 100644 (file)
index 6410cbc..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "moodle-question-qbankmanager": {
-        "requires": [
-            "node",
-            "selector-css3"
-        ]
-    }
-}
index d942fcb..b44726f 100644 (file)
@@ -51,6 +51,8 @@ $templatecontext = [
     'hasregionmainsettingsmenu' => !empty($regionmainsettingsmenu)
 ];
 
-$templatecontext['flatnavigation'] = $PAGE->flatnav;
+$nav = $PAGE->flatnav;
+$templatecontext['flatnavigation'] = $nav;
+$templatecontext['firstcollectionlabel'] = $nav->get_collectionlabel();
 echo $OUTPUT->render_from_template('theme_boost/columns2', $templatecontext);
 
index 022df60..49ed3ba 100644 (file)
@@ -47,7 +47,7 @@
     <div id="page" class="container-fluid">
         <div id="page-content" class="row pb-3">
             <div id="region-main-box" class="col-12">
-                <section id="region-main">
+                <section id="region-main" aria-label="{{#str}}content{{/str}}">
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
                     {{{ output.activity_navigation }}}
index 7b329cb..d0425d3 100644 (file)
@@ -68,7 +68,7 @@
                     <div> {{{ output.region_main_settings_menu }}} </div>
                 </div>
                 {{/hasregionmainsettingsmenu}}
-                <section id="region-main" {{#hasblocks}}class="has-blocks mb-3"{{/hasblocks}}>
+                <section id="region-main" {{#hasblocks}}class="has-blocks mb-3"{{/hasblocks}} aria-label="{{#str}}content{{/str}}">
 
                     {{#hasregionmainsettingsmenu}}
                         <div class="region_main_settings_menu_proxy"></div>
@@ -80,7 +80,7 @@
 
                 </section>
                 {{#hasblocks}}
-                <section data-region="blocks-column" class="d-print-none">
+                <section data-region="blocks-column" class="d-print-none" aria-label="{{#str}}blocks{{/str}}">
                     {{{ sidepreblocks }}}
                 </section>
                 {{/hasblocks}}
index ed8881e..a52440b 100644 (file)
@@ -21,5 +21,5 @@
     data-container="body" data-toggle="popover"
     data-placement="{{#ltr}}right{{/ltr}}{{^ltr}}left{{/ltr}}" data-content="{{text}} {{completedoclink}}"
     data-html="true" tabindex="0" data-trigger="focus">
-  {{#pix}}help, core, {{alt}}{{/pix}}
+  {{#pix}}help, core, {{{alt}}}{{/pix}}
 </a>
index 5450089..b0cdaa0 100644 (file)
                             <input type="text" name="username" id="username"
                                 class="form-control"
                                 value="{{username}}"
-                                placeholder={{#quote}}{{^canloginbyemail}}{{#str}}username{{/str}}{{/canloginbyemail}}{{#canloginbyemail}}{{#str}}usernameemail{{/str}}{{/canloginbyemail}}{{/quote}}>
+                                placeholder={{#quote}}{{^canloginbyemail}}{{#str}}username{{/str}}{{/canloginbyemail}}{{#canloginbyemail}}{{#str}}usernameemail{{/str}}{{/canloginbyemail}}{{/quote}}
+                                autocomplete="username">
                         </div>
                         <div class="form-group">
                             <label for="password" class="sr-only">{{#str}} password {{/str}}</label>
                             <input type="password" name="password" id="password" value=""
                                 class="form-control"
-                                placeholder={{#quote}}{{#str}}password{{/str}}{{/quote}}>
+                                placeholder={{#quote}}{{#str}}password{{/str}}{{/quote}}
+                                autocomplete="current-password">
                         </div>
                         {{#rememberusername}}
                             <div class="rememberpass mt-3">
index 07d0c04..207081f 100644 (file)
@@ -63,7 +63,7 @@
         ]
     }
 }}
-<nav role="navigation">
+<nav role="navigation" aria-label="{{#str}}breadcrumb, access{{/str}}">
     <ol class="breadcrumb">
         {{#get_items}}
             {{#has_action}}
index b2532fb..09f779f 100644 (file)
@@ -9,7 +9,7 @@
                 size="{{element.size}}"
                 {{#error}}
                     autofocus aria-describedby="id_error_{{element.name}}"
-                {{/error}} {{{attributes}}}>
+                {{/error}} {{{element.attributes}}}>
         {{/element.frozen}}
     {{/element}}
 {{/ core_form/element-template }}
index f19e907..3c9d44a 100644 (file)
         ]
     }
 }}
-<nav class="list-group">
+<nav class="list-group" aria-label="{{firstcollectionlabel}}">
 {{# flatnavigation }}
     {{#showdivider}}
 </nav>
-<nav class="list-group m-t-1">
+<nav class="list-group m-t-1" aria-label="{{get_collectionlabel}}">
     {{/showdivider}}
     {{#action}}
     <a class="list-group-item list-group-item-action {{#isactive}}active{{/isactive}}" href="{{{action}}}" data-key="{{key}}" data-isexpandable="{{isexpandable}}" data-indent="{{get_indent}}" data-showdivider="{{showdivider}}" data-type="{{type}}" data-nodetype="{{nodetype}}" data-collapse="{{collapse}}" data-forceopen="{{forceopen}}" data-isactive="{{isactive}}" data-hidden="{{hidden}}" data-preceedwithhr="{{preceedwithhr}}" {{#parent.key}}data-parent-key="{{.}}"{{/parent.key}}>
index 4cb4c34..9827bbd 100644 (file)
@@ -28,7 +28,7 @@
         {{{ output.login_info }}}
         <div class="tool_usertours-resettourcontainer"></div>
         {{{ output.home_link }}}
-        <nav class="nav navbar-nav d-md-none">
+        <nav class="nav navbar-nav d-md-none" aria-label="{{#str}}custommenu, admin{{/str}}">
             {{# output.custom_menu_flat }}
                 <ul class="list-unstyled pt-3">
                     {{> theme_boost/custom_menu_footer }}
@@ -38,4 +38,4 @@
         {{{ output.standard_footer_html }}}
         {{{ output.standard_end_of_body_html }}}
     </div>
-</footer>
\ No newline at end of file
+</footer>
index ebca109..f44017d 100644 (file)
@@ -40,7 +40,7 @@
     <div id="page" class="container-fluid mt-0">
         <div id="page-content" class="row">
             <div id="region-main-box" class="col-12">
-                <section id="region-main" class="col-12">
+                <section id="region-main" class="col-12" aria-label="{{#str}}content{{/str}}">
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
                     {{{ output.course_content_footer }}}
index f62c6c8..0330892 100644 (file)
@@ -56,7 +56,7 @@
         </div>
 
         <div id="page-content" class="row">
-            <section id="region-main" class="col-12">
+            <section id="region-main" class="col-12" aria-label="{{#str}}content{{/str}}">
                 {{{ output.main_content }}}
             </section>
         </div>
index dbbca33..8dadab0 100644 (file)
@@ -17,7 +17,7 @@
 {{!
     secure navbar.
 }}
-<nav class="fixed-top navbar navbar-light bg-white navbar-expand moodle-has-zindex">
+<nav class="fixed-top navbar navbar-light bg-white navbar-expand moodle-has-zindex" aria-label="{{#str}}navigation{{/str}}">
 
         <a href="{{{ config.wwwroot }}}" class="navbar-brand {{# output.should_display_navbar_logo }}has-logo{{/ output.should_display_navbar_logo }}
             {{^ output.should_display_navbar_logo }}
@@ -37,4 +37,4 @@
             {{{ output.secure_login_info }}}
             </li>
         </ul>
-</nav>
\ No newline at end of file
+</nav>
index 385e3ab..a6ceaba 100644 (file)
@@ -59,7 +59,7 @@
 
         <div id="page-content" class="row">
             <div id="region-main-box" class="col-12">
-                <section id="region-main" {{#hasblocks}}class="has-blocks"{{/hasblocks}}>
+                <section id="region-main" {{#hasblocks}}class="has-blocks"{{/hasblocks}} aria-label="{{#str}}content{{/str}}">
 
                     {{{ output.course_content_header }}}
                     {{{ output.main_content }}}
@@ -67,7 +67,7 @@
 
                 </section>
                 {{#hasblocks}}
-                <section data-region="blocks-column">
+                <section data-region="blocks-column" aria-label="{{#str}}blocks{{/str}}">
                     {{{ sidepreblocks }}}
                 </section>
                 {{/hasblocks}}
index fe2fd31..31dd330 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
 }
 
 require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Class user_editadvanced_form.
@@ -96,7 +97,8 @@ class user_editadvanced_form extends moodleform {
             }
         }
 
-        $mform->addElement('text', 'username', get_string('username'), 'size="20"');
+        $purpose = user_edit_map_field_purpose($userid, 'username');
+        $mform->addElement('text', 'username', get_string('username'), 'size="20"' . $purpose);
         $mform->addHelpButton('username', 'username', 'auth');
         $mform->setType('username', PARAM_RAW);
 
@@ -116,7 +118,9 @@ class user_editadvanced_form extends moodleform {
         if (!empty($CFG->passwordpolicy)) {
             $mform->addElement('static', 'passwordpolicyinfo', '', print_password_policy());
         }
-        $mform->addElement('passwordunmask', 'newpassword', get_string('newpassword'), 'size="20"');
+
+        $purpose = user_edit_map_field_purpose($userid, 'password');
+        $mform->addElement('passwordunmask', 'newpassword', get_string('newpassword'), 'size="20"' . $purpose);
         $mform->addHelpButton('newpassword', 'newpassword');
         $mform->setType('newpassword', core_user::get_property_type('password'));
         $mform->disabledIf('newpassword', 'createpassword', 'checked');
index b071e25..c8d2f68 100644 (file)
@@ -22,6 +22,8 @@
  * @package core_user
  */
 
+require_once($CFG->dirroot . '/user/lib.php');
+
 /**
  * Cancels the requirement for a user to update their email address.
  *
@@ -258,7 +260,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
 
     // Add the necessary names.
     foreach (useredit_get_required_name_fields() as $fullname) {
-        $mform->addElement('text', $fullname,  get_string($fullname),  'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, $fullname);
+        $mform->addElement('text', $fullname,  get_string($fullname),  'maxlength="100" size="30"' . $purpose);
         if ($stringman->string_exists('missing'.$fullname, 'core')) {
             $strmissingfield = get_string('missing'.$fullname, 'core');
         } else {
@@ -271,7 +274,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     $enabledusernamefields = useredit_get_enabled_name_fields();
     // Add the enabled additional name fields.
     foreach ($enabledusernamefields as $addname) {
-        $mform->addElement('text', $addname,  get_string($addname), 'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, $addname);
+        $mform->addElement('text', $addname,  get_string($addname), 'maxlength="100" size="30"' . $purpose);
         $mform->setType($addname, PARAM_NOTAGS);
     }
 
@@ -282,7 +286,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
                 . get_string('emailchangecancel', 'auth') . '</a>';
         $mform->addElement('static', 'emailpending', get_string('email'), $notice);
     } else {
-        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"');
+        $purpose = user_edit_map_field_purpose($user->id, 'email');
+        $mform->addElement('text', 'email', get_string('email'), 'maxlength="100" size="30"' . $purpose);
         $mform->addRule('email', $strrequired, 'required', null, 'client');
         $mform->setType('email', PARAM_RAW_TRIMMED);
     }
@@ -301,9 +306,10 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
         $mform->setDefault('city', $CFG->defaultcity);
     }
 
+    $purpose = user_edit_map_field_purpose($user->id, 'country');
     $choices = get_string_manager()->get_list_of_countries();
     $choices = array('' => get_string('selectacountry') . '...') + $choices;
-    $mform->addElement('select', 'country', get_string('selectacountry'), $choices);
+    $mform->addElement('select', 'country', get_string('selectacountry'), $choices, $purpose);
     if (!empty($CFG->country)) {
         $mform->setDefault('country', core_user::get_property_default('country'));
     }
@@ -319,7 +325,9 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     }
 
     if ($user->id < 0) {
-        $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
+        $purpose = user_edit_map_field_purpose($user->id, 'lang');
+        $translations = get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose);
         $lang = empty($user->lang) ? $CFG->lang : $user->lang;
         $mform->setDefault('lang', $lang);
     }
@@ -366,7 +374,8 @@ function useredit_shared_definition(&$mform, $editoroptions, $filemanageroptions
     if (count($disabledusernamefields) > 0) {
         $mform->addElement('header', 'moodle_additional_names', get_string('additionalnames'));
         foreach ($disabledusernamefields as $allname) {
-            $mform->addElement('text', $allname, get_string($allname), 'maxlength="100" size="30"');
+            $purpose = user_edit_map_field_purpose($user->id, $allname);
+            $mform->addElement('text', $allname, get_string($allname), 'maxlength="100" size="30"' . $purpose);
             $mform->setType($allname, PARAM_NOTAGS);
         }
     }
index 4dab6be..50d0911 100644 (file)
@@ -27,6 +27,7 @@ if (!defined('MOODLE_INTERNAL')) {
 }
 
 require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
 
 /**
  * Class user_edit_form.
@@ -57,7 +58,9 @@ class user_edit_language_form extends moodleform {
         $mform->addElement('hidden', 'course', $COURSE->id);
         $mform->setType('course', PARAM_INT);
 
-        $mform->addElement('select', 'lang', get_string('preferredlanguage'), get_string_manager()->get_list_of_translations());
+        $purpose = user_edit_map_field_purpose($userid, 'lang');
+        $translations = get_string_manager()->get_list_of_translations();
+        $mform->addElement('select', 'lang', get_string('preferredlanguage'), $translations, $purpose);
         $mform->setDefault('lang', core_user::get_property_default('lang'));
 
         $this->add_action_buttons(true, get_string('savechanges'));
index c8b5ccc..63b94c9 100644 (file)
@@ -1573,3 +1573,42 @@ function core_user_inplace_editable($itemtype, $itemid, $newvalue) {
         return \core_user\output\user_roles_editable::update($itemid, $newvalue);
     }
 }
+
+/**
+ * Map an internal field name to a valid purpose from: "https://www.w3.org/TR/WCAG21/#input-purposes"
+ *
+ * @param integer $userid
+ * @param string $fieldname
+ * @return string $purpose (empty string if there is no mapping).
+ */
+function user_edit_map_field_purpose($userid, $fieldname) {
+    global $USER;
+
+    $currentuser = ($userid == $USER->id) && !\core\session\manager::is_loggedinas();
+    // These are the fields considered valid to map and auto fill from a browser.
+    // We do not include fields that are in a collapsed section by default because
+    // the browser could auto-fill the field and cause a new value to be saved when
+    // that field was never visible.
+    $validmappings = array(
+        'username' => 'username',
+        'password' => 'current-password',
+        'firstname' => 'given-name',
+        'lastname' => 'family-name',
+        'middlename' => 'additional-name',
+        'email' => 'email',
+        'country' => 'country',
+        'lang' => 'language'
+    );
+
+    $purpose = '';
+    if (!$currentuser) {
+        // Do not set a purpose.
+        $purpose = '';
+    }
+    if (isset($validmappings[$fieldname])) {
+        $purpose = ' autocomplete="' . $validmappings[$fieldname] . '" ';
+    }
+
+    return $purpose;
+}
+
index 36339e6..2e39f12 100644 (file)
@@ -53,4 +53,37 @@ class behat_user extends behat_base {
         $this->execute("behat_general::i_click_on", array("//select[@id='formactionid']" .
                                                           "/option[contains(., " . $nodetext . ")]", "xpath_element"));
     }
+
+    /**
+     * The input field should have autocomplete set to this value.
+     *
+     * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     * @param string $field The field to select.
+     * @param string $purpose The expected purpose.
+     */
+    public function the_field_should_have_purpose($field, $purpose) {
+        $fld = behat_field_manager::get_form_field_from_label($field, $this);
+
+        $value = $fld->get_attribute('autocomplete');
+        if ($value != $purpose) {
+            $reason = 'The "' . $field . '" field does not have purpose "' . $purpose . '"';
+            throw new ExpectationException($reason, $this->getSession());
+        }
+    }
+
+    /**
+     * The input field should not have autocomplete set to this value.
+     *
+     * @Then /^the field "(?P<field_string>(?:[^"]|\\")*)" should not have purpose "(?P<purpose_string>(?:[^"]|\\")*)"$/
+     * @param string $field The field to select.
+     * @param string $purpose The expected purpose we do not want.
+     */
+    public function the_field_should_not_have_purpose($field, $purpose) {
+        $fld = behat_field_manager::get_form_field_from_label($field, $this);
+
+        $value = $fld->get_attribute('autocomplete');
+        if ($value == $purpose) {
+            throw new ExpectationException('The "' . $field . '" field does have purposea "' . $purpose . '"', $this->getSession());
+        }
+    }
 }
diff --git a/user/tests/behat/input-purpose.feature b/user/tests/behat/input-purpose.feature
new file mode 100644 (file)
index 0000000..f466f65
--- /dev/null
@@ -0,0 +1,43 @@
+@core @core_user
+Feature: The purpose of each input field collecting information about the user can be determined
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname          | lastname | email                           |
+      | unicorn  | unicorn | 1        | unicorn@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "course enrolments" exist:
+      | user                | course | role    |
+      | unicorn             | C1     | student |
+
+  @javascript
+  Scenario: Fields for other users are not auto filled
+    When I log in as "admin"
+    And I navigate to "Users > Accounts > Browse list of users" in site administration
+    And I click on ".icon[title=Edit]" "css_element" in the "unicorn@example.com" "table_row"
+    And I expand all fieldsets
+    Then the field "Username" should not have purpose "username"
+    And the field "First name" should not have purpose "given-name"
+    And the field "Surname" should not have purpose "family-name"
+    And the field "Email" should not have purpose "email"
+    And the field "Select a country" should not have purpose "country"
+    And I press "Cancel"
+    And I follow "Preferred language"
+    And the field "Preferred language" should not have purpose "language"
+
+  @javascript
+  Scenario: My own user fields are auto filled
+    When I log in as "unicorn"
+    And I follow "Profile" in the user menu
+    And I click on "Edit profile" "link" in the "region-main" "region"
+    And I expand all fieldsets
+    Then the field "First name" should have purpose "given-name"
+    And the field "Surname" should have purpose "family-name"
+    And the field "Email" should have purpose "email"
+    And the field "Select a country" should have purpose "country"
+    And I press "Cancel"
+    And I follow "Preferences" in the user menu
+    And I follow "Preferred language"
+    And the field "Preferred language" should have purpose "language"
index e978fad..1cdcc82 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019021500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019021500.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.