$data->analysables[] = $obj;
}
+ if (empty($data->analysables)) {
+ $data->noanalysables = [
+ 'message' => get_string('noinvalidanalysables', 'tool_analytics'),
+ 'announce' => true,
+ ];
+ }
return $data;
}
}
$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';
<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>
--- /dev/null
+<?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);
+ }
+}
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;
}
}
+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 {
$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>.';
--- /dev/null
+<?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('');
+ }
+}
))
),
'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.'),
));
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.
'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
+ ],
);
}
}
$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;
}
}
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;
$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) {
'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;
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">
<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}}
</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>
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"
--- /dev/null
+<?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;
+ }
+
+}
// Get needed strings
$this->loadStrings(array(
+ 'addpersistent' => 'tool_xmldb',
'change' => 'tool_xmldb',
'vieworiginal' => 'tool_xmldb',
'viewedited' => 'tool_xmldb',
$b .= '<a href="index.php?action=view_table_sql&table=' . $tableparam . '&dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' .$this->str['viewsqlcode'] . ']</a>';
// The view php code button
$b .= ' <a href="index.php?action=view_table_php&table=' . $tableparam . '&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 .= ' ' . \html_writer::link($url, '[' . $this->str['addpersistent'] . ']');
+
// The save button (if possible)
if ($cansavenow) {
$b .= ' <a href="index.php?action=save_xml_file&sesskey=' . sesskey() . '&dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '&time=' . time() . '&unload=false&postaction=edit_table&table=' . $tableparam . '&dir=' . urlencode(str_replace($CFG->dirroot, '', $dirpath)) . '">[' . $this->str['save'] . ']</a>';
*/
$string['actual'] = 'Actual';
+$string['addpersistent'] = 'Add mandatory persistent fields';
$string['aftertable'] = 'After table:';
$string['back'] = 'Back';
$string['backtomainview'] = 'Back to main';
$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';
$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"' : '';
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.
// 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;
// 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();
}
}
$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());
+ }
}
*/
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
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;
+ }
}
/**
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.
*
$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'));
+ }
+
}
$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;
}
if (!isset($moduleinfo->conditionfieldgroup)) {
$moduleinfo->conditionfieldgroup = array();
}
+ if (!isset($moduleinfo->visibleoncoursepage)) {
+ $moduleinfo->visibleoncoursepage = 1;
+ }
return $moduleinfo;
}
$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';
--- /dev/null
+// 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,
+ };
+});
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.
*
// 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.
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;
}
--- /dev/null
+// 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);
+ }
+ };
+});
+++ /dev/null
-{
- "name": "moodle-form-showadvanced",
- "builds": {
- "moodle-form-showadvanced": {
- "jsfiles": [
- "showadvanced.js"
- ]
- }
- }
-}
+++ /dev/null
-/**
- * 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);
-};
+++ /dev/null
-{
- "moodle-form-showadvanced": {
- "requires": [
- "node",
- "base",
- "selector-css3"
- ]
- }
-}
$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]);
}
}
// 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
*
$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.
*
* @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) {
*/
protected $count = 0;
+ /**
+ * Label for collection of nodes.
+ * @var string
+ */
+ protected $collectionlabel = '';
+
/**
* Adds a navigation node to the collection
*
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.
*
/** @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
*
$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
/**
* 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);
+ }
}
/**
public function set_indent($val) {
$this->indent = $val;
}
-
}
/**
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', '');
}
}
- $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);
}
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);
$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);
}
}
+
+ /**
+ * 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;
+ }
}
/**
</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>
=== 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.
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 {
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);
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir.'/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
/**
* Reset forgotten password form definition.
* 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');
$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');
* @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);
};
/**
* @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;
});
};
+ /**
+ * 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;
});
* @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]',
}
}.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, [
* @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.
*
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');
- });
};
/**
$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;
}
</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}}
// 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);
// 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]));
// 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',
$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);
}
$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',
$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);
}
// 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'));
// 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'));
// 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'));
$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.
$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'));
}
// 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'));
$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.
$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'));
}
// 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'));
// 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'));
// 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'));
$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'));
$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'));
// 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'));
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;
}
// 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();
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();
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();
$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);
+ }
+
}
--- /dev/null
+// 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();
+ },
+ };
+});
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() {
}
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() {
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.
}
echo '</div>';
+ $PAGE->requires->js_call_amd('core_question/qbankmanager', 'init');
+
$this->display_bottom_controls($totalnumber, $recurse, $category, $catcontext, $addcontexts);
echo '</fieldset>';
--- /dev/null
+@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
+++ /dev/null
-{
- "name": "moodle-question-qbankmanager",
- "builds": {
- "moodle-question-qbankmanager": {
- "jsfiles": [
- "qbankmanager.js"
- ]
- }
- }
-}
+++ /dev/null
-// 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;
+++ /dev/null
-{
- "moodle-question-qbankmanager": {
- "requires": [
- "node",
- "selector-css3"
- ]
- }
-}
'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);
<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 }}}
<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>
</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}}
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>
<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">
]
}
}}
-<nav role="navigation">
+<nav role="navigation" aria-label="{{#str}}breadcrumb, access{{/str}}">
<ol class="breadcrumb">
{{#get_items}}
{{#has_action}}
size="{{element.size}}"
{{#error}}
autofocus aria-describedby="id_error_{{element.name}}"
- {{/error}} {{{attributes}}}>
+ {{/error}} {{{element.attributes}}}>
{{/element.frozen}}
{{/element}}
{{/ core_form/element-template }}
]
}
}}
-<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}}>
{{{ 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 }}
{{{ output.standard_footer_html }}}
{{{ output.standard_end_of_body_html }}}
</div>
-</footer>
\ No newline at end of file
+</footer>
<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 }}}
</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>
{{!
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 }}
{{{ output.secure_login_info }}}
</li>
</ul>
-</nav>
\ No newline at end of file
+</nav>
<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 }}}
</section>
{{#hasblocks}}
- <section data-region="blocks-column">
+ <section data-region="blocks-column" aria-label="{{#str}}blocks{{/str}}">
{{{ sidepreblocks }}}
</section>
{{/hasblocks}}
}
require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
/**
* Class user_editadvanced_form.
}
}
- $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);
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');
* @package core_user
*/
+require_once($CFG->dirroot . '/user/lib.php');
+
/**
* Cancels the requirement for a user to update their email address.
*
// 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 {
$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);
}
. 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);
}
$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'));
}
}
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);
}
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);
}
}
}
require_once($CFG->dirroot.'/lib/formslib.php');
+require_once($CFG->dirroot.'/user/lib.php');
/**
* Class user_edit_form.
$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'));
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;
+}
+
$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());
+ }
+ }
}
--- /dev/null
+@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"
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.