/lib/yuilib/*/*/*-coverage.js
atlassian-ide-plugin.xml
/node_modules/
+/.vscode/
);
$interactive = empty($options['non-interactive']);
+$skipdatabase = $options['skip-database'];
// set up language
$lang = clean_param($options['lang'], PARAM_SAFEDIR);
}
} while ($hintdatabase !== '');
-// ask for fullname
-if ($interactive) {
- cli_separator();
- cli_heading(get_string('fullsitename', 'moodle'));
+// If --skip-database option is provided, we do not need to ask for site fullname, shortname, adminuser, adminpass, adminemail.
+// These fields will be requested during the database install part.
+if (!$skipdatabase) {
+ // Ask for fullname.
+ if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('fullsitename', 'moodle'));
- if ($options['fullname'] !== '') {
- $prompt = get_string('clitypevaluedefault', 'admin', $options['fullname']);
+ if ($options['fullname'] !== '') {
+ $prompt = get_string('clitypevaluedefault', 'admin', $options['fullname']);
+ } else {
+ $prompt = get_string('clitypevalue', 'admin');
+ }
+
+ do {
+ $options['fullname'] = cli_input($prompt, $options['fullname']);
+ } while (empty($options['fullname']));
} else {
- $prompt = get_string('clitypevalue', 'admin');
+ if (empty($options['fullname'])) {
+ $a = (object)['option' => 'fullname', 'value' => $options['fullname']];
+ cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+ }
}
- do {
- $options['fullname'] = cli_input($prompt, $options['fullname']);
- } while (empty($options['fullname']));
-} else {
- if (empty($options['fullname'])) {
- $a = (object)array('option'=>'fullname', 'value'=>$options['fullname']);
- cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
- }
-}
+ // Ask for shortname.
+ if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('shortsitename', 'moodle'));
-// ask for shortname
-if ($interactive) {
- cli_separator();
- cli_heading(get_string('shortsitename', 'moodle'));
+ if ($options['shortname'] !== '') {
+ $prompt = get_string('clitypevaluedefault', 'admin', $options['shortname']);
+ } else {
+ $prompt = get_string('clitypevalue', 'admin');
+ }
- if ($options['shortname'] !== '') {
- $prompt = get_string('clitypevaluedefault', 'admin', $options['shortname']);
+ do {
+ $options['shortname'] = cli_input($prompt, $options['shortname']);
+ } while (empty($options['shortname']));
} else {
- $prompt = get_string('clitypevalue', 'admin');
+ if (empty($options['shortname'])) {
+ $a = (object)['option' => 'shortname', 'value' => $options['shortname']];
+ cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+ }
}
- do {
- $options['shortname'] = cli_input($prompt, $options['shortname']);
- } while (empty($options['shortname']));
-} else {
- if (empty($options['shortname'])) {
- $a = (object)array('option'=>'shortname', 'value'=>$options['shortname']);
- cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+ // Ask for admin user name.
+ if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('cliadminusername', 'install'));
+ if (!empty($options['adminuser'])) {
+ $prompt = get_string('clitypevaluedefault', 'admin', $options['adminuser']);
+ } else {
+ $prompt = get_string('clitypevalue', 'admin');
+ }
+ do {
+ $options['adminuser'] = cli_input($prompt, $options['adminuser']);
+ } while (empty($options['adminuser']) or $options['adminuser'] === 'guest');
+ } else {
+ if ((empty($options['adminuser']) || $options['adminuser'] === 'guest')) {
+ $a = (object)['option' => 'adminuser', 'value' => $options['adminuser']];
+ cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+ }
}
-}
-// ask for admin user name
-if ($interactive) {
- cli_separator();
- cli_heading(get_string('cliadminusername', 'install'));
- if (!empty($options['adminuser'])) {
- $prompt = get_string('clitypevaluedefault', 'admin', $options['adminuser']);
- } else {
+ // Ask for admin user password.
+ if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('cliadminpassword', 'install'));
$prompt = get_string('clitypevalue', 'admin');
+ do {
+ $options['adminpass'] = cli_input($prompt);
+ } while (empty($options['adminpass']) or $options['adminpass'] === 'admin');
+ } else {
+ if ((empty($options['adminpass']) or $options['adminpass'] === 'admin')) {
+ $a = (object)['option' => 'adminpass', 'value' => $options['adminpass']];
+ cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+ }
}
- do {
- $options['adminuser'] = cli_input($prompt, $options['adminuser']);
- } while (empty($options['adminuser']) or $options['adminuser'] === 'guest');
-} else {
- if (empty($options['adminuser']) or $options['adminuser'] === 'guest') {
- $a = (object)array('option'=>'adminuser', 'value'=>$options['adminuser']);
- cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
+
+ // Ask for the admin email address.
+ if ($interactive) {
+ cli_separator();
+ cli_heading(get_string('cliadminemail', 'install'));
+ $prompt = get_string('clitypevaluedefault', 'admin', $options['adminemail']);
+ $options['adminemail'] = cli_input($prompt, $options['adminemail']);
}
-}
-// ask for admin user password
-if ($interactive) {
- cli_separator();
- cli_heading(get_string('cliadminpassword', 'install'));
- $prompt = get_string('clitypevalue', 'admin');
- do {
- $options['adminpass'] = cli_input($prompt);
- } while (empty($options['adminpass']) or $options['adminpass'] === 'admin');
-} else {
- if (empty($options['adminpass']) or $options['adminpass'] === 'admin') {
- $a = (object)array('option'=>'adminpass', 'value'=>$options['adminpass']);
+ // Validate that the address provided was an e-mail address.
+ if (!empty($options['adminemail']) && !validate_email($options['adminemail'])) {
+ $a = (object)['option' => 'adminemail', 'value' => $options['adminemail']];
cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
}
}
-// Ask for the admin email address.
-if ($interactive) {
- cli_separator();
- cli_heading(get_string('cliadminemail', 'install'));
- $prompt = get_string('clitypevaluedefault', 'admin', $options['adminemail']);
- $options['adminemail'] = cli_input($prompt, $options['adminemail']);
-}
-
-// Validate that the address provided was an e-mail address.
-if (!empty($options['adminemail']) && !validate_email($options['adminemail'])) {
- $a = (object) array('option' => 'adminemail', 'value' => $options['adminemail']);
- cli_error(get_string('cliincorrectvalueerror', 'admin', $a));
-}
-
// Ask for the upgrade key.
if ($interactive) {
cli_separator();
$CFG->upgradekey = $options['upgradekey'];
}
-if ($interactive) {
- if (!$options['agree-license']) {
- cli_separator();
- cli_heading(get_string('copyrightnotice'));
- echo "Moodle - Modular Object-Oriented Dynamic Learning Environment\n";
- echo get_string('gpl3')."\n\n";
- echo get_string('doyouagree')."\n";
- $prompt = get_string('cliyesnoprompt', 'admin');
- $input = cli_input($prompt, '', array(get_string('clianswerno', 'admin'), get_string('cliansweryes', 'admin')));
- if ($input == get_string('clianswerno', 'admin')) {
- exit(1);
+// The user does not also need to pass agree-license when --skip-database is provided as the user will need to accept
+// the license again in the database install part.
+if (!$skipdatabase) {
+ if ($interactive) {
+ if (!$options['agree-license']) {
+ cli_separator();
+ cli_heading(get_string('copyrightnotice'));
+ echo "Moodle - Modular Object-Oriented Dynamic Learning Environment\n";
+ echo get_string('gpl3')."\n\n";
+ echo get_string('doyouagree')."\n";
+ $prompt = get_string('cliyesnoprompt', 'admin');
+ $input = cli_input($prompt, '', array(get_string('clianswerno', 'admin'), get_string('cliansweryes', 'admin')));
+ if ($input == get_string('clianswerno', 'admin')) {
+ exit(1);
+ }
+ }
+ } else {
+ if (!$options['agree-license'] && !$skipdatabase) {
+ cli_error(get_string('climustagreelicense', 'install'));
}
- }
-} else {
- if (!$options['agree-license']) {
- cli_error(get_string('climustagreelicense', 'install'));
}
}
cli_error(get_string('pluginschecktodo', 'admin'));
}
-if (!$options['skip-database']) {
+if (!$skipdatabase) {
install_cli_database($options, $interactive);
// This needs to happen at the end to ensure it occurs after all caches
// have been purged for the last time.
Options:
-h --help Print this help.
--show-all Displays a list of all installed plugins.
+ --show-contrib Displays a list of all third-party installed plugins.
--show-missing Displays a list of plugins missing from disk.
--purge-missing Uninstall all missing from disk plugins.
--plugins=<plugin name> A comma separated list of plugins to be uninstalled. E.g. mod_assign,mod_forum
# php uninstall_plugins.php --show-all
Prints tab-separated list of all installed plugins.
+ # php uninstall_plugins.php --show-contrib
+ Prints tab-separated list of all third-party installed plugins.
+
# php uninstall_plugins.php --show-missing
Prints tab-separated list of all missing from disk plugins.
list($options, $unrecognised) = cli_get_params([
'help' => false,
'show-all' => false,
+ 'show-contrib' => false,
'show-missing' => false,
'purge-missing' => false,
'plugins' => false,
$pluginman = core_plugin_manager::instance();
$plugininfo = $pluginman->get_plugins();
-if ($options['show-all'] || $options['show-missing']) {
+if ($options['show-all'] || $options['show-missing'] || $options['show-contrib']) {
foreach ($plugininfo as $type => $plugins) {
foreach ($plugins as $name => $plugin) {
+ if ($options['show-contrib'] && $plugin->is_standard()) {
+ continue;
+ }
$pluginstring = $plugin->component . "\t" . $plugin->displayname;
- if ($options['show-all']) {
+ if ($options['show-all'] || $options['show-contrib']) {
cli_writeln($pluginstring);
} else {
if ($plugin->get_status() === core_plugin_manager::PLUGIN_STATUS_MISSING) {
$temp->add(new admin_setting_configcheckbox('debugdisplay', new lang_string('debugdisplay', 'admin'), new lang_string('configdebugdisplay', 'admin'), ini_get_bool('display_errors')));
$temp->add(new admin_setting_configcheckbox('perfdebug', new lang_string('perfdebug', 'admin'), new lang_string('configperfdebug', 'admin'), '7', '15', '7'));
$temp->add(new admin_setting_configcheckbox('debugstringids', new lang_string('debugstringids', 'admin'), new lang_string('debugstringids_desc', 'admin'), 0));
+ $temp->add(new admin_setting_configselect('debugsqltrace',
+ new lang_string('debugsqltrace', 'admin'),
+ new lang_string('debugsqltrace_desc', 'admin'), 0, array(
+ 0 => new lang_string('disabled', 'admin'),
+ 1 => new lang_string('debugsqltrace1', 'admin'),
+ 2 => new lang_string('debugsqltrace2', 'admin'),
+ 100 => new lang_string('debugsqltrace100', 'admin'))));
$temp->add(new admin_setting_configcheckbox('debugvalidators', new lang_string('debugvalidators', 'admin'), new lang_string('configdebugvalidators', 'admin'), 0));
$temp->add(new admin_setting_configcheckbox('debugpageinfo', new lang_string('debugpageinfo', 'admin'), new lang_string('configdebugpageinfo', 'admin'), 0));
$ADMIN->add('development', $temp);
defined('MOODLE_INTERNAL') || die();
+use lang_string;
use core\persistent;
/**
- * Class for loading/storing competencies from the DB.
+ * Class for loading/storing data requests from the DB.
*
* @copyright 2018 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
],
'comments' => [
'type' => PARAM_TEXT,
+ 'message' => new lang_string('errorinvalidrequestcomments', 'tool_dataprivacy'),
'default' => ''
],
'commentsformat' => [
'default' => FORMAT_PLAIN
],
'userid' => [
- 'default' => 0,
+ 'default' => function() {
+ global $USER;
+ return $USER->id;
+ },
'type' => PARAM_INT
],
'requestedby' => [
redirect($returnurl, get_string('contactdpoviaprivacypolicy', 'tool_dataprivacy'), 0, \core\output\notification::NOTIFY_ERROR);
}
-$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage)]);
-$mform->set_data(['type' => $requesttype]);
+$mform = new tool_dataprivacy_data_request_form($url->out(false), ['manage' => !empty($manage),
+ 'persistent' => new \tool_dataprivacy\data_request(0, (object) ['type' => $requesttype])]);
// Data request cancelled.
if ($mform->is_cancelled()) {
*/
use tool_dataprivacy\api;
+use tool_dataprivacy\data_request;
use tool_dataprivacy\local\helper;
defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
* @package tool_dataprivacy
*/
-class tool_dataprivacy_data_request_form extends moodleform {
+class tool_dataprivacy_data_request_form extends \core\form\persistent {
+
+ /** @var string Name of the persistent class. */
+ protected static $persistentclass = data_request::class;
/** @var bool Flag to indicate whether this form is being rendered for managing data requests or for regular requests. */
protected $manage = false;
api::DATAREQUEST_TYPE_EXPORT => get_string('requesttypeexport', 'tool_dataprivacy'),
api::DATAREQUEST_TYPE_DELETE => get_string('requesttypedelete', 'tool_dataprivacy')
];
+
$mform->addElement('select', 'type', get_string('requesttype', 'tool_dataprivacy'), $options);
- $mform->setType('type', PARAM_INT);
$mform->addHelpButton('type', 'requesttype', 'tool_dataprivacy');
// Request comments text area.
$textareaoptions = ['cols' => 60, 'rows' => 10];
$mform->addElement('textarea', 'comments', get_string('requestcomments', 'tool_dataprivacy'), $textareaoptions);
- $mform->setType('type', PARAM_ALPHANUM);
$mform->addHelpButton('comments', 'requestcomments', 'tool_dataprivacy');
// Action buttons.
}
}
+ /**
+ * Get the default data. Unset the default userid if managing data requests
+ *
+ * @return stdClass
+ */
+ protected function get_default_data() {
+ $data = parent::get_default_data();
+ if ($this->manage) {
+ unset($data->userid);
+ }
+
+ return $data;
+ }
+
/**
* Form validation.
*
- * @param array $data
+ * @param stdClass $data
* @param array $files
+ * @param array $errors
* @return array
* @throws coding_exception
* @throws dml_exception
*/
- public function validation($data, $files) {
+ public function extra_validation($data, $files, array &$errors) {
global $USER;
- $errors = [];
$validrequesttypes = [
api::DATAREQUEST_TYPE_EXPORT,
api::DATAREQUEST_TYPE_DELETE
];
- if (!in_array($data['type'], $validrequesttypes)) {
+ if (!in_array($data->type, $validrequesttypes)) {
$errors['type'] = get_string('errorinvalidrequesttype', 'tool_dataprivacy');
}
- if (api::has_ongoing_request($data['userid'], $data['type'])) {
+ $userid = $data->userid;
+
+ if (api::has_ongoing_request($userid, $data->type)) {
$errors['type'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy');
}
// Check if current user can create data deletion request.
- $userid = $data['userid'];
- if ($data['type'] == api::DATAREQUEST_TYPE_DELETE) {
+ if ($data->type == api::DATAREQUEST_TYPE_DELETE) {
if ($userid == $USER->id) {
if (!api::can_create_data_deletion_request_for_self()) {
$errors['type'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy');
$string['emailsalutation'] = 'Dear {$a},';
$string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
$string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
+$string['errorinvalidrequestcomments'] = 'Please ensure your comment contains plain text only.';
$string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
$string['errorinvalidrequeststatus'] = 'Invalid request status!';
$string['errorinvalidrequesttype'] = 'Invalid request type!';
// Saving data.
$this->data = $coursedata;
+
+ // Get enrolment data. Where the course already exists, we can also perform validation.
$this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
+ if ($exists) {
+ $errors = $this->validate_enrolment_data($coursedata['id'], $this->enrolmentdata);
+
+ if (!empty($errors)) {
+ foreach ($errors as $key => $message) {
+ $this->error($key, $message);
+ }
+
+ return false;
+ }
+ }
if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') {
$this->data['tags'] = preg_split('/\s*,\s*/', trim($this->rawdata['tags']), -1, PREG_SPLIT_NO_EMPTY);
$context->mark_dirty();
}
+ /**
+ * Validate passed enrolment data against an existing course
+ *
+ * @param int $courseid
+ * @param array[] $enrolmentdata
+ * @return lang_string[] Errors keyed on error code
+ */
+ protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array {
+ // Nothing to validate.
+ if (empty($enrolmentdata)) {
+ return [];
+ }
+
+ $errors = [];
+
+ $enrolmentplugins = tool_uploadcourse_helper::get_enrolment_plugins();
+ $instances = enrol_get_instances($courseid, false);
+
+ foreach ($enrolmentdata as $method => $options) {
+ $plugin = $enrolmentplugins[$method];
+
+ // Find matching instances by enrolment method.
+ $methodinstances = array_filter($instances, static function(stdClass $instance) use ($method) {
+ return (strcmp($instance->enrol, $method) == 0);
+ });
+
+ if (!empty($options['delete'])) {
+ // Ensure user is able to delete the instances.
+ foreach ($methodinstances as $methodinstance) {
+ if (!$plugin->can_delete_instance($methodinstance)) {
+ $errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($methodinstance));
+
+ break;
+ }
+ }
+ } else if (!empty($options['disable'])) {
+ // Ensure user is able to toggle instance statuses.
+ foreach ($methodinstances as $methodinstance) {
+ if (!$plugin->can_hide_show_instance($methodinstance)) {
+ $errors['errorcannotdisableenrolment'] =
+ new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($methodinstance));
+
+ break;
+ }
+ }
+ } else {
+ // Ensure user is able to create/update instance.
+ $methodinstance = empty($methodinstances) ? null : reset($methodinstances);
+ if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) ||
+ (!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) {
+
+ $errors['errorcannotcreateorupdateenrolment'] =
+ new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($methodinstance));
+
+ break;
+ }
+ }
+ }
+
+ return $errors;
+ }
+
/**
* Add the enrolment data for the course.
*
foreach ($instances as $instance) {
if ($instance->enrol == $enrolmethod) {
$plugin = $enrolmentplugins[$instance->enrol];
- $plugin->delete_instance($instance);
+
+ // Ensure user is able to delete the instance.
+ if ($plugin->can_delete_instance($instance)) {
+ $plugin->delete_instance($instance);
+ } else {
+ $this->error('errorcannotdeleteenrolment',
+ new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($instance)));
+ }
+
break;
}
}
foreach ($instances as $instance) {
if ($instance->enrol == $enrolmethod) {
$plugin = $enrolmentplugins[$instance->enrol];
- $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
- $enrol_updated = true;
+
+ // Ensure user is able to toggle instance status.
+ if ($plugin->can_hide_show_instance($instance)) {
+ $plugin->update_status($instance, ENROL_INSTANCE_DISABLED);
+ } else {
+ $this->error('errorcannotdisableenrolment',
+ new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($instance)));
+ }
+
break;
}
}
} else {
- $plugin = null;
- if (empty($instance)) {
- $plugin = $enrolmentplugins[$enrolmethod];
+ // Create/update enrolment.
+ $plugin = $enrolmentplugins[$enrolmethod];
+
+ // Ensure user is able to create/update instance.
+ if (empty($instance) && $plugin->can_add_instance($course->id)) {
$instance = new stdClass();
$instance->id = $plugin->add_default_instance($course);
$instance->roleid = $plugin->get_config('roleid');
$instance->status = ENROL_INSTANCE_ENABLED;
- } else {
- $plugin = $enrolmentplugins[$instance->enrol];
+ } else if (!empty($instance) && $plugin->can_edit_instance($instance)) {
$plugin->update_status($instance, ENROL_INSTANCE_ENABLED);
+ } else {
+ $this->error('errorcannotcreateorupdateenrolment',
+ new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
+ $plugin->get_instance_name($instance)));
+
+ break;
}
// Now update values.
*
* The result is cached for faster execution.
*
- * @return array
+ * @return enrol_plugin[]
*/
public static function get_enrolment_plugins() {
$cache = cache::make('tool_uploadcourse', 'helper');
$string['defaultvaluescustomfieldcategory'] = 'Default values for \'{$a}\'';
$string['encoding'] = 'Encoding';
$string['encoding_help'] = 'Encoding of the CSV file.';
+$string['errorcannotcreateorupdateenrolment'] = 'Cannot create or update enrolment method \'{$a}\'';
+$string['errorcannotdeleteenrolment'] = 'Cannot delete enrolment method \'{$a}\'';
+$string['errorcannotdisableenrolment'] = 'Cannot disable enrolment method \'{$a}\'';
$string['errorwhilerestoringcourse'] = 'Error while restoring the course';
$string['errorwhiledeletingcourse'] = 'Error while deleting the course';
$string['generatedshortnameinvalid'] = 'The generated shortname is invalid';
public function test_enrolment_data() {
$this->resetAfterTest(true);
+ // We need to set the current user as one with the capability to edit manual enrolment instances in the new course.
+ $this->setAdminUser();
+
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
$updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
$data = array('shortname' => 'c1', 'summary' => 'S', 'fullname' => 'FN', 'category' => '1');
$this->assertEquals(strtotime('12th July 2013'), $enroldata['manual']->enrolenddate);
}
+ /**
+ * Data provider for testing enrolment errors
+ *
+ * @return array
+ */
+ public function enrolment_uploaddata_error_provider(): array {
+ return [
+ ['errorcannotcreateorupdateenrolment', [
+ 'shortname' => 'C1',
+ 'enrolment_1' => 'manual',
+ ]],
+ ['errorcannotdeleteenrolment', [
+ 'shortname' => 'C1',
+ 'enrolment_1' => 'manual',
+ 'enrolment_1_delete' => '1',
+ ]],
+ ['errorcannotdisableenrolment', [
+ 'shortname' => 'C1',
+ 'enrolment_1' => 'manual',
+ 'enrolment_1_disable' => '1',
+ ]],
+ ];
+ }
+
+ /**
+ * Test that user without permission, cannot modify enrolment instances when creating courses
+ *
+ * @param string $expectederror
+ * @param array $uploaddata
+ *
+ * @dataProvider enrolment_uploaddata_error_provider
+ */
+ public function test_enrolment_error_create_course(string $expectederror, array $uploaddata): void {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ // Create category in which to create the new course.
+ $category = $this->getDataGenerator()->create_category();
+ $categorycontext = context_coursecat::instance($category->id);
+
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ // Assign the user as a manager of the category, disable ability to configure manual enrolment instances.
+ $roleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+ role_assign($roleid, $user->id, $categorycontext);
+ role_change_permission($roleid, $categorycontext, 'enrol/manual:config', CAP_PROHIBIT);
+
+ $mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+
+ $upload = new tool_uploadcourse_course($mode, $updatemode, array_merge($uploaddata, [
+ 'category' => $category->id,
+ 'fullname' => 'My course',
+ ]));
+
+ // Enrolment validation isn't performed during 'prepare' for new courses.
+ $this->assertTrue($upload->prepare());
+ $upload->proceed();
+
+ $errors = $upload->get_errors();
+ $this->assertArrayHasKey($expectederror, $errors);
+
+ $this->assertEquals(get_string($expectederror, 'tool_uploadcourse', 'Manual enrolments'),
+ (string) $errors[$expectederror]);
+ }
+
+ /**
+ * Test that user without permission, cannot modify enrolment instances when updating courses
+ *
+ * @param string $expectederror
+ * @param array $uploaddata
+ *
+ * @dataProvider enrolment_uploaddata_error_provider
+ */
+ public function test_enrolment_error_update_course(string $expectederror, array $uploaddata): void {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ // Create category in which to create the new course.
+ $category = $this->getDataGenerator()->create_category();
+ $categorycontext = context_coursecat::instance($category->id);
+
+ $course = $this->getDataGenerator()->create_course([
+ 'category' => $category->id,
+ 'shortname' => $uploaddata['shortname'],
+ ]);
+
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ // Assign the user as a manager of the category, disable ability to configure manual enrolment instances.
+ $roleid = $DB->get_field('role', 'id', ['shortname' => 'manager']);
+ role_assign($roleid, $user->id, $categorycontext);
+ role_change_permission($roleid, $categorycontext, 'enrol/manual:config', CAP_PROHIBIT);
+
+ // Sanity check.
+ $instances = enrol_get_instances($course->id, true);
+ $this->assertCount(1, $instances);
+ $this->assertEquals('manual', reset($instances)->enrol);
+
+ $mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
+ $updatemode = tool_uploadcourse_processor::UPDATE_ALL_WITH_DATA_ONLY;
+
+ $upload = new tool_uploadcourse_course($mode, $updatemode, $uploaddata);
+
+ $this->assertFalse($upload->prepare());
+
+ $errors = $upload->get_errors();
+ $this->assertArrayHasKey($expectederror, $errors);
+
+ $this->assertEquals(get_string($expectederror, 'tool_uploadcourse', 'Manual enrolments'),
+ (string) $errors[$expectederror]);
+ }
+
/**
* Test upload processing of course custom fields
*/
*/
import $ from 'jquery';
+import * as Aria from 'core/aria';
import Popper from 'core/popper';
/**
});
}
- this.listeners.forEach(function (listener) {
+ this.listeners.forEach(function(listener) {
listener.node.on.apply(listener.node, listener.args);
});
let hidden = child.attr(attrName);
if (!hidden) {
child.attr(stateHolder, true);
- child.attr(attrName, true);
+ Aria.hide(child);
}
};
*/
accessibilityHide() {
let stateHolder = 'data-has-hidden';
- let attrName = 'aria-hidden';
let showFunction = function(child) {
let hidden = child.attr(stateHolder);
if (typeof hidden !== 'undefined') {
child.removeAttr(stateHolder);
- child.removeAttr(attrName);
+ Aria.unhide(child);
}
};
$physicaldefault = null;
}
+ // For number fields there are issues with type differences, so let's convert
+ // everything to a float.
+ if ($xmldbfield->getType() === XMLDB_TYPE_NUMBER) {
+ if ($physicaldefault !== null) {
+ $physicaldefault = (float) $physicaldefault;
+ }
+ if ($xmldbdefault !== null) {
+ $xmldbdefault = (float) $xmldbdefault;
+ }
+ }
+
// There *is* a default and it's wrong.
if ($physicaldefault !== $xmldbdefault) {
$xmldbtext = self::display_default($xmldbdefault);
LEFT JOIN {context} ctx ON ap.contextid = ctx.id
WHERE ctx.id IS NULL)");
- $contextsql = "SELECT id FROM {context} ctx";
- $DB->delete_records_select('analytics_predictions', "contextid NOT IN ($contextsql)");
- $DB->delete_records_select('analytics_indicator_calc', "contextid NOT IN ($contextsql)");
+ // Cleanup analaytics predictions/calcs with MySQL friendly sub-select.
+ $DB->execute("DELETE FROM {analytics_predictions} WHERE id IN (
+ SELECT oldpredictions.id
+ FROM (
+ SELECT p.id
+ FROM {analytics_predictions} p
+ LEFT JOIN {context} ctx ON p.contextid = ctx.id
+ WHERE ctx.id IS NULL
+ ) oldpredictions
+ )");
+
+ $DB->execute("DELETE FROM {analytics_indicator_calc} WHERE id IN (
+ SELECT oldcalcs.id FROM (
+ SELECT c.id
+ FROM {analytics_indicator_calc} c
+ LEFT JOIN {context} ctx ON c.contextid = ctx.id
+ WHERE ctx.id IS NULL
+ ) oldcalcs
+ )");
// Clean up stuff that depends on analysable ids that do not exist anymore.
$confirmationpath = $confirmationurl->out_as_local_url(false);
$url = $confirmationpath . '?' . 'data='. $secret .'/'. $username;
- $this->getSession()->visit($this->locate_path($url));
+ $this->execute('behat_general::i_visit', [$url]);
}
}
}
// Visit login page.
- $this->getSession()->visit($this->locate_path($loginurl->out_as_local_url()));
+ $this->execute('behat_general::i_visit', [$loginurl]);
// Enter username and password.
$this->execute('behat_forms::i_set_the_field_to', array('Username', $this->escape($username)));
// table elements are used, and we need to catch exceptions contantly.
// Go to homepage.
- $this->getSession()->visit($this->locate_path('/?redirect=0'));
+ $this->execute('behat_general::i_visit', ['/?redirect=0']);
$this->execute("behat_general::wait_until_the_page_is_ready");
// Click the course link.
// table elements are used, and we need to catch exceptions contantly.
// Go to homepage.
- $this->getSession()->visit($this->locate_path('/?redirect=0'));
+ $this->execute('behat_general::i_visit', ['/?redirect=0']);
// Click the course link.
$this->execute("behat_general::click_link", $backupcourse);
// table elements are used, and we need to catch exceptions contantly.
// Go to homepage.
- $this->getSession()->visit($this->locate_path('/?redirect=0'));
+ $this->execute('behat_general::i_visit', ['/?redirect=0']);
$this->execute("behat_general::wait_until_the_page_is_ready");
// Click the course link.
'core_course/events',
'block_myoverview/selectors',
'core/paged_content_events',
+ 'core/aria',
],
function(
$,
Templates,
CourseEvents,
Selectors,
- PagedContentEvents
+ PagedContentEvents,
+ Aria
) {
var SELECTORS = {
*/
var hideFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
+
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.addClass('hidden');
- isFavouriteIcon.attr('aria-hidden', true);
+ Aria.hide(isFavouriteIcon);
+
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.removeClass('hidden');
- notFavourteIcon.attr('aria-hidden', false);
+ Aria.unhide(notFavourteIcon);
};
/**
*/
var showFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
+
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.removeClass('hidden');
- isFavouriteIcon.attr('aria-hidden', false);
+ Aria.unhide(isFavouriteIcon);
+
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.addClass('hidden');
- notFavourteIcon.attr('aria-hidden', true);
+ Aria.hide(notFavourteIcon);
};
/**
* @copyright 2015 John Okely <john@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/templates', 'core/notification', 'core/url'], function($, Templates, Notification, Url) {
+define([
+ 'jquery',
+ 'core/templates',
+ 'core/notification',
+ 'core/url',
+ 'core/aria',
+], function(
+ $,
+ Templates,
+ Notification,
+ Url,
+ Aria
+) {
// Mappings for the different types of nodes coming from the navigation.
// Copied from lib/navigationlib.php navigation_node constants.
function buildDOM(rootElement, nodes) {
var ul = $('<ul></ul>');
ul.attr('role', 'group');
- ul.attr('aria-hidden', true);
+ Aria.hide(ul);
$.each(nodes, function(index, node) {
if (typeof node !== 'object') {
var group = element.find('#' + item.attr('aria-owns'));
item.attr('aria-expanded', true);
- group.attr('aria-hidden', false);
+ Aria.unhide(group);
} else {
if (element.parent().hasClass('contains_branch')) {
element.parent().removeClass('contains_branch');
'core/templates',
'core_course/events',
'core_course/repository',
+ 'core/aria',
],
function(
$,
PagedContentPagingBar,
Templates,
CourseEvents,
- CoursesRepository
+ CoursesRepository,
+ Aria
) {
// Constants.
var pagingBar = root.find(SELECTORS.PAGING_BAR);
pagingBar.css('opacity', 1);
pagingBar.css('visibility', 'visible');
- pagingBar.attr('aria-hidden', 'false');
+ Aria.unhide(pagingBar);
};
/**
var pagingBar = root.find(SELECTORS.PAGING_BAR);
pagingBar.css('opacity', 0);
pagingBar.css('visibility', 'hidden');
- pagingBar.attr('aria-hidden', 'true');
+ Aria.hide(pagingBar);
};
/**
*/
public function i_view_the_calendar_for($month, $year) {
$time = make_timestamp($year, $month, 1);
- $this->getSession()->visit($this->locate_path('/calendar/view.php?view=month&course=1&time='.$time));
+ $this->execute('behat_general::i_visit', ['/calendar/view.php?view=month&course=1&time='.$time]);
}
*/
public function i_am_viewing_site_calendar() {
$url = new moodle_url('/calendar/view.php', ['view' => 'month']);
- $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
+ $this->execute('behat_general::i_visit', [$url]);
}
}
const sectionModal = buildModal(bodyPromise, footerData);
// Now we have a modal we should start fetching data.
- const data = await fetchModuleData();
+ // If an error occurs while fetching the data, display the error within the modal.
+ const data = await fetchModuleData().catch(async(e) => {
+ const errorTemplateData = {
+ 'errormessage': e.message
+ };
+ bodyPromiseResolver(await Templates.render('core_course/local/activitychooser/error', errorTemplateData));
+ });
+
+ // Early return if there is no module data.
+ if (!data) {
+ return;
+ }
// Apply the section id to all the module instance links.
const builtModuleData = sectionIdMapper(data, caller.dataset.sectionid, caller.dataset.sectionreturnid);
return $favmods;
}
+ // Make sure the guest user exists in the database.
+ if (!\core_user::get_user($CFG->siteguest)) {
+ throw new \coding_exception('The guest user does not exist in the database.');
+ }
+
$favourites = $this->get_content_favourites(self::RECOMMENDATION_PREFIX, \context_user::instance($CFG->siteguest));
$recommendationcache->set($CFG->siteguest, $favourites);
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Specialised restore for format_topics
+ * Specialised restore for Topics course format.
*
* @package format_topics
* @category backup
defined('MOODLE_INTERNAL') || die();
/**
- * Specialised restore for format_topics
+ * Specialised restore for Topics course format.
*
- * Processes 'numsections' from the old backup files and hides sections that used to be "orphaned"
+ * Processes 'numsections' from the old backup files and hides sections that used to be "orphaned".
*
* @package format_topics
* @category backup
}
/**
- * Creates a dummy path element in order to be able to execute code after restore
+ * Creates a dummy path element in order to be able to execute code after restore.
*
* @return restore_path_element[]
*/
}
/**
- * Dummy process method
+ * Dummy process method.
+ *
+ * @return void
*/
public function process_dummy_course() {
}
/**
- * Executed after course restore is complete
+ * Executed after course restore is complete.
+ *
+ * This method is only executed if course configuration was overridden.
*
- * This method is only executed if course configuration was overridden
+ * @return void
*/
public function after_restore_course() {
global $DB;
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
/**
- * Privacy Subsystem implementation for format_topics.
+ * Privacy Subsystem implementation for Topics course format.
*
* @package format_topics
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+
namespace format_topics\privacy;
+
defined('MOODLE_INTERNAL') || die();
+
+use core_privacy\local\metadata\null_provider;
+
/**
- * Privacy Subsystem for format_topics implementing null_provider.
+ * Privacy Subsystem for Topics course format implementing null_provider.
*
* @copyright 2018 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class provider implements \core_privacy\local\metadata\null_provider {
+class provider implements null_provider {
+
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
- public static function get_reason() : string {
+ public static function get_reason(): string {
return 'privacy:metadata';
}
}
\ No newline at end of file
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Upgrade scripts for course format "Topics"
+ * Upgrade scripts for Topics course format.
*
* @package format_topics
* @copyright 2017 Marina Glancy
defined('MOODLE_INTERNAL') || die();
/**
- * Upgrade script for format_topics
+ * Upgrade script for Topics course format.
*
- * @param int $oldversion the version we are upgrading from
+ * @param int|float $oldversion the version we are upgrading from
* @return bool result
*/
function xmldb_format_topics_upgrade($oldversion) {
-// Javascript functions for Topics course format
+// Javascript functions for Topics course format.
M.course = M.course || {};
M.course.format = M.course.format || {};
/**
- * Get sections config for this format
+ * Get sections config for this format.
*
* The section structure is:
* <ul class="topics">
*/
M.course.format.get_config = function() {
return {
- container_node : 'ul',
- container_class : 'topics',
- section_node : 'li',
- section_class : 'section'
+ container_node: 'ul',
+ container_class: 'topics',
+ section_node: 'li',
+ section_class: 'section'
};
-}
+};
/**
- * Swap section
+ * Swap section.
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
- * @return {NodeList} section list
*/
M.course.format.swap_sections = function(Y, node1, node2) {
var CSS = {
- COURSECONTENT : 'course-content',
- SECTIONADDMENUS : 'section_add_menus'
+ COURSECONTENT: 'course-content',
+ SECTIONADDMENUS: 'section_add_menus'
};
- var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.course.format.get_section_selector(Y));
+ var sectionlist = Y.Node.all('.' + CSS.COURSECONTENT + ' ' + M.course.format.get_section_selector(Y));
// Swap the non-ajax menus, noting these are not always present (depends on theme and user prefs).
- if (sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS)) {
- sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
+ if (sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS)) {
+ sectionlist.item(node1).one('.' + CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.' + CSS.SECTIONADDMENUS));
}
-}
+};
/**
- * Process sections after ajax response
+ * Process sections after ajax response.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} sectionlist of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
- * @return void
*/
M.course.format.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
- SECTIONNAME : 'sectionname'
+ SECTIONNAME: 'sectionname'
},
SELECTORS = {
- SECTIONLEFTSIDE : '.left .section-handle .icon'
+ SECTIONLEFTSIDE: '.left .section-handle .icon'
};
if (response.action == 'move') {
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
var content = Y.Node.create('<span>' + response.sectiontitles[i] + '</span>');
- sectionlist.item(i).all('.'+CSS.SECTIONNAME).setHTML(content);
+ sectionlist.item(i).all('.' + CSS.SECTIONNAME).setHTML(content);
// Update the drag handle.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE).ancestor('.section-handle');
str = ele.getAttribute('title');
stridx = str.lastIndexOf(' ');
- newstr = str.substr(0, stridx +1) + i;
+ newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('title', newstr);
// Update the aria-label for the section.
sectionlist.item(i).setAttribute('aria-label', content.get('innerText').trim());
}
}
-}
+};
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Topics course format. Display the whole course as "topics" made of modules.
+ * Topics course format. Display the whole course as "topics" made of modules.
*
* @package format_topics
* @copyright 2006 The Open University
require_once($CFG->libdir.'/filelib.php');
require_once($CFG->libdir.'/completionlib.php');
-// Horrible backwards compatible parameter aliasing..
+// Horrible backwards compatible parameter aliasing.
if ($topic = optional_param('topic', 0, PARAM_INT)) {
$url = $PAGE->url;
$url->param('section', $topic);
debugging('Outdated topic param passed to course/view.php', DEBUG_DEVELOPER);
redirect($url);
}
-// End backwards-compatible aliasing..
+// End backwards-compatible aliasing.
$context = context_course::instance($course->id);
// Retrieve course format option fields and add them to the $course object.
$course = course_get_format($course)->get_course();
-if (($marker >=0) && has_capability('moodle/course:setcurrentsection', $context) && confirm_sesskey()) {
+if (($marker >= 0) && has_capability('moodle/course:setcurrentsection', $context) && confirm_sesskey()) {
$course->marker = $marker;
course_set_marker($course->id, $marker);
}
$renderer->print_multiple_section_page($course, null, null, null, null);
}
-// Include course format js module
+// Include course format js module.
$PAGE->requires->js('/course/format/topics/format.js');
<?php
-
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Strings for component 'format_topics', language 'en', branch 'MOODLE_20_STABLE'
+ * Strings for component Topics course format.
*
* @package format_topics
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * This file contains main class for the course format Topic
+ * This file contains main class for Topics course format.
*
* @since Moodle 2.0
* @package format_topics
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot. '/course/format/lib.php');
+use core\output\inplace_editable;
+
/**
- * Main class for the Topics course format
+ * Main class for the Topics course format.
*
* @package format_topics
* @copyright 2012 Marina Glancy
class format_topics extends format_base {
/**
- * Returns true if this course format uses sections
+ * Returns true if this course format uses sections.
*
* @return bool
*/
/**
* Returns the display name of the given section that the course prefers.
*
- * Use section name is specified by user. Otherwise use default ("Topic #")
+ * Use section name is specified by user. Otherwise use default ("Topic #").
*
* @param int|stdClass $section Section object from database or just field section.section
* @return string Display name that the course format prefers, e.g. "Topic 2"
$section = $this->get_section($section);
if ((string)$section->name !== '') {
return format_string($section->name, true,
- array('context' => context_course::instance($this->courseid)));
+ ['context' => context_course::instance($this->courseid)]);
} else {
return $this->get_default_section_name($section);
}
}
/**
- * The URL to use for the specified course (with section)
+ * The URL to use for the specified course (with section).
*
* @param int|stdClass $section Section object from database or just field course_sections.section
* if omitted the course view page is returned
* 'sr' (int) used by multipage formats to specify to which section to return
* @return null|moodle_url
*/
- public function get_view_url($section, $options = array()) {
+ public function get_view_url($section, $options = []) {
global $CFG;
$course = $this->get_course();
- $url = new moodle_url('/course/view.php', array('id' => $course->id));
+ $url = new moodle_url('/course/view.php', ['id' => $course->id]);
$sr = null;
if (array_key_exists('sr', $options)) {
}
/**
- * Returns the information about the ajax support in the given source format
+ * Returns the information about the ajax support in the given source format.
*
* The returned object's property (boolean)capable indicates that
* the course format supports Moodle course ajax features.
}
/**
- * Loads all of the course sections into the navigation
+ * Loads all of the course sections into the navigation.
*
* @param global_navigation $navigation
* @param navigation_node $node The course node within the navigation
+ * @return void
*/
public function extend_course_navigation($navigation, navigation_node $node) {
global $PAGE;
- // if section is specified in course/view.php, make sure it is expanded in navigation
+ // If section is specified in course/view.php, make sure it is expanded in navigation.
if ($navigation->includesectionnum === false) {
$selectedsection = optional_param('section', null, PARAM_INT);
if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') &&
}
}
- // check if there are callbacks to extend course navigation
+ // Check if there are callbacks to extend course navigation.
parent::extend_course_navigation($navigation, $node);
// We want to remove the general section if it is empty.
}
/**
- * Custom action after section has been moved in AJAX mode
+ * Custom action after section has been moved in AJAX mode.
*
* Used in course/rest.php
*
* @return array This will be passed in ajax respose
*/
- function ajax_section_move() {
+ public function ajax_section_move() {
global $PAGE;
- $titles = array();
+ $titles = [];
$course = $this->get_course();
$modinfo = get_fast_modinfo($course);
$renderer = $this->get_renderer($PAGE);
$titles[$number] = $renderer->section_title($section, $course);
}
}
- return array('sectiontitles' => $titles, 'action' => 'move');
+ return ['sectiontitles' => $titles, 'action' => 'move'];
}
/**
- * Returns the list of blocks to be automatically added for the newly created course
+ * Returns the list of blocks to be automatically added for the newly created course.
*
* @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
* each of values is an array of block names (for left and right side columns)
*/
public function get_default_blocks() {
- return array(
- BLOCK_POS_LEFT => array(),
- BLOCK_POS_RIGHT => array()
- );
+ return [
+ BLOCK_POS_LEFT => [],
+ BLOCK_POS_RIGHT => [],
+ ];
}
/**
- * Definitions of the additional options that this course format uses for course
+ * Definitions of the additional options that this course format uses for course.
*
* Topics format uses the following options:
* - coursedisplay
static $courseformatoptions = false;
if ($courseformatoptions === false) {
$courseconfig = get_config('moodlecourse');
- $courseformatoptions = array(
- 'hiddensections' => array(
+ $courseformatoptions = [
+ 'hiddensections' => [
'default' => $courseconfig->hiddensections,
'type' => PARAM_INT,
- ),
- 'coursedisplay' => array(
+ ],
+ 'coursedisplay' => [
'default' => $courseconfig->coursedisplay,
'type' => PARAM_INT,
- ),
- );
+ ],
+ ];
}
if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) {
- $courseformatoptionsedit = array(
- 'hiddensections' => array(
+ $courseformatoptionsedit = [
+ 'hiddensections' => [
'label' => new lang_string('hiddensections'),
'help' => 'hiddensections',
'help_component' => 'moodle',
'element_type' => 'select',
- 'element_attributes' => array(
- array(
+ 'element_attributes' => [
+ [
0 => new lang_string('hiddensectionscollapsed'),
1 => new lang_string('hiddensectionsinvisible')
- )
- ),
- ),
- 'coursedisplay' => array(
+ ],
+ ],
+ ],
+ 'coursedisplay' => [
'label' => new lang_string('coursedisplay'),
'element_type' => 'select',
- 'element_attributes' => array(
- array(
+ 'element_attributes' => [
+ [
COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'),
- COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi')
- )
- ),
+ COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi'),
+ ],
+ ],
'help' => 'coursedisplay',
'help_component' => 'moodle',
- )
- );
+ ],
+ ];
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
}
return $courseformatoptions;
}
/**
- * Updates format options for a course
+ * Updates format options for a course.
*
* In case if course format was changed to 'topics', we try to copy options
* 'coursedisplay' and 'hiddensections' from the previous format.
}
/**
- * Whether this format allows to delete sections
+ * Whether this format allows to delete sections.
*
* Do not call this function directly, instead use {@link course_can_delete_section()}
*
}
/**
- * Prepares the templateable object to display section name
+ * Prepares the templateable object to display section name.
*
* @param \section_info|\stdClass $section
* @param bool $linkifneeded
* @param bool $editable
* @param null|lang_string|string $edithint
* @param null|lang_string|string $editlabel
- * @return \core\output\inplace_editable
+ * @return inplace_editable
*/
public function inplace_editable_render_section_name($section, $linkifneeded = true,
- $editable = null, $edithint = null, $editlabel = null) {
+ $editable = null, $edithint = null, $editlabel = null) {
if (empty($edithint)) {
$edithint = new lang_string('editsectionname', 'format_topics');
}
return !$section->section || $section->visible;
}
+ /**
+ * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide).
+ *
+ * Access to the course is already validated in the WS but the callback has to make sure
+ * that particular action is allowed by checking capabilities
+ *
+ * Course formats should register.
+ *
+ * @param section_info|stdClass $section
+ * @param string $action
+ * @param int $sr
+ * @return null|array any data for the Javascript post-processor (must be json-encodeable)
+ */
public function section_action($section, $action, $sr) {
global $PAGE;
}
/**
- * Implements callback inplace_editable() allowing to edit values in-place
+ * Implements callback inplace_editable() allowing to edit values in-place.
*
* @param string $itemtype
* @param int $itemid
* @param mixed $newvalue
- * @return \core\output\inplace_editable
+ * @return inplace_editable
*/
function format_topics_inplace_editable($itemtype, $itemid, $newvalue) {
global $DB, $CFG;
if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
$section = $DB->get_record_sql(
'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?',
- array($itemid, 'topics'), MUST_EXIST);
+ [$itemid, 'topics'], MUST_EXIST);
return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue);
}
}
* @since Moodle 2.3
*/
-
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/course/format/renderer.php');
class format_topics_renderer extends format_section_renderer_base {
/**
- * Constructor method, calls the parent constructor
+ * Constructor method, calls the parent constructor.
*
* @param moodle_page $page
* @param string $target one of rendering target constants
public function __construct(moodle_page $page, $target) {
parent::__construct($page, $target);
- // Since format_topics_renderer::section_edit_control_items() only displays the 'Highlight' control when editing mode is on
- // we need to be sure that the link 'Turn editing mode on' is available for a user who does not have any other managing capability.
+ // Since format_topics_renderer::section_edit_control_items() only displays the 'Highlight' control
+ // when editing mode is on we need to be sure that the link 'Turn editing mode on' is available for a user
+ // who does not have any other managing capability.
$page->set_other_editing_capability('moodle/course:setcurrentsection');
}
/**
- * Generate the starting container html for a list of sections
+ * Generate the starting container html for a list of sections.
+ *
* @return string HTML to output.
*/
protected function start_section_list() {
- return html_writer::start_tag('ul', array('class' => 'topics'));
+ return html_writer::start_tag('ul', ['class' => 'topics']);
}
/**
- * Generate the closing container html for a list of sections
+ * Generate the closing container html for a list of sections.
+ *
* @return string HTML to output.
*/
protected function end_section_list() {
}
/**
- * Generate the title for this section page
+ * Generate the title for this section page.
+ *
* @return string the page title
*/
protected function page_title() {
}
/**
- * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page
+ * Generate the section title, wraps it in a link to the section page if page is to be displayed on a separate page.
*
- * @param stdClass $section The course_section entry from DB
+ * @param section_info|stdClass $section The course_section entry from DB
* @param stdClass $course The course entry from DB
* @return string HTML to output.
*/
}
/**
- * Generate the section title to be displayed on the section page, without a link
+ * Generate the section title to be displayed on the section page, without a link.
*
- * @param stdClass $section The course_section entry from DB
- * @param stdClass $course The course entry from DB
+ * @param section_info|stdClass $section The course_section entry from DB
+ * @param int|stdClass $course The course entry from DB
* @return string HTML to output.
*/
public function section_title_without_link($section, $course) {
}
/**
- * Generate the edit control items of a section
+ * Generate the edit control items of a section.
*
- * @param stdClass $course The course entry from DB
- * @param stdClass $section The course_section entry from DB
+ * @param int|stdClass $course The course entry from DB
+ * @param section_info|stdClass $section The course_section entry from DB
* @param bool $onsectionpage true if being printed on a section page
* @return array of edit control items
*/
protected function section_edit_control_items($course, $section, $onsectionpage = false) {
if (!$this->page->user_is_editing()) {
- return array();
+ return [];
}
$coursecontext = context_course::instance($course->id);
}
$url->param('sesskey', sesskey());
- $controls = array();
+ $controls = [];
if ($section->section && has_capability('moodle/course:setcurrentsection', $coursecontext)) {
if ($course->marker == $section->section) { // Show the "light globe" on/off.
$url->param('marker', 0);
$highlightoff = get_string('highlightoff');
- $controls['highlight'] = array('url' => $url, "icon" => 'i/marked',
- 'name' => $highlightoff,
- 'pixattr' => array('class' => ''),
- 'attr' => array('class' => 'editing_highlight',
- 'data-action' => 'removemarker'));
+ $controls['highlight'] = [
+ 'url' => $url,
+ 'icon' => 'i/marked',
+ 'name' => $highlightoff,
+ 'pixattr' => ['class' => ''],
+ 'attr' => [
+ 'class' => 'editing_highlight',
+ 'data-action' => 'removemarker'
+ ],
+ ];
} else {
$url->param('marker', $section->section);
$highlight = get_string('highlight');
- $controls['highlight'] = array('url' => $url, "icon" => 'i/marker',
- 'name' => $highlight,
- 'pixattr' => array('class' => ''),
- 'attr' => array('class' => 'editing_highlight',
- 'data-action' => 'setmarker'));
+ $controls['highlight'] = [
+ 'url' => $url,
+ 'icon' => 'i/marker',
+ 'name' => $highlight,
+ 'pixattr' => ['class' => ''],
+ 'attr' => [
+ 'class' => 'editing_highlight',
+ 'data-action' => 'setmarker'
+ ],
+ ];
}
}
// If the edit key exists, we are going to insert our controls after it.
if (array_key_exists("edit", $parentcontrols)) {
- $merged = array();
+ $merged = [];
// We can't use splice because we are using associative arrays.
// Step through the array and merge the arrays.
foreach ($parentcontrols as $key => $action) {
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * format_topics related unit tests
+ * Topics course format related unit tests.
*
* @package format_topics
* @copyright 2015 Marina Glancy
require_once($CFG->dirroot . '/course/lib.php');
/**
- * format_topics related unit tests
+ * Topics course format related unit tests.
*
* @package format_topics
* @copyright 2015 Marina Glancy
/**
* Tests for format_topics::get_section_name method with default section names.
+ *
+ * @return void
*/
public function test_get_section_name() {
global $DB;
// Generate a course with 5 sections.
$generator = $this->getDataGenerator();
$numsections = 5;
- $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
- array('createsections' => true));
+ $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'],
+ ['createsections' => true]);
// Get section names for course.
- $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $coursesections = $DB->get_records('course_sections', ['course' => $course->id]);
// Test get_section_name with default section names.
$courseformat = course_get_format($course);
/**
* Tests for format_topics::get_section_name method with modified section names.
+ *
+ * @return void
*/
public function test_get_section_name_customised() {
global $DB;
// Generate a course with 5 sections.
$generator = $this->getDataGenerator();
$numsections = 5;
- $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
- array('createsections' => true));
+ $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'],
+ ['createsections' => true]);
// Get section names for course.
- $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $coursesections = $DB->get_records('course_sections', ['course' => $course->id]);
// Modify section names.
$customname = "Custom Section";
}
// Requery updated section names then test get_section_name.
- $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $coursesections = $DB->get_records('course_sections', ['course' => $course->id]);
$courseformat = course_get_format($course);
foreach ($coursesections as $section) {
// Assert that with modified section names, get_section_name returns the modified section name.
/**
* Tests for format_topics::get_default_section_name.
+ *
+ * @return void
*/
public function test_get_default_section_name() {
global $DB;
// Generate a course with 5 sections.
$generator = $this->getDataGenerator();
$numsections = 5;
- $course = $generator->create_course(array('numsections' => $numsections, 'format' => 'topics'),
- array('createsections' => true));
+ $course = $generator->create_course(['numsections' => $numsections, 'format' => 'topics'],
+ ['createsections' => true]);
// Get section names for course.
- $coursesections = $DB->get_records('course_sections', array('course' => $course->id));
+ $coursesections = $DB->get_records('course_sections', ['course' => $course->id]);
// Test get_default_section_name with default section names.
$courseformat = course_get_format($course);
}
/**
- * Test web service updating section name
+ * Test web service updating section name.
+ *
+ * @return void
*/
public function test_update_inplace_editable() {
global $CFG, $DB, $PAGE;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
- $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'topics'),
- array('createsections' => true));
- $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+ $course = $this->getDataGenerator()->create_course(['numsections' => 5, 'format' => 'topics'],
+ ['createsections' => true]);
+ $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]);
// Call webservice without necessary permissions.
try {
}
// Change to teacher and make sure that section name can be updated using web service update_inplace_editable().
- $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
$res = core_external::update_inplace_editable('format_topics', 'sectionname', $section->id, 'New section name');
$res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
$this->assertEquals('New section name', $res['value']);
- $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+ $this->assertEquals('New section name', $DB->get_field('course_sections', 'name', ['id' => $section->id]));
}
/**
- * Test callback updating section name
+ * Test callback updating section name.
+ *
+ * @return void
*/
public function test_inplace_editable() {
global $DB, $PAGE;
$this->resetAfterTest();
$user = $this->getDataGenerator()->create_user();
- $course = $this->getDataGenerator()->create_course(array('numsections' => 5, 'format' => 'topics'),
- array('createsections' => true));
- $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $course = $this->getDataGenerator()->create_course(['numsections' => 5, 'format' => 'topics'],
+ ['createsections' => true]);
+ $teacherrole = $DB->get_record('role', ['shortname' => 'editingteacher']);
$this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
$this->setUser($user);
- $section = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 2));
+ $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => 2]);
// Call callback format_topics_inplace_editable() directly.
- $tmpl = component_callback('format_topics', 'inplace_editable', array('sectionname', $section->id, 'Rename me again'));
+ $tmpl = component_callback('format_topics', 'inplace_editable', ['sectionname', $section->id, 'Rename me again']);
$this->assertInstanceOf('core\output\inplace_editable', $tmpl);
$res = $tmpl->export_for_template($PAGE->get_renderer('core'));
$this->assertEquals('Rename me again', $res['value']);
- $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', array('id' => $section->id)));
+ $this->assertEquals('Rename me again', $DB->get_field('course_sections', 'name', ['id' => $section->id]));
// Try updating using callback from mismatching course format.
try {
- $tmpl = component_callback('format_weeks', 'inplace_editable', array('sectionname', $section->id, 'New name'));
+ component_callback('format_weeks', 'inplace_editable', ['sectionname', $section->id, 'New name']);
$this->fail('Exception expected');
} catch (moodle_exception $e) {
$this->assertEquals(1, preg_match('/^Can\'t find data record in database/', $e->getMessage()));
$this->setTimezone('UTC');
- $params = array('format' => 'topics', 'numsections' => 5, 'startdate' => 1445644800);
+ $params = ['format' => 'topics', 'numsections' => 5, 'startdate' => 1445644800];
$course = $this->getDataGenerator()->create_course($params);
- $category = $DB->get_record('course_categories', array('id' => $course->category));
+ $category = $DB->get_record('course_categories', ['id' => $course->category]);
$args = [
'course' => $course,
}
/**
- * Test for get_view_url() to ensure that the url is only given for the correct cases
+ * Test for get_view_url() to ensure that the url is only given for the correct cases.
+ *
+ * @return void
*/
public function test_get_view_url() {
global $CFG;
// Generate a course with two sections (0 and 1) and two modules.
$generator = $this->getDataGenerator();
- $course1 = $generator->create_course(array('format' => 'topics'));
- course_create_sections_if_missing($course1, array(0, 1));
+ $course1 = $generator->create_course(['format' => 'topics']);
+ course_create_sections_if_missing($course1, [0, 1]);
$data = (object)['id' => $course1->id];
$format = course_get_format($course1);
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * Version details
+ * Version details.
*
- * @package format
- * @subpackage topics
+ * @package format_topics
* @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
--- /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/>.
+}}
+{{!
+ @template core_course/local/activitychooser/error
+
+ Chooser error template.
+
+ Variables required for this template:
+ * errormessage - The error message
+
+ Example context (json):
+ {
+ "errormessage": "Error"
+ }
+}}
+<div class="p-2 px-sm-5 py-sm-4">
+ <div class="alert alert-danger" role="alert">
+ <h5 class="alert-heading">
+ <i class="fa fa-exclamation-circle fa-fw text-danger"></i>
+ {{#str}} error, error {{/str}}
+ </h5>
+ <hr>
+ <p class="text-break">{{{errormessage}}}</p>
+ </div>
+</div>
$this->save();
}
- if ($fromform['text']) {
+ if (array_key_exists('text', $fromform)) {
$textoptions = $this->value_editor_options();
$data = (object) ['field_editor' => $fromform];
$data = file_postupdate_standard_editor($data, 'field', $textoptions, $textoptions['context'],
$handler->instance_form_save($data);
}
+ /**
+ * Test that instance form save empties the field content for blank values
+ */
+ public function test_instance_form_save_clear(): void {
+ global $CFG;
+
+ require_once("{$CFG->dirroot}/customfield/tests/fixtures/test_instance_form.php");
+
+ $this->setAdminUser();
+
+ $handler = $this->cfcat->get_handler();
+
+ // Set our custom field to a known value.
+ $submitdata = (array) $this->courses[1] + [
+ 'customfield_myfield1_editor' => ['text' => 'I can see it in your eyes', 'format' => FORMAT_HTML],
+ 'customfield_myfield2_editor' => ['text' => 'I can see it in your smile', 'format' => FORMAT_HTML],
+ ];
+
+ core_customfield_test_instance_form::mock_submit($submitdata, []);
+ $form = new core_customfield_test_instance_form('post', ['handler' => $handler, 'instance' => $this->courses[1]]);
+ $handler->instance_form_save($form->get_data());
+
+ $this->assertEquals($submitdata['customfield_myfield1_editor']['text'],
+ core_customfield\data_controller::create($this->cfdata[1]->get('id'))->export_value());
+
+ // Now empty our non-required field.
+ $submitdata['customfield_myfield1_editor']['text'] = '';
+
+ core_customfield_test_instance_form::mock_submit($submitdata, []);
+ $form = new core_customfield_test_instance_form('post', ['handler' => $handler, 'instance' => $this->courses[1]]);
+ $handler->instance_form_save($form->get_data());
+
+ $this->assertEmpty(core_customfield\data_controller::create($this->cfdata[1]->get('id'))->export_value());
+ }
+
/**
* Test for data_controller::get_value and export_value
*/
case 'useridnumber':
case 'useremail':
case 'username':
- // Skip invalid row with blank user field.
- if (!empty($value)) {
- $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]);
- }
+ $this->studentid = $this->check_user_exists($value, $userfields[$mappingidentifier]);
break;
case 'new':
$this->import_new_grade_item($header, $key, $value);
--- /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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['clianswerno'] = 'n';
+$string['cliansweryes'] = 'j';
+$string['cliincorrectvalueretry'] = 'Falscha Wert, probiers nummal';
+$string['cliyesnoprompt'] = 'druck j (bedeit ja) oda n (bedeit na)';
--- /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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'de';
+$string['thislanguage'] = 'Bairisch';
defined('MOODLE_INTERNAL') || die();
$string['language'] = 'à¤à¤¾à¤·à¤¾';
+$string['moodlelogo'] = 'Moodle लोगो';
$string['next'] = 'अगला';
$string['reload'] = 'सीमा से अधिक लादना';
--- /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/>.
+
+/**
+ * Automatically generated strings for Moodle installer
+ *
+ * Do not edit this file manually! It contains just a subset of strings
+ * needed during the very first steps of installation. This file was
+ * generated automatically by export-installer.php (which is part of AMOS
+ * {@link http://docs.moodle.org/dev/Languages/AMOS}) using the
+ * list of strings defined in /install/stringnames.txt.
+ *
+ * @package installer
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['parentlanguage'] = 'hi';
+$string['thislanguage'] = 'हिंदी';
$string['debugnone'] = 'NONE: Do not show any errors or warnings';
$string['debugnormal'] = 'NORMAL: Show errors, warnings and notices';
$string['debugpageinfo'] = 'Show page information';
+$string['debugsqltrace'] = 'Show origin of SQL calls';
+$string['debugsqltrace1'] = 'Show only a single calling line';
+$string['debugsqltrace2'] = 'Show 2 lines of stack trace';
+$string['debugsqltrace100'] = 'Show full stack trace';
+$string['debugsqltrace_desc'] = 'If enabled adds either partial or full PHP stacktrace into the SQL as a comment';
$string['debugstringids'] = 'Show origin of languages strings';
$string['debugstringids_desc'] = 'If enabled, language string components and identifiers are displayed when ?strings=1 or &strings=1 is appended to the page URL.';
$string['debugvalidators'] = 'Show validator links';
$string['errorupdatinggradeitemaggregationcoef'] = 'Error updating the aggregation coefficient (weight or extra credit) of grade item ID {$a->id}';
$string['eventgradedeleted'] = 'Grade deleted';
$string['eventgradeitemcreated'] = 'Grade item created';
+$string['eventgradeitemdeleted'] = 'Grade item deleted';
$string['eventgradeitemupdated'] = 'Grade item updated';
$string['eventgradelettercreated'] = 'Grade letter created';
$string['eventgradeletterdeleted'] = 'Grade letter deleted';
/** @var string The external URL that we should link to when someone requests this external page. */
public $url;
- /** @var string The role capability/permission a user must have to access this external page. */
+ /** @var array The role capability/permission a user must have to access this external page. */
public $req_capability;
/** @var object The context in which capability/permission should be checked, default is site context. */
/** @var admin_settingdependency[] list of settings to hide when certain conditions are met */
protected $dependencies = [];
- /** @var string The role capability/permission a user must have to access this external page. */
+ /** @var array The role capability/permission a user must have to access this external page. */
public $req_capability;
/** @var object The context in which capability/permission should be checked, default is site context. */
--- /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/>.
+
+/**
+ * Helpers to perform ARIA compliance changes to the DOM.
+ *
+ * @module core/aria
+ * @class aria
+ * @package core
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+export {
+ hide,
+ unhide,
+ hideSiblings,
+ unhideSiblings,
+} from './local/aria/aria-hidden';
*/
import $ from 'jquery';
import * as PubSub from 'core/pubsub';
+import * as Aria from 'core/aria';
import DrawerEvents from 'core/drawer_events';
/**
*
* @param {Object} root The drawer container.
*/
-const show = (root) => {
+const show = root => {
+ // Ensure that it is a jQuery.
+ root = $(root);
+
+ Aria.unhide(root.get());
root.removeClass('hidden');
root.attr('aria-expanded', true);
- root.removeAttr('aria-hidden');
root.focus();
PubSub.publish(DrawerEvents.DRAWER_SHOWN, root);
*
* @param {Object} root The drawer container.
*/
-const hide = (root) => {
+const hide = root => {
+ // Ensure that it is a jQuery.
+ root = $(root);
+
root.addClass('hidden');
root.attr('aria-expanded', false);
- root.attr('aria-hidden', true);
+ Aria.hide(root.get());
+
PubSub.publish(DrawerEvents.DRAWER_HIDDEN, root);
};
*/
/* globals require: false */
define(
- ['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification', 'core/loadingicon'],
-function($, log, str, templates, notification, LoadingIcon) {
+ ['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification', 'core/loadingicon', 'core/aria'],
+function($, log, str, templates, notification, LoadingIcon, Aria) {
// Private functions and variables.
/** @var {Object} KEYS - List of keycode constants. */
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Count the visible items.
- var length = suggestionsElement.children('[aria-hidden=false]').length;
+ var length = suggestionsElement.children(':not([aria-hidden])').length;
// Limit the index to the upper/lower bounds of the list (wrap in both directions).
index = index % length;
while (index < 0) {
index += length;
}
// Find the specified element.
- var element = $(suggestionsElement.children('[aria-hidden=false]').get(index));
+ var element = $(suggestionsElement.children(':not([aria-hidden])').get(index));
// Find the index of this item in the full list of suggestions (including hidden).
var globalIndex = $(suggestionsElement.children('[role=option]')).index(element);
// Create an id we can assign to this element.
// Find the active one.
var element = suggestionsElement.children('[aria-selected=true]');
// Find it's index.
- var current = suggestionsElement.children('[aria-hidden=false]').index(element);
+ var current = suggestionsElement.children(':not([aria-hidden])').index(element);
// Activate the next one.
return activateItem(current + 1, state);
};
var element = suggestionsElement.children('[aria-selected=true]');
// Find it's index.
- var current = suggestionsElement.children('[aria-hidden=false]').index(element);
+ var current = suggestionsElement.children(':not([aria-hidden])').index(element);
// Activate the previous one.
return activateItem(current - 1, state);
inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
// Hide the suggestions list (from screen readers too).
- suggestionsElement.hide().attr('aria-hidden', true);
+ Aria.hide(suggestionsElement.get());
+ suggestionsElement.hide();
return $.Deferred().resolve();
};
// Get the element again.
suggestionsElement = $(document.getElementById(state.suggestionsId));
+
// Show it if it is hidden.
- suggestionsElement.show().attr('aria-hidden', false);
+ Aria.unhide(suggestionsElement.get());
+ suggestionsElement.show();
+
// For each option in the list, hide it if it doesn't match the query.
suggestionsElement.children().each(function(index, node) {
node = $(node);
if ((options.caseSensitive && node.text().indexOf(searchquery) > -1) ||
(!options.caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) {
- node.show().attr('aria-hidden', false);
+ Aria.unhide(node.get());
+ node.show();
matchingElements = true;
} else {
- node.hide().attr('aria-hidden', true);
+ node.hide();
+ Aria.hide(node.get());
}
});
// If we found any matches, show the list.
var element = $(e.currentTarget).closest('[role=option]');
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Find the index of the clicked on suggestion.
- var current = suggestionsElement.children('[aria-hidden=false]').index(element);
+ var current = suggestionsElement.children(':not([aria-hidden])').index(element);
// Activate it.
activateItem(current, state)
return false;
}
- originalSelect.css('visibility', 'hidden').attr('aria-hidden', true);
+ Aria.hide(originalSelect.get());
+ originalSelect.css('visibility', 'hidden');
// Hide the original select.
var suggestionsElement = $(document.getElementById(state.suggestionsId));
// Hide the suggestions by default.
- suggestionsElement.hide().attr('aria-hidden', true);
+ suggestionsElement.hide();
+ Aria.hide(suggestionsElement.get());
return;
})
--- /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/>.
+
+/**
+ * ARIA helpers related to the aria-hidden attribute.
+ *
+ * @module core/local/aria/aria-hidden.
+ * @class aria
+ * @package core
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import {getList} from 'core/normalise';
+import Selectors from './selectors';
+
+// The map of MutationObserver objects for an object.
+const childObserverMap = new Map();
+const siblingObserverMap = new Map();
+
+/**
+ * Determine whether the browser supports the MutationObserver system.
+ *
+ * @returns {Bool}
+ */
+const supportsMutationObservers = () => (MutationObserver && typeof MutationObserver === 'function');
+
+/**
+ * Disable element focusability, disabling the tabindex for child elements which are normally focusable.
+ *
+ * @param {HTMLElement} target
+ */
+const disableElementFocusability = target => {
+ if (!(target instanceof HTMLElement)) {
+ // This element is not an HTMLElement.
+ // This can happen for Text Nodes.
+ return;
+ }
+
+ if (target.matches(Selectors.elements.focusable)) {
+ disableAndStoreTabIndex(target);
+ }
+
+ target.querySelectorAll(Selectors.elements.focusable).forEach(disableAndStoreTabIndex);
+};
+
+/**
+ * Remove the current tab-index and store it for later restoration.
+ *
+ * @param {HTMLElement} element
+ */
+const disableAndStoreTabIndex = element => {
+ if (typeof element.dataset.ariaHiddenTabIndex !== 'undefined') {
+ // This child already has a hidden attribute.
+ // Do not modify it as the original value will be lost.
+ return;
+ }
+
+ // Store the old tabindex in a data attribute.
+ if (element.getAttribute('tabindex')) {
+ element.dataset.ariaHiddenTabIndex = element.getAttribute('tabindex');
+ } else {
+ element.dataset.ariaHiddenTabIndex = '';
+ }
+ element.setAttribute('tabindex', -1);
+};
+
+/**
+ * Re-enable element focusability, restoring any tabindex.
+ *
+ * @param {HTMLElement} target
+ */
+const enableElementFocusability = target => {
+ if (!(target instanceof HTMLElement)) {
+ // This element is not an HTMLElement.
+ // This can happen for Text Nodes.
+ return;
+ }
+
+ if (target.matches(Selectors.elements.focusableToUnhide)) {
+ restoreTabIndex(target);
+ }
+
+ target.querySelectorAll(Selectors.elements.focusableToUnhide).forEach(restoreTabIndex);
+};
+
+/**
+ * Restore the tab-index of the supplied element.
+ *
+ * When disabling focusability the current tab-index is stored in the ariaHiddenTabIndex data attribute.
+ * This is used to restore the tab-index, but only whilst the parent nodes remain unhidden.
+ *
+ * @param {HTMLElement} element
+ */
+const restoreTabIndex = element => {
+ if (element.closest(Selectors.aria.hidden)) {
+ // This item still has a hidden parent, or is hidden itself. Do not unhide it.
+ return;
+ }
+
+ const oldTabIndex = element.dataset.ariaHiddenTabIndex;
+ if (oldTabIndex === '') {
+ element.removeAttribute('tabindex');
+ } else {
+ element.setAttribute('tabindex', oldTabIndex);
+ }
+
+ delete element.dataset.ariaHiddenTabIndex;
+};
+
+/**
+ * Update the supplied DOM Module to be hidden.
+ *
+ * @param {HTMLElement} target
+ * @returns {Array}
+ */
+export const hide = target => getList(target).forEach(_hide);
+
+const _hide = target => {
+ if (!(target instanceof HTMLElement)) {
+ // This element is not an HTMLElement.
+ // This can happen for Text Nodes.
+ return;
+ }
+
+ if (target.closest(Selectors.aria.hidden)) {
+ // This Element, or a parent Element, is already hidden.
+ // Stop processing.
+ return;
+ }
+
+ // Set the aria-hidden attribute to true.
+ target.setAttribute('aria-hidden', true);
+
+ // Based on advice from https://dequeuniversity.com/rules/axe/3.3/aria-hidden-focus, upon setting the aria-hidden
+ // attribute, all focusable elements underneath that element should be modified such that they are not focusable.
+ disableElementFocusability(target);
+
+ if (supportsMutationObservers()) {
+ // Add a MutationObserver to check for new children to the tree.
+ const newNodeObserver = new MutationObserver(mutationList => {
+ mutationList.forEach(mutation => {
+ mutation.addedNodes.forEach(disableElementFocusability);
+ });
+ });
+
+ newNodeObserver.observe(target, {childList: true, subtree: true});
+ childObserverMap.set(target, newNodeObserver);
+ }
+};
+
+/**
+ * Reverse the effect of the hide action.
+ *
+ * @param {HTMLElement} target
+ * @returns {Array}
+ */
+export const unhide = target => getList(target).forEach(_unhide);
+
+const _unhide = target => {
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ // Note: The aria-hidden attribute should be removed, and not set to false.
+ // The presence of the attribute is sufficient for some browsers to treat it as being true, regardless of its value.
+ target.removeAttribute('aria-hidden');
+
+ // Restore the tabindex across all child nodes of the target.
+ enableElementFocusability(target);
+
+ // Remove the focusability MutationObserver watching this tree.
+ if (childObserverMap.has(target)) {
+ childObserverMap.get(target).disconnect();
+ childObserverMap.delete(target);
+ }
+};
+
+/**
+ * Correctly mark all siblings of the supplied target Element as hidden.
+ *
+ * @param {HTMLElement} target
+ * @returns {Array}
+ */
+export const hideSiblings = target => getList(target).forEach(_hideSiblings);
+
+const _hideSiblings = target => {
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ if (!target.parentElement) {
+ return;
+ }
+
+ target.parentElement.childNodes.forEach(node => {
+ if (node === target) {
+ // Skip self;
+ return;
+ }
+
+ hide(node);
+ });
+
+ if (supportsMutationObservers()) {
+ // Add a MutationObserver to check for new children to the tree.
+ const newNodeObserver = new MutationObserver(mutationList => {
+ mutationList.forEach(mutation => {
+ mutation.addedNodes.forEach(node => {
+ if (target.contains(node)) {
+ // Skip self, and children of self.
+ return;
+ }
+
+ hide(node);
+ });
+ });
+ });
+
+ newNodeObserver.observe(target.parentElement, {childList: true, subtree: true});
+ siblingObserverMap.set(target.parentElement, newNodeObserver);
+ }
+};
+
+/**
+ * Correctly reverse the hide action of all children of the supplied target Element.
+ *
+ * @param {HTMLElement} target
+ * @returns {Array}
+ */
+export const unhideSiblings = target => getList(target).forEach(_unhideSiblings);
+
+const _unhideSiblings = target => {
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ if (!target.parentElement) {
+ return;
+ }
+
+ target.parentElement.childNodes.forEach(node => {
+ if (node === target) {
+ // Skip self;
+ return;
+ }
+
+ unhide(node);
+ });
+
+ // Remove the sibling MutationObserver watching this tree.
+ if (siblingObserverMap.has(target.parentElement)) {
+ siblingObserverMap.get(target.parentElement).disconnect();
+ siblingObserverMap.delete(target.parentElement);
+ }
+};
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-const selectors = {
- focusable: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
-};
+import Selectors from './selectors';
const lockRegionStack = [];
const initialFocusElementStack = [];
// For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector
// to capture this.
// The use of Array.some just ensures that we stop as soon as we have a successful focus.
- const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable));
+ const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable));
// The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.
// We must include it in the calculation of descendants to ensure that looping works correctly.
// For example, a disabled text area cannot be focused, and it becomes difficult to provide a decent query selector
// to capture this.
// The use of Array.some just ensures that we stop as soon as we have a successful focus.
- const focusableElements = Array.from(lockRegion.querySelectorAll(selectors.focusable)).reverse();
+ const focusableElements = Array.from(lockRegion.querySelectorAll(Selectors.elements.focusable)).reverse();
// The lock region itself may be focusable. This is particularly true on Moodle's older dialogues.
// We must include it in the calculation of descendants to ensure that looping works correctly.
--- /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/>.
+
+/**
+ * Selectors used for ARIA.
+ *
+ * @module core/local/aria/selectors
+ * @class selectors
+ * @package core
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+export default {
+ aria: {
+ hidden: '[aria-hidden]',
+ },
+ elements: {
+ focusable: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
+ focusableToUnhide: '[data-aria-hidden-tab-index]',
+ },
+};
'core/modal_events',
'core/local/aria/focuslock',
'core/pending',
-], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending) {
+ 'core/aria',
+], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending, Aria) {
var SELECTORS = {
CONTAINER: '[data-region="modal-container"]',
* @method accessibilityShow
*/
Modal.prototype.accessibilityShow = function() {
- // We need to get a list containing each sibling element and the shallowest
- // non-ancestral nodes in the DOM. We can shortcut this a little by leveraging
- // the fact that this dialogue is always appended to the document body therefore
- // it's siblings are the shallowest non-ancestral nodes. If that changes then
- // this code should also be updated.
- $('body').children().each(function(index, child) {
- // Skip the current modal.
- if (!this.root.is(child)) {
- child = $(child);
- var hidden = child.attr('aria-hidden');
- // If they are already hidden we can ignore them.
- if (hidden !== 'true') {
- // Save their current state.
- child.data('previous-aria-hidden', hidden);
- this.hiddenSiblings.push(child);
-
- // Hide this node from screen readers.
- child.attr('aria-hidden', 'true');
- }
- }
- }.bind(this));
-
// Make us visible to screen readers.
- this.root.attr('aria-hidden', 'false');
+ Aria.unhide(this.root.get());
+
+ // Hide siblings.
+ Aria.hideSiblings(this.root.get()[0]);
};
/**
* @method accessibilityHide
*/
Modal.prototype.accessibilityHide = function() {
- this.root.attr('aria-hidden', 'true');
-
- // Restore the sibling nodes back to their original values.
- $.each(this.hiddenSiblings, function(index, sibling) {
- sibling = $(sibling);
- var previousValue = sibling.data('previous-aria-hidden');
- // If the element didn't previously have an aria-hidden attribute
- // then we can just remove the one we set.
- if (typeof previousValue == 'undefined') {
- sibling.removeAttr('aria-hidden');
- } else {
- // Otherwise set it back to the old value (which will be false).
- sibling.attr('aria-hidden', previousValue);
- }
- });
+ // Unhide siblings.
+ Aria.unhideSiblings(this.root.get()[0]);
- // Clear the cache. No longer need to store these.
- this.hiddenSiblings = [];
+ // Hide this modal.
+ Aria.hide(this.root.get());
};
/**
--- /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/>.
+
+/**
+ * Normalisation helpers.
+ *
+ * @module core/normalise
+ * @class normalise
+ * @package core
+ * @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import jQuery from 'jquery';
+
+export const getList = nodes => {
+ if (nodes instanceof HTMLElement) {
+ // A single record to conver to a NodeList.
+ return [nodes];
+ }
+
+ if (nodes instanceof Array) {
+ // A single record to conver to a NodeList.
+ return nodes;
+ }
+
+ if (nodes instanceof NodeList) {
+ // Already a NodeList.
+ return Array.from(nodes);
+ }
+
+ if (nodes instanceof jQuery) {
+ // A jQuery object to a NodeList.
+ return nodes.get();
+ }
+
+ // Fallback to just having a go.
+ return Array.from(nodes);
+};
-define(['jquery'], function($) {
+define(['jquery', 'core/aria'], function($, Aria) {
/**
* Tooltip class.
var tooltipele = $(document.getElementById(tooltipId));
tooltipele.show();
- tooltipele.attr('aria-hidden', 'false');
+ Aria.unhide(tooltipele);
if (!tooltipele.is('.tooltip')) {
// Change the markup to a bootstrap tooltip.
var tooltipele = document.getElementById(tooltipId);
$(tooltipele).hide();
- $(tooltipele).attr('aria-hidden', 'true');
+ Aria.hide(tooltipele);
}
};
Tree.prototype.finishExpandingGroup = function(item) {
// Expand the group.
var group = this.getGroupFromItem(item);
- group.attr('aria-hidden', 'false');
+ group.removeAttr('aria-hidden');
item.attr('aria-expanded', 'true');
// Update the list of visible items.
}
/**
- * Returns whether the scenario is running in a browser that can run Javascript or not.
+ * Whether Javascript is available in the current Session.
*
* @return boolean
*/
protected function running_javascript() {
- return get_class($this->getSession()->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
+ return self::running_javascript_in_session($this->getSession());
+ }
+
+ /**
+ * Require that javascript be available in the current Session.
+ *
+ * @throws DriverException
+ */
+ protected function require_javascript() {
+ return self::require_javascript_in_session($this->getSession());
+ }
+
+ /**
+ * Whether Javascript is available in the specified Session.
+ *
+ * @param Session $session
+ * @return boolean
+ */
+ protected static function running_javascript_in_session(Session $session): bool {
+ return get_class($session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver';
+ }
+
+ /**
+ * Require that javascript be available for the specified Session.
+ *
+ * @param Session $session
+ * @throws DriverException
+ */
+ protected static function require_javascript_in_session(Session $session): void {
+ if (self::running_javascript_in_session($session)) {
+ return;
+ }
+
+ throw new DriverException('Javascript is required');
}
/**
}
// Check on page to see if it's an app page. Safest way is to look for added JavaScript.
- return $this->getSession()->evaluateScript('typeof window.behat') === 'object';
+ return $this->evaluate_script('return typeof window.behat') === 'object';
}
/**
// The window inner height will be as specified, which means the available viewport will
// actually be smaller if there is a horizontal scrollbar. We assume that horizontal
// scrollbars are rare so this doesn't matter.
- $offset = $this->getSession()->getDriver()->evaluateScript(
- 'return (function() { var before = document.body.style.overflowY;' .
- 'document.body.style.overflowY = "scroll";' .
- 'var result = {};' .
- 'result.x = window.outerWidth - document.body.offsetWidth;' .
- 'result.y = window.outerHeight - window.innerHeight;' .
- 'document.body.style.overflowY = before;' .
- 'return result; })();');
+ $js = <<<EOF
+return (function() {
+ var before = document.body.style.overflowY;
+ document.body.style.overflowY = "scroll";
+ var result = {};
+ result.x = window.outerWidth - document.body.offsetWidth;
+ result.y = window.outerHeight - window.innerHeight;
+ document.body.style.overflowY = before;
+ return result;
+})();
+EOF;
+ $offset = $this->evaluate_script($js);
$width += $offset['x'];
$height += $offset['y'];
}
} else {
return "incomplete"
}
- }());'));
- $pending = $session->evaluateScript($jscode);
+ })()'));
+ $pending = self::evaluate_script_in_session($session, $jscode);
} catch (NoSuchWindow $nsw) {
// We catch an exception here, in case we just closed the window we were interacting with.
// No javascript is running if there is no window right?
public static function get_named_replacements(): array {
return [];
}
+
+ /**
+ * Evaluate the supplied script in the current session, returning the result.
+ *
+ * @param string $script
+ * @return mixed
+ */
+ public function evaluate_script(string $script) {
+ return self::evaluate_script_in_session($this->getSession(), $script);
+ }
+
+ /**
+ * Evaluate the supplied script in the specified session, returning the result.
+ *
+ * @param Session $session
+ * @param string $script
+ * @return mixed
+ */
+ public static function evaluate_script_in_session(Session $session, string $script) {
+ self::require_javascript_in_session($session);
+
+ return $session->evaluateScript($script);
+ }
+
+ /**
+ * Execute the supplied script in the current session.
+ *
+ * No result will be returned.
+ *
+ * @param string $script
+ */
+ public function execute_script(string $script): void {
+ self::execute_script_in_session($this->getSession(), $script);
+ }
+
+ /**
+ * Excecute the supplied script in the specified session.
+ *
+ * No result will be returned.
+ *
+ * @param Session $session
+ * @param string $script
+ */
+ public static function execute_script_in_session(Session $session, string $script): void {
+ self::require_javascript_in_session($session);
+
+ $session->executeScript($script);
+ }
+
+ /**
+ * Get the session key for the current session via Javascript.
+ *
+ * @return string
+ */
+ public function get_sesskey(): string {
+ $script = <<<EOF
+return (function() {
+if (M && M.cfg && M.cfg.sesskey) {
+ return M.cfg.sesskey;
+}
+return '';
+})()
+EOF;
+
+ return $this->evaluate_script($script);
+ }
}
if ($this->running_javascript()) {
$value = addslashes($value);
$js = '
-var editor = Y.one(document.getElementById("'.$editorid.'editable"));
-if (editor) {
- editor.setHTML("' . $value . '");
-}
-editor = Y.one(document.getElementById("'.$editorid.'"));
-editor.set("value", "' . $value . '");
+(function() {
+ var editor = Y.one(document.getElementById("'.$editorid.'editable"));
+ if (editor) {
+ editor.setHTML("' . $value . '");
+ }
+ editor = Y.one(document.getElementById("'.$editorid.'"));
+ editor.set("value", "' . $value . '");
+})();
';
- $this->session->executeScript($js);
+ behat_base::execute_script_in_session($this->session, $js);
} else {
parent::set_value($value);
}
r.selectNodeContents(e);
s.setSingleRange(r);
}()); ';
- $this->session->executeScript($js);
+ behat_base::execute_script_in_session($this->session, $js);
}
/**
if ($this->running_javascript()) {
$id = $this->field->getAttribute('id');
$js = <<<JS
-require(["jquery"], function($) {
- var wrapper = $(document.getElementById("{$id}")).closest('[data-passwordunmask="wrapper"]');
- wrapper.find('[data-passwordunmask="edit"]').trigger("click");
-});
+(function() {
+ require(["jquery"], function($) {
+ var wrapper = $(document.getElementById("{$id}")).closest('[data-passwordunmask="wrapper"]');
+ wrapper.find('[data-passwordunmask="edit"]').trigger("click");
+ });
+})();
JS;
- $this->session->executeScript($js);
+ behat_base::execute_script_in_session($this->session, $js);
}
$this->field->setValue($value);
--- /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/>.
+
+/**
+ * Grade item deleted event.
+ *
+ * @package core
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Grade item deleted event class.
+ *
+ * @package core
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class grade_item_deleted extends grade_item_created {
+
+ /**
+ * Init method.
+ *
+ * @return void
+ */
+ protected function init() {
+ $this->data['objecttable'] = 'grade_items';
+ $this->data['crud'] = 'd';
+ $this->data['edulevel'] = self::LEVEL_OTHER;
+ }
+
+ /**
+ * Return localised event name.
+ *
+ * @return string
+ */
+ public static function get_name() {
+ return get_string('eventgradeitemdeleted', 'core_grades');
+ }
+
+ /**
+ * Returns description of what happened.
+ *
+ * @return string
+ */
+ public function get_description() {
+ return "The user with id '" . $this->userid . "' deleted a grade item with id '" . $this->objectid . "'" .
+ " of type '" . $this->other['itemtype'] . "' and name '" . $this->other['itemname'] . "'" .
+ " from the course with the id '" . $this->courseid . "'.";
+ }
+
+}
+++ /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/>.
-
-/**
- * Deprecated file, classes moved to autoloaded locations
- *
- * @package core
- * @subpackage course
- * @copyright 2013 Marina Glancy
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-defined('MOODLE_INTERNAL') || die();
-
-debugging('Class coursecat is now alias to autoloaded class core_course_category, ' .
- 'course_in_list is an alias to core_course_list_element. '.
- 'Class coursecat_sortable_records is deprecated without replacement. Do not include coursecatlib.php',
- DEBUG_DEVELOPER);
-
-/**
- * An array of records that is sortable by many fields.
- *
- * For more info on the ArrayObject class have a look at php.net.
- *
- * @package core
- * @subpackage course
- * @copyright 2013 Sam Hemelryk
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class coursecat_sortable_records extends ArrayObject {
-
- /**
- * An array of sortable fields.
- * Gets set temporarily when sort is called.
- * @var array
- */
- protected $sortfields = array();
-
- /**
- * Sorts this array using the given fields.
- *
- * @param array $records
- * @param array $fields
- * @return array
- */
- public static function sort(array $records, array $fields) {
- $records = new coursecat_sortable_records($records);
- $records->sortfields = $fields;
- $records->uasort(array($records, 'sort_by_many_fields'));
- return $records->getArrayCopy();
- }
-
- /**
- * Sorts the two records based upon many fields.
- *
- * This method should not be called itself, please call $sort instead.
- * It has been marked as access private as such.
- *
- * @access private
- * @param stdClass $a
- * @param stdClass $b
- * @return int
- */
- public function sort_by_many_fields($a, $b) {
- foreach ($this->sortfields as $field => $mult) {
- // Nulls first.
- if (is_null($a->$field) && !is_null($b->$field)) {
- return -$mult;
- }
- if (is_null($b->$field) && !is_null($a->$field)) {
- return $mult;
- }
-
- if (is_string($a->$field) || is_string($b->$field)) {
- // String fields.
- if ($cmp = strcoll($a->$field, $b->$field)) {
- return $mult * $cmp;
- }
- } else {
- // Int fields.
- if ($a->$field > $b->$field) {
- return $mult;
- }
- if ($a->$field < $b->$field) {
- return -$mult;
- }
- }
- }
- return 0;
- }
-}
upgrade_main_savepoint(true, 2020071100.01);
}
+ if ($oldversion < 2020072300.01) {
+ // Restore and set the guest user if it has been previously removed via GDPR, or set to an nonexistent
+ // user account.
+ $currentguestuser = $DB->get_record('user', array('id' => $CFG->siteguest));
+
+ if (!$currentguestuser) {
+ if (!$guest = $DB->get_record('user', array('username' => 'guest', 'mnethostid' => $CFG->mnet_localhost_id))) {
+ // Create a guest user account.
+ $guest = new stdClass();
+ $guest->auth = 'manual';
+ $guest->username = 'guest';
+ $guest->password = hash_internal_user_password('guest');
+ $guest->firstname = get_string('guestuser');
+ $guest->lastname = ' ';
+ $guest->email = 'root@localhost';
+ $guest->description = get_string('guestuserinfo');
+ $guest->mnethostid = $CFG->mnet_localhost_id;
+ $guest->confirmed = 1;
+ $guest->lang = $CFG->lang;
+ $guest->timemodified= time();
+ $guest->id = $DB->insert_record('user', $guest);
+ }
+ // Set the guest user.
+ set_config('siteguest', $guest->id);
+ }
+
+ // Main savepoint reached.
+ upgrade_main_savepoint(true, 2020072300.01);
+ }
+
return true;
}
// convert table names
$sql = $this->fix_table_names($sql);
+ // Optionally add debug trace to sql as a comment.
+ $sql = $this->add_sql_debugging($sql);
+
// cast booleans to 1/0 int and detect forbidden objects
foreach ($params as $key => $value) {
$this->detect_objects($value);
}
}
+ /**
+ * Add an SQL comment to trace all sql calls back to the calling php code
+ * @param string $sql Original sql
+ * @return string Instrumented sql
+ */
+ protected function add_sql_debugging(string $sql): string {
+ global $CFG;
+
+ if (!property_exists($CFG, 'debugsqltrace')) {
+ return $sql;
+ }
+
+ $level = $CFG->debugsqltrace;
+
+ if (empty($level)) {
+ return $sql;
+ }
+
+ $callers = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
+
+ // Ignore moodle_database internals.
+ $callers = array_filter($callers, function($caller) {
+ return empty($caller['class']) || $caller['class'] != 'moodle_database';
+ });
+
+ $callers = array_slice($callers, 0, $level);
+
+ $text = trim(format_backtrace($callers, true));
+
+ // Convert all linebreaks to SQL comments, optionally
+ // also eating any * formatting.
+ $text = preg_replace("/(^|\n)\*?\s*/", "\n-- ", $text);
+
+ // Convert all ? to 'unknown' in the sql coment so these don't get
+ // caught by fix_sql_params().
+ $text = str_replace('?', 'unknown', $text);
+
+ // Convert tokens like :test to ::test for the same reason.
+ $text = preg_replace('/(?<!:):[a-z][a-z0-9_]*/', ':\0', $text);
+
+ return $sql . $text;
+ }
+
+
/**
* Ensures that limit params are numeric and positive integers, to be passed to the database.
* We explicitly treat null, '' and -1 as 0 in order to provide compatibility with how limit
$this->assertSame(array_values($params), array_values($inparams));
}
+ /**
+ * Test the database debugging as SQL comment.
+ */
+ public function test_add_sql_debugging() {
+ global $CFG;
+ $DB = $this->tdb;
+
+ require_once($CFG->dirroot . '/lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php');
+ $fixture = new test_dml_sql_debugging_fixture($this);
+
+ $sql = "SELECT * FROM {users}";
+
+ $out = $fixture->four($sql);
+
+ $CFG->debugsqltrace = 0;
+ $this->assertEquals("SELECT * FROM {users}", $out);
+
+ $CFG->debugsqltrace = 1;
+ $out = $fixture->four($sql);
+ $expected = <<<EOD
+SELECT * FROM {users}
+-- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
+EOD;
+ $this->assertEquals($expected, $out);
+
+ $CFG->debugsqltrace = 2;
+ $out = $fixture->four($sql);
+ $expected = <<<EOD
+SELECT * FROM {users}
+-- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
+-- line 74 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->one()
+EOD;
+ $this->assertEquals($expected, $out);
+
+ $CFG->debugsqltrace = 5;
+ $out = $fixture->four($sql);
+ $expected = <<<EOD
+SELECT * FROM {users}
+-- line 65 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to ReflectionMethod->invoke()
+-- line 74 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->one()
+-- line 83 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->two()
+-- line 92 of /lib/dml/tests/fixtures/test_dml_sql_debugging_fixture.php: call to test_dml_sql_debugging_fixture->three()
+-- line 476 of /lib/dml/tests/dml_test.php: call to test_dml_sql_debugging_fixture->four()
+EOD;
+ $this->assertEquals($expected, $out);
+
+ $CFG->debugsqltrace = 0;
+ }
+
public function test_strtok() {
// Strtok was previously used by bound emulation, make sure it is not used any more.
$DB = $this->tdb;
--- /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/>.
+
+/**
+ * Test SQL debugging fixture
+ *
+ * @package core
+ * @category dml
+ * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Test SQL debugging fixture
+ *
+ * @package core
+ * @category dml
+ * @copyright 2020 Brendan Heywood <brendan@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class test_dml_sql_debugging_fixture {
+ /** @var db handle */
+ private $db;
+
+ /**
+ * constructor
+ * @param testcase $testcase test object
+ */
+ public function __construct($testcase) {
+ $this->db = $testcase->getMockBuilder(\moodle_database::class)
+ ->getMockForAbstractClass();
+ }
+
+ /**
+ * Get db handle
+ * @return a db handle
+ */
+ public function get_mock() {
+ return $this->db;
+ }
+
+ /**
+ * Test caller in stacktrace
+ * @param string $sql original sql
+ * @return string sql with comments
+ */
+ public function one(string $sql) {
+ $method = new \ReflectionMethod($this->db, 'add_sql_debugging');
+ $method->setAccessible(true);
+ return $method->invoke($this->db, $sql);
+ }
+
+ /**
+ * Test caller in stacktrace
+ * @param string $sql original sql
+ * @return string sql with comments
+ */
+ public function two(string $sql) {
+ return $this->one($sql);
+ }
+
+ /**
+ * Test caller in stacktrace
+ * @param string $sql original sql
+ * @return string sql with comments
+ */
+ public function three(string $sql) {
+ return $this->two($sql);
+ }
+
+ /**
+ * Test caller in stacktrace
+ * @param string $sql original sql
+ * @return string sql with comments
+ */
+ public function four(string $sql) {
+ return $this->three($sql);
+ }
+
+}
*/
var CSS = {
- RESPONSIVE: 'img-responsive',
+ RESPONSIVE: 'img-fluid',
INPUTALIGNMENT: 'atto_image_alignment',
INPUTALT: 'atto_image_altentry',
INPUTHEIGHT: 'atto_image_heightentry',
// Replace placeholder with actual image.
newhtml = template({
url: file.url,
- presentation: true
+ presentation: true,
+ classlist: CSS.RESPONSIVE
});
newimage = Y.Node.create(newhtml);
if (placeholder) {
$this->delete_all_grades($source);
$success = parent::delete($source);
$transaction->allow_commit();
+
+ if ($success) {
+ $event = \core\event\grade_item_deleted::create_from_grade_item($this);
+ $event->trigger();
+ }
+
return $success;
}
* @param {HTMLElement} tableRoot
* @param {String} columnToHide
* @param {Bool} refreshContent
+ * @returns {Promise}
*/
export const hideColumn = (tableRoot, columnToHide, refreshContent = true) => {
const hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
hiddenColumns.push(columnToHide);
- updateTable(tableRoot, {hiddenColumns}, refreshContent);
+ return updateTable(tableRoot, {hiddenColumns}, refreshContent);
};
/**
* @param {HTMLElement} tableRoot
* @param {String} columnToShow
* @param {Bool} refreshContent
+ * @returns {Promise}
*/
export const showColumn = (tableRoot, columnToShow, refreshContent = true) => {
let hiddenColumns = JSON.parse(tableRoot.dataset.tableHiddenColumns);
hiddenColumns = hiddenColumns.filter(columnName => columnName !== columnToShow);
- updateTable(tableRoot, {hiddenColumns}, refreshContent);
+ return updateTable(tableRoot, {hiddenColumns}, refreshContent);
};
/**
}
}
- // Now, update the column attributes for collapsed columns
- foreach (array_keys($this->columns) as $column) {
- if (!empty($this->prefs['collapse'][$column])) {
- $this->column_style[$column]['width'] = '10px';
- }
- }
+ $this->set_hide_show_preferences();
+ $this->set_sorting_preferences();
+ $this->set_initials_preferences();
- // Now, update the column attributes for collapsed columns
+ // Now, reduce the width of collapsed columns and remove the width from columns that should be expanded.
foreach (array_keys($this->columns) as $column) {
if (!empty($this->prefs['collapse'][$column])) {
$this->column_style[$column]['width'] = '10px';
+ } else {
+ unset($this->column_style[$column]['width']);
}
}
- $this->set_sorting_preferences();
- $this->set_initials_preferences();
-
if (empty($this->baseurl)) {
debugging('You should set baseurl when using flexible_table.');
global $PAGE;
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+ @template core/paging_bar
+
+ This template renders the horizontal bar with page links, e.g.
+ | « | 1 | 2 | 3 | » |
+
+ Example context (json):
+ {
+ "previous": null,
+ "next": {
+ "page": 2,
+ "url": "./page.php?p=1"
+ },
+ "first": null,
+ "last": {
+ "page": 100,
+ "url": "./page.php?p=99"
+ },
+ "label": "Page",
+ "pages": [
+ {
+ "page": 1,
+ "active": true,
+ "url": null
+ },
+ {
+ "page": 2,
+ "active": false,
+ "url": "./page.php?p=1"
+ }
+ ],
+ "haspages": true,
+ "pagesize": 10
+ }
+}}
{{#haspages}}
<nav aria-label="{{label}}" class="pagination pagination-centered justify-content-center">
<ul class="mt-1 pagination " data-page-size="{{pagesize}}">
<a href="{{url}}" class="page-link">{{page}}</a>
</li>
<li class="page-item disabled" data-page-number="{{page}}">
- <span class="page-link">…</a>
+ <span class="page-link">…</span>
</li>
{{/first}}
{{#pages}}
{{/pages}}
{{#last}}
<li class="page-item disabled" data-page-number="{{page}}">
- <span class="page-link">…</a>
+ <span class="page-link">…</span>
</li>
<li class="page-item" data-page-number="{{page}}">
<a href="{{url}}" class="page-link">{{page}}</a>
global $CFG;
// Visit the Ionic URL and wait for it to load.
- $this->getSession()->visit($url);
+ $this->execute('behat_general::i_visit', [$url]);
$this->spin(
function($context, $args) {
$title = $context->getSession()->getPage()->find('xpath', '//title');
}, false, 60);
// Run the scripts to install Moodle 'pending' checks.
- $this->getSession()->executeScript(
- file_get_contents(__DIR__ . '/app_behat_runtime.js'));
+ $this->execute_script(file_get_contents(__DIR__ . '/app_behat_runtime.js'));
// Wait until the site login field appears OR the main page.
$situation = $this->spin(
*/
public function i_press_the_standard_button_in_the_app(string $button) {
$this->spin(function($context, $args) use ($button) {
- $result = $this->getSession()->evaluateScript('return window.behat.pressStandard("' .
- $button . '");');
+ $result = $this->evaluate_script("return window.behat.pressStandard('{$button}');");
if ($result !== 'OK') {
throw new DriverException('Error pressing standard button - ' . $result);
}
*/
public function i_close_the_popup_in_the_app() {
$this->spin(function($context, $args) {
- $result = $this->getSession()->evaluateScript('return window.behat.closePopup();');
+ $result = $this->evaluate_script("return window.behat.closePopup();");
if ($result !== 'OK') {
throw new DriverException('Error closing popup - ' . $result);
}
} else {
$nearbit = '';
}
- $result = $context->getSession()->evaluateScript('return window.behat.press("' .
+ $result = $this->evaluate_script('return window.behat.press("' .
addslashes_js($text) . '"' . $nearbit .');');
if ($result !== 'OK') {
throw new DriverException('Error pressing item - ' . $result);
*/
public function i_set_the_field_in_the_app(string $field, string $value) {
$this->spin(function($context, $args) use ($field, $value) {
- $result = $this->getSession()->evaluateScript('return window.behat.setField("' .
+ $result = $this->evaluate_script('return window.behat.setField("' .
addslashes_js($field) . '", "' . addslashes_js($value) . '");');
if ($result !== 'OK') {
throw new DriverException('Error setting field - ' . $result);
*/
public function the_header_should_be_in_the_app(string $text) {
$result = $this->spin(function($context, $args) {
- $result = $this->getSession()->evaluateScript('return window.behat.getHeader();');
+ $result = $this->evaluate_script('return window.behat.getHeader();');
if (substr($result, 0, 3) !== 'OK:') {
throw new DriverException('Error getting header - ' . $result);
}
if (count($names) !== 2) {
throw new DriverException('Expected to see 2 tabs open, not ' . count($names));
}
- $this->getSession()->getDriver()->executeScript('window.close()');
+ $this->execute_script('window.close()');
$this->getSession()->switchToWindow($names[0]);
}
* @throws DriverException If the navigator.online mode is not available
*/
public function i_switch_offline_mode(string $offline) {
- $this->getSession()->evaluateScript('appProvider.setForceOffline(' . $offline . ');');
+ $this->execute_script('appProvider.setForceOffline(' . $offline . ');');
}
}
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
require_once(__DIR__ . '/../../../lib/behat/behat_field_manager.php');
-use Behat\Gherkin\Node\TableNode as TableNode,
- Behat\Gherkin\Node\PyStringNode as PyStringNode,
- Behat\Mink\Exception\ExpectationException as ExpectationException,
- Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+use Behat\Gherkin\Node\{TableNode, PyStringNode};
+use Behat\Mink\Element\NodeElement;
+use Behat\Mink\Exception\{ElementNotFoundException, ExpectationException};
/**
* Forms-related steps definitions.
* @return void
*/
public function i_set_the_field_with_xpath_to($fieldxpath, $value) {
- $fieldnode = $this->find('xpath', $fieldxpath);
- $this->ensure_node_is_visible($fieldnode);
- $field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
- $field->set_value($value);
+ $this->set_field_node_value($this->find('xpath', $fieldxpath), $value);
}
/**
* @return void
*/
protected function set_field_value($fieldlocator, $value) {
-
// We delegate to behat_form_field class, it will
// guess the type properly as it is a select tag.
$field = behat_field_manager::get_form_field_from_label($fieldlocator, $this);
$field->set_value($value);
}
+ /**
+ * Generic field setter to be used by chainable steps.
+ *
+ * @param NodeElement $fieldnode
+ * @param string $value
+ */
+ public function set_field_node_value(NodeElement $fieldnode, string $value): void {
+ $this->ensure_node_is_visible($fieldnode);
+ $field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
+ $field->set_value($value);
+ }
+
/**
* Generic field setter.
*
* @param string $containerelement Element we look in
*/
protected function set_field_value_in_container($fieldlocator, $value, $containerselectortype, $containerelement) {
-
$node = $this->get_node_in_container('field', $fieldlocator, $containerselectortype, $containerelement);
- // We delegate to behat_form_field class, it will
- // guess the type properly as it is a select tag.
- $field = behat_field_manager::get_form_field($node, $this->getSession());
- $field->set_value($value);
+ $this->set_field_node_value($node, $value);
}
/**
* @Given /^I am on homepage$/
*/
public function i_am_on_homepage() {
- $this->getSession()->visit($this->locate_path('/'));
+ $this->execute('behat_general::i_visit', ['/']);
}
/**
* @Given /^I am on site homepage$/
*/
public function i_am_on_site_homepage() {
- $this->getSession()->visit($this->locate_path('/?redirect=0'));
+ $this->execute('behat_general::i_visit', ['/?redirect=0']);
}
/**
* @Given /^I am on course index$/
*/
public function i_am_on_course_index() {
- $this->getSession()->visit($this->locate_path('/course/index.php'));
+ $this->execute('behat_general::i_visit', ['/course/index.php']);
}
/**
// unnamed window (presumably the main window) to some other named
// window, then we first set the main window name to a conventional
// value that we can later use this name to switch back.
- $this->getSession()->executeScript(
- 'if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
+ $this->execute_script('if (window.name == "") window.name = "' . self::MAIN_WINDOW_NAME . '"');
$this->getSession()->switchToWindow($windowname);
}
$names = $this->getSession()->getWindowNames();
for ($index = 1; $index < count($names); $index ++) {
$this->getSession()->switchToWindow($names[$index]);
- $this->getSession()->executeScript("window.open('', '_self').close();");
+ $this->execute_script("window.open('', '_self').close();");
}
$names = $this->getSession()->getWindowNames();
if (count($names) !== 1) {
return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING;
})()
EOF;
- $ok = $this->getSession()->getDriver()->evaluateScript($js);
+ $ok = $this->evaluate_script($js);
} else {
// Using following xpath axe to find it.
* @Given /^I trigger cron$/
*/
public function i_trigger_cron() {
- $this->getSession()->visit($this->locate_path('/admin/cron.php'));
+ $this->execute('behat_general::i_visit', ['/admin/cron.php']);
}
/**
$this->pageloaddetectionrunning = true;
- $session->executeScript(
- 'var span = document.createElement("span");
- span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
- span.setAttribute("style", "display: none;");
- document.body.appendChild(span);');
+ $this->execute_script(
+ 'var span = document.createElement("span");
+ span.setAttribute("data-rel", "' . self::PAGE_LOAD_DETECTION_STRING . '");
+ span.setAttribute("style", "display: none;");
+ document.body.appendChild(span);'
+ );
}
/**
$xpath = addslashes_js($element->getXpath());
$script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
- $targetisfocused = $this->getSession()->evaluateScript($script);
+ $targetisfocused = $this->evaluate_script($script);
if ($not == ' not') {
if ($targetisfocused) {
throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
$xpath = addslashes_js($element->getXpath());
$script = 'return (function() { return document.activeElement === document.evaluate("' . $xpath . '",
document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; })(); ';
- $targetisfocused = $this->getSession()->evaluateScript($script);
+ $targetisfocused = $this->evaluate_script($script);
if ($not == ' not') {
if ($targetisfocused) {
throw new ExpectationException("$nodeelement $nodeselectortype is focused", $this->getSession());
$value = [\WebDriver\Key::ENTER];
$this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
}
+
+ /**
+ * Visit a local URL relative to the behat root.
+ *
+ * @When I visit :localurl
+ *
+ * @param string|moodle_url $localurl The URL relative to the behat_wwwroot to visit.
+ */
+ public function i_visit($localurl): void {
+ $localurl = new moodle_url($localurl);
+ $this->getSession()->visit($this->locate_path($localurl->out_as_local_url(false)));
+ }
}
*/
protected static $currentstepexception = null;
+ /**
+ * If an Exception is thrown in the BeforeScenario hook it will cause the Scenario to be skipped, and the exit code
+ * to be non-zero triggering a potential rerun.
+ *
+ * To combat this the exception is stored and re-thrown when looking for exceptions.
+ * This allows the test to instead be failed and re-run correctly.
+ *
+ * @var null|Exception
+ */
+ protected static $currentscenarioexception = null;
+
/**
* If we are saving any kind of dump on failure we should use the same parent dir during a run.
*
$message = <<<EOF
Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site.
- {$comandpath}
+ {$commandpath}
EOF;
self::log_and_stop($message);
// The `before_first_scenario_start_session` function will have started the session instead.
return;
}
+ self::$currentscenarioexception = null;
- $this->restart_session();
+ try {
+ $this->restart_session();
+ } catch (Exception $e) {
+ self::$currentscenarioexception = $e;
+ }
}
/**
*/
public function before_scenario_hook(BeforeScenarioScope $scope) {
global $DB;
+ if (self::$currentscenarioexception) {
+ // A BeforeScenario hook triggered an exception and marked this test as failed.
+ // Skip this hook as it will likely fail.
+ return;
+ }
+
$suitename = $scope->getSuite()->getName();
// Register behat selectors for theme, if suite is changed. We do it for every suite change.
// Again, this would be better in the BeforeSuite hook, but that does not have access to the selectors in
// order to perform the necessary searches.
$session = $this->getSession();
- $session->visit($this->locate_path('/'));
+ $this->execute('behat_general::i_visit', ['/']);
// Checking that the root path is a Moodle test site.
if (self::is_first_scenario()) {
* @BeforeStep
*/
public function before_step_javascript(BeforeStepScope $scope) {
+ if (self::$currentscenarioexception) {
+ // A BeforeScenario hook triggered an exception and marked this test as failed.
+ // Skip this hook as it will likely fail.
+ return;
+ }
+
self::$currentstepexception = null;
// Only run if JS.
* @see Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester
*/
public function i_look_for_exceptions() {
+ // If the scenario already failed in a hook throw the exception.
+ if (!is_null(self::$currentscenarioexception)) {
+ throw self::$currentscenarioexception;
+ }
+
// If the step already failed in a hook throw the exception.
if (!is_null(self::$currentstepexception)) {
throw self::$currentstepexception;
)";
// Adding an extra click we need to show the 'Log in' link.
- if (!$this->getSession()->getDriver()->evaluateScript($navbuttonjs)) {
+ if (!$this->evaluate_script($navbuttonjs)) {
return false;
}
}
}
- $this->getSession()->visit($this->locate_path($url->out_as_local_url()));
+ $this->execute('behat_general::i_visit', [$url]);
}
// Restore global user variable.
* @throws Exception if the specified page cannot be determined.
*/
public function i_am_on_page(string $page) {
- $this->getSession()->visit($this->locate_path(
- $this->resolve_page_helper($page)->out_as_local_url()));
+ $this->execute('behat_general::i_visit', [$this->resolve_page_helper($page)]);
}
/**
* @throws Exception if the specified page cannot be determined.
*/
public function i_am_on_page_instance(string $identifier, string $type) {
- $this->getSession()->visit($this->locate_path(
- $this->resolve_page_instance_helper($identifier, $type)->out_as_local_url()));
+ $this->execute('behat_general::i_visit', [$this->resolve_page_instance_helper($identifier, $type)]);
}
/**
global $DB;
$course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
$url = new moodle_url('/course/view.php', ['id' => $course->id]);
- $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
+ $this->execute('behat_general::i_visit', [$url]);
}
/**
- * Opens the course homepage with editing mode on.
+ * Open the course homepage with editing mode enabled.
*
* @Given /^I am on "(?P<coursefullname_string>(?:[^"]|\\")*)" course homepage with editing mode on$/
* @throws coding_exception
*/
public function i_am_on_course_homepage_with_editing_mode_on($coursefullname) {
global $DB;
+
$course = $DB->get_record("course", array("fullname" => $coursefullname), 'id', MUST_EXIST);
$url = new moodle_url('/course/view.php', ['id' => $course->id]);
- $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
+
+ if ($this->running_javascript() && $sesskey = $this->get_sesskey()) {
+ // Javascript is running so it is possible to grab the session ket and jump straight to editing mode.
+ $url->param('edit', 1);
+ $url->param('sesskey', $sesskey);
+ $this->execute('behat_general::i_visit', [$url]);
+
+ return;
+ }
+
+ // Visit the course page.
+ $this->execute('behat_general::i_visit', [$url]);
+
try {
$this->execute("behat_forms::press_button", get_string('turneditingon'));
} catch (Exception $e) {
if (!preg_match($fixtureregex, $url)) {
throw new coding_exception("URL {$url} is not a fixture URL");
}
- $this->getSession()->visit($this->locate_path($url));
+ $this->execute('behat_general::i_visit', [$url]);
}
}
--- /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/>.
+
+/**
+ * Grade item deleted event tests.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+/**
+ * Test for grade item deleted event.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Tom Dickman <tomdickman@catalyst-au.net>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass \core\event\grade_item_deleted
+ */
+class grade_item_deleted_testcase extends \advanced_testcase {
+
+ /**
+ * Test the grade item deleted event.
+ *
+ * @covers ::create_from_grade_item
+ */
+ public function test_grade_item_deleted() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $course = $this->getDataGenerator()->create_course();
+ $gradeitemrecord = $this->getDataGenerator()->create_grade_item(['courseid' => $course->id]);
+ $gradeitem = \grade_item::fetch(['id' => $gradeitemrecord->id, 'courseid' => $course->id]);
+
+ $countgradeitems = $DB->count_records('grade_items');
+
+ // Trigger and capture the event for deleting a grade item.
+ $sink = $this->redirectEvents();
+ $gradeitem->delete();
+ $events = $sink->get_events();
+ $sink->close();
+
+ // Event should only be triggered once.
+ $this->assertCount(1, $events);
+ $event = reset($events);
+
+ // Expect that the grade item was deleted and the event data is valid.
+ $this->assertEquals($countgradeitems - 1, $DB->count_records('grade_items'));
+ $this->assertInstanceOf('\core\event\grade_item_deleted', $event);
+ $eventdata = $event->get_data();
+ $this->assertEquals($gradeitem->id, $eventdata['objectid']);
+ $this->assertEquals($gradeitem->courseid, $eventdata['courseid']);
+ $this->assertEquals(\context_course::instance($gradeitem->courseid)->id, $eventdata['contextid']);
+ $this->assertEquals($gradeitem->itemname, $eventdata['other']['itemname']);
+ $this->assertEquals($gradeitem->itemtype, $eventdata['other']['itemtype']);
+ $this->assertEquals($gradeitem->itemmodule, $eventdata['other']['itemmodule']);
+ }
+}
* The form element 'htmleditor', which was deprecated in 3.6, has been removed.
* The `core_output_load_fontawesome_icon_map` web service has been deprecated and replaced by
`core_output_load_fontawesome_icon_system_map` which takes the name of the theme to generate the icon system map for.
+* The class coursecat_sortable_records has been removed.
=== 3.9 ===
* Following function has been deprecated, please use \core\task\manager::run_from_cli().
[
'username' => $frm->username,
'password' => $frm->password,
- 'resendconfirmemail' => true
+ 'resendconfirmemail' => true,
+ 'logintoken' => \core\session\manager::get_login_token()
]
);
echo $OUTPUT->single_button($resendconfirmurl, get_string('emailconfirmationresend'));
'jquery',
'core/pubsub',
'core/str',
- 'core_message/message_drawer_events'
+ 'core_message/message_drawer_events',
+ 'core/aria',
],
function(
$,
PubSub,
Str,
- MessageDrawerEvents
+ MessageDrawerEvents,
+ Aria
) {
/* @var {object} routes Message drawer route elements and callbacks. */
element.attr('data-from-panel', true);
}
element.removeClass('hidden');
- element.attr('aria-hidden', false);
+ Aria.unhide(element.get());
} else {
// For the message index page elements in the left panel should not be hidden.
if (!element.attr('data-in-panel')) {
element.addClass('hidden');
- element.attr('aria-hidden', true);
+ Aria.hide(element.get());
} else if (newRoute == 'view-search' || newRoute == 'view-overview') {
element.addClass('hidden');
- element.attr('aria-hidden', true);
+ Aria.hide(element.get());
}
}
});
'core/str',
'core/templates',
'core/user_date',
- 'core_message/message_drawer_view_conversation_constants'
+ 'core_message/message_drawer_view_conversation_constants',
+ 'core/aria',
],
function(
$,
Str,
Templates,
UserDate,
- Constants
+ Constants,
+ Aria
) {
var SELECTORS = Constants.SELECTORS;
var TEMPLATES = Constants.TEMPLATES;
var showConfirmDialogueContainer = function(root) {
var container = getConfirmDialogueContainer(root);
var siblings = container.siblings(':not(.hidden)');
- siblings.attr('aria-hidden', true);
- siblings.attr('tabindex', -1);
+ Aria.hide(siblings.get());
siblings.attr('data-confirm-dialogue-hidden', true);
container.removeClass('hidden');
var hideConfirmDialogueContainer = function(root) {
var container = getConfirmDialogueContainer(root);
var siblings = container.siblings('[data-confirm-dialogue-hidden="true"]');
- siblings.removeAttr('aria-hidden');
- siblings.removeAttr('tabindex');
+ Aria.unhide(siblings.get());
siblings.removeAttr('data-confirm-dialogue-hidden');
container.addClass('hidden');
var retry = element.find(SELECTORS.RETRY_SEND);
loading.addClass('hidden');
- loading.attr('aria-hidden', 'true');
+ Aria.hide(loading.get());
+
time.addClass('hidden');
- time.attr('aria-hidden', 'true');
+ Aria.hide(time.get());
+
retry.addClass('hidden');
- retry.attr('aria-hidden', 'true');
+ Aria.hide(retry.get());
+
element.removeClass('border border-danger');
switch (after.sendState) {
case 'pending':
loading.removeClass('hidden');
- loading.attr('aria-hidden', 'false');
+ Aria.unhide(loading.get());
break;
case 'error':
retry.removeClass('hidden');
- retry.attr('aria-hidden', 'false');
+ Aria.unhide(retry.get());
element.addClass('border border-danger');
break;
case 'sent':
time.removeClass('hidden');
- time.attr('aria-hidden', 'false');
+ Aria.unhide(time.get());
break;
}
}
if (after.errorMessage) {
messageContainer.removeClass('hidden');
- messageContainer.attr('aria-hidden', 'false');
+ Aria.unhide(messageContainer.get());
message.text(after.errorMessage);
} else {
messageContainer.addClass('hidden');
- messageContainer.attr('aria-hidden', 'true');
+ Aria.unhide(messageContainer.get());
message.text('');
}
}
if (show) {
container.removeClass('hidden');
- container.attr('aria-hidden', false);
+ Aria.unhide(container.get());
container.find(SELECTORS.EMOJI_PICKER_SEARCH_INPUT).focus();
} else {
container.addClass('hidden');
- container.attr('aria-hidden', true);
+ Aria.hide(container.get());
}
};
if (show) {
container.removeClass('hidden');
- container.attr('aria-hidden', false);
+ Aria.unhide(container.get());
} else {
container.addClass('hidden');
- container.attr('aria-hidden', true);
+ Aria.hide(container.get());
}
};
$userto = \core_user::get_user($requesteduserid);
$url = new \moodle_url('/message/index.php', ['view' => 'contactrequests']);
- $subject = get_string('messagecontactrequestsubject', 'core_message', (object) [
+ $subject = get_string_manager()->get_string('messagecontactrequestsubject', 'core_message', (object) [
'sitename' => format_string($SITE->fullname, true, ['context' => \context_system::instance()]),
'user' => $userfromfullname,
- ]);
- $fullmessage = get_string('messagecontactrequest', 'core_message', (object) [
+ ], $userto->lang);
+
+ $fullmessage = get_string_manager()->get_string('messagecontactrequest', 'core_message', (object) [
'url' => $url->out(),
'user' => $userfromfullname,
- ]);
+ ], $userto->lang);
$message = new \core\message\message();
$message->courseid = SITEID;
// There are 3 types of filter right now.
var filterPanel = this._region.find('[data-region="configure-filters"]');
var filters = filterPanel.find('select');
+ var preferenceNames = [];
this._filters = [];
filters.each(function(idx, ele) {
- this._filters.push($(ele).val());
+ var element = $(ele);
+ this._filters.push(element.val());
+ preferenceNames.push('assign_' + element.prop('name'));
}.bind(this));
// Update the active filter string.
var select = this._region.find('[data-action=change-user]');
var currentUserID = select.data('currentuserid');
- var preferenceNames = ['assign_filter', 'assign_workflowfilter', 'assign_markerfilter'];
this._updateFilterPreferences(currentUserID, this._filters, preferenceNames).done(function() {
// Reload the list of users to apply the new filters.
if (!this._loadAllUsers()) {
var event = { clientX: 100, clientY: 250, preventDefault: function() {} };
instance.edit_start(event);
}()); ';
- $this->getSession()->executeScript($js);
+ $this->execute_script($js);
sleep(1);
$js = ' (function() {
var instance = M.assignfeedback_editpdf.instance;
var event = { clientX: 150, clientY: 275, preventDefault: function() {} };
instance.edit_move(event);
}()); ';
- $this->getSession()->executeScript($js);
+ $this->execute_script($js);
sleep(1);
$js = ' (function() {
var instance = M.assignfeedback_editpdf.instance;
var event = { clientX: 200, clientY: 300, preventDefault: function() {} };
instance.edit_end(event);
}()); ';
- $this->getSession()->executeScript($js);
+ $this->execute_script($js);
sleep(1);
}
$mform->addElement('header', 'general', get_string('gradingoptions', 'assign'));
// Visible elements.
- $options = array(-1 => get_string('all'), 10 => '10', 20 => '20', 50 => '50', 100 => '100');
+ $options = array(10 => '10', 20 => '20', 50 => '50', 100 => '100', -1 => get_string('all'));
$maxperpage = get_config('assign', 'maxperpage');
if (isset($maxperpage) && $maxperpage != -1) {
unset($options[-1]);
--- /dev/null
+@mod @mod_assign
+Feature: In an assignment, teachers can change filters in the grading app
+ In order to manage submissions more easily
+ As a teacher
+ I need to preserve filter settings between the grader app and grading table.
+
+ @javascript
+ Scenario: Set filters in the grading table and see them in the grading app
+ Given the following "courses" exist:
+ | fullname | shortname | category | groupmode |
+ | Course 1 | C1 | 0 | 1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | marker1 | Marker | 1 | marker1@example.com |
+ | marker2 | Marker | 2 | marker2@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | marker1 | C1 | teacher |
+ | marker2 | C1 | teacher |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I add a "Assignment" to section "1" and I fill the form with:
+ | Assignment name | Test assignment name |
+ | Description | Submit your online text |
+ | assignsubmission_onlinetext_enabled | 1 |
+ | assignsubmission_file_enabled | 0 |
+ | Use marking workflow | Yes |
+ | Use marking allocation | Yes |
+ And I follow "Test assignment name"
+ And I navigate to "View all submissions" in current page administration
+ And I click on "Grade" "link" in the "Student 1" "table_row"
+ And I set the field "allocatedmarker" to "Marker 1"
+ And I set the field "workflowstate" to "In marking"
+ And I set the field "Notify students" to "0"
+ And I press "Save changes"
+ And I press "OK"
+ And I click on "Edit settings" "link"
+ And I log out
+ When I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test assignment name"
+ And I navigate to "View all submissions" in current page administration
+ And I set the field "filter" to "Not submitted"
+ And I set the field "markerfilter" to "Marker 1"
+ And I set the field "workflowfilter" to "In marking"
+ And I click on "Grade" "link" in the "Student 1" "table_row"
+ Then the field "filter" matches value "Not submitted"
+ And the field "markerfilter" matches value "Marker 1"
+ And the field "workflowfilter" matches value "In marking"
+
+ @javascript
+ Scenario: Set filters in the grading app and see them in the grading table
+ Given the following "courses" exist:
+ | fullname | shortname | category | groupmode |
+ | Course 1 | C1 | 0 | 1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ | marker1 | Marker | 1 | marker1@example.com |
+ | marker2 | Marker | 2 | marker2@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | marker1 | C1 | teacher |
+ | marker2 | C1 | teacher |
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I add a "Assignment" to section "1" and I fill the form with:
+ | Assignment name | Test assignment name |
+ | Description | Submit your online text |
+ | assignsubmission_onlinetext_enabled | 1 |
+ | assignsubmission_file_enabled | 0 |
+ | Use marking workflow | Yes |
+ | Use marking allocation | Yes |
+ And I follow "Test assignment name"
+ And I navigate to "View all submissions" in current page administration
+ And I click on "Grade" "link" in the "Student 1" "table_row"
+ And I set the field "allocatedmarker" to "Marker 1"
+ And I set the field "workflowstate" to "In marking"
+ And I set the field "Notify students" to "0"
+ And I press "Save changes"
+ And I press "OK"
+ And I click on "Edit settings" "link"
+ And I log out
+ When I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Test assignment name"
+ And I navigate to "View all submissions" in current page administration
+ And I click on "Grade" "link" in the "Student 1" "table_row"
+ And I click on "[data-region=user-filters]" "css_element"
+ And I set the field "filter" to "Not submitted"
+ # The popup closes for some reason, so it needs to be reopened.
+ And I click on "[data-region=user-filters]" "css_element"
+ And I set the field "markerfilter" to "Marker 1"
+ And I set the field "workflowfilter" to "In marking"
+ And I click on "View all submissions" "link"
+ Then the field "filter" matches value "Not submitted"
+ And the field "markerfilter" matches value "Marker 1"
+ And the field "workflowfilter" matches value "In marking"
'name', 'intro', 'introformat', 'publish',
'showresults', 'display', 'allowupdate', 'allowmultiple', 'showunanswered',
'limitanswers', 'timeopen', 'timeclose', 'timemodified',
- 'completionsubmit', 'showpreview', 'includeinactive'));
+ 'completionsubmit', 'showpreview', 'includeinactive', 'showavailable'));
$options = new backup_nested_element('options');
$choicedetails['limitanswers'] = $choice->limitanswers;
$choicedetails['showunanswered'] = $choice->showunanswered;
$choicedetails['includeinactive'] = $choice->includeinactive;
+ $choicedetails['showavailable'] = $choice->showavailable;
}
if (has_capability('moodle/course:manageactivities', $context)) {
'showpreview' => new external_value(PARAM_BOOL, 'Show preview before timeopen', VALUE_OPTIONAL),
'timemodified' => new external_value(PARAM_INT, 'Time of last modification', VALUE_OPTIONAL),
'completionsubmit' => new external_value(PARAM_BOOL, 'Completion on user submission', VALUE_OPTIONAL),
+ 'showavailable' => new external_value(PARAM_BOOL, 'Show available spaces', VALUE_OPTIONAL),
'section' => new external_value(PARAM_INT, 'Course section id', VALUE_OPTIONAL),
'visible' => new external_value(PARAM_BOOL, 'Visible', VALUE_OPTIONAL),
'groupmode' => new external_value(PARAM_INT, 'Group mode', VALUE_OPTIONAL),
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="mod/choice/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/choice"
+<XMLDB PATH="mod/choice/db" VERSION="20200616" COMMENT="XMLDB file for Moodle mod/choice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="intro" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-
<FIELD NAME="publish" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="showresults" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="display" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timeclose" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="showpreview" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-
<FIELD NAME="completionsubmit" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1, then the activity will be automatically marked as 'complete' once the user submits their choice."/>
+ <FIELD NAME="showavailable" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If this field is set to 1, then the the number of available space on choice options will be shown, given limitanswers is set to 1."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<INDEX NAME="course" UNIQUE="false" FIELDS="course"/>
</INDEXES>
</TABLE>
-
<TABLE NAME="choice_options" COMMENT="available options to choice">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEYS>
-
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="choiceid" TYPE="foreign" FIELDS="choiceid" REFTABLE="choice" REFFIELDS="id"/>
</KEYS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="choiceid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
-
<FIELD NAME="optionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
</FIELDS>
<KEY NAME="optionid" TYPE="foreign" FIELDS="optionid" REFTABLE="choice_options" REFFIELDS="id"/>
</KEYS>
<INDEXES>
-
<INDEX NAME="userid" UNIQUE="false" FIELDS="userid"/>
</INDEXES>
</TABLE>
defined('MOODLE_INTERNAL') || die();
function xmldb_choice_upgrade($oldversion) {
- global $CFG;
+ global $CFG, $DB;
+
+ $dbman = $DB->get_manager();
// Automatically generated Moodle v3.5.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v3.9.0 release upgrade line.
// Put any upgrade step following this.
+ if ($oldversion < 2020061600) {
+ // Define field showavailable to be added to choice.
+ $table = new xmldb_table('choice');
+ $field = new xmldb_field('showavailable', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'completionsubmit');
+
+ if (!$dbman->field_exists($table, $field)) {
+ $dbman->add_field($table, $field);
+ }
+
+ // Choice savepoint reached.
+ upgrade_mod_savepoint(true, 2020061600, 'choice');
+ }
return true;
}
$string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Choice activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)';
$string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth';
$string['limit'] = 'Limit';
+$string['limita'] = 'Limit: {$a}';
$string['limitno'] = 'Limit {no}';
$string['limitanswers'] = 'Limit the number of responses allowed';
$string['modulename'] = 'Choice';
$string['removeresponses'] = 'Remove all responses';
$string['responses'] = 'Responses';
$string['responsesresultgraphheader'] = 'Graph display';
+$string['responsesa'] = 'Responses: {$a}';
$string['responsesto'] = 'Responses to {$a}';
$string['results'] = 'Results';
$string['savemychoice'] = 'Save my choice';
$string['search:activity'] = 'Choice - activity information';
$string['selectalloption'] = 'Select all "{$a}"';
+$string['showavailable'] = 'Show available spaces';
+$string['showavailable_help'] = 'Allow students to see how many available spaces there are per option.';
$string['showpreview'] = 'Show preview';
$string['showpreview_help'] = 'Allow students to preview the available options before the choice is opened for submission.';
$string['showunanswered'] = 'Show column for unanswered';
$cdisplay = array('options'=>array());
- $cdisplay['limitanswers'] = true;
+ $cdisplay['limitanswers'] = $choice->limitanswers;
+ $cdisplay['showavailable'] = $choice->showavailable;
+
$context = context_module::instance($coursemodule->id);
foreach ($choice->option as $optionid => $text) {
$mform->addElement('selectyesno', 'limitanswers', get_string('limitanswers', 'choice'));
$mform->addHelpButton('limitanswers', 'limitanswers', 'choice');
+ $mform->addElement('selectyesno', 'showavailable', get_string('showavailable', 'choice'));
+ $mform->addHelpButton('showavailable', 'showavailable', 'choice');
+ $mform->hideIf('showavailable', 'limitanswers', 'eq', 0);
+
$repeatarray = array();
$repeatarray[] = $mform->createElement('text', 'option', get_string('optionno', 'choice'));
$repeatarray[] = $mform->createElement('text', 'limit', get_string('limitno', 'choice'));
$choicecount = 0;
foreach ($options['options'] as $option) {
$choicecount++;
- $html .= html_writer::start_tag('li', array('class'=>'option'));
+ $html .= html_writer::start_tag('li', array('class' => 'option mr-3'));
if ($multiple) {
$option->attributes->name = 'answer[]';
$option->attributes->type = 'checkbox';
$availableoption--;
}
+ if (!empty($options['limitanswers']) && !empty($options['showavailable'])) {
+ $labeltext .= html_writer::empty_tag('br');
+ $labeltext .= get_string("responsesa", "choice", $option->countanswers);
+ $labeltext .= html_writer::empty_tag('br');
+ $labeltext .= get_string("limita", "choice", $option->maxanswers);
+ }
+
$html .= html_writer::empty_tag('input', (array)$option->attributes + $disabled);
$html .= html_writer::tag('label', $labeltext, array('for'=>$option->attributes->id));
$html .= html_writer::end_tag('li');
$headertitle = get_string('notanswered', 'choice');
} else if ($optionid > 0) {
$headertitle = format_string($choices->options[$optionid]->text);
+ if (!empty($choices->options[$optionid]->user) && count($choices->options[$optionid]->user) > 0) {
+ if ((count($choices->options[$optionid]->user)) == ($choices->options[$optionid]->maxanswer)) {
+ $headertitle .= ' ' . get_string('full', 'choice');
+ }
+ }
}
$celltext = $headertitle;
if (!empty($options->user) && count($options->user) > 0) {
$numberofuser = count($options->user);
}
-
+ if (($choices->limitanswers) && ($choices->showavailable)) {
+ $numberofuser .= html_writer::empty_tag('br');
+ $numberofuser .= get_string("limita", "choice", $options->maxanswer);
+ }
$celloption->text = html_writer::div($celltext, 'text-center');
$optionsnames[$optionid] = $celltext;
$cellusernumber->text = html_writer::div($numberofuser, 'text-center');
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
+ | student3 | Student | 3 | student3@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
+ | student3 | C1 | student |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "Choice" to section "1" and I fill the form with:
| Choice name | Choice name |
| Description | Choice Description |
| Limit the number of responses allowed | 1 |
+ | Show available spaces | 1 |
| option[0] | Option 1 |
| limit[0] | 1 |
| option[1] | Option 2 |
And I am on "Course 1" course homepage
And I follow "Choice name"
And I should see "Option 1 (Full)"
+ And I should see "Responses: 1"
+ And I should see "Limit: 1"
And the "choice_1" "radio" should be disabled
+ And I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Choice name"
+ And I navigate to "Edit settings" in current page administration
+ And I set the following fields to these values:
+ | Limit the number of responses allowed | No |
+ And I press "Save and return to course"
+ And I log out
+ And I log in as "student3"
+ And I am on "Course 1" course homepage
+ And I follow "Choice name"
+ Then I should not see "Limit: 1"
+ And the "choice_1" "radio" should be enabled
| Description | Choice Description |
| Allow more than one choice to be selected | Yes |
| Limit the number of responses allowed | 1 |
+ | Show available spaces | 1 |
| option[0] | Option 1 |
| limit[0] | 1 |
| option[1] | Option 2 |
And the "#choice_1" "css_element" should be disabled
And the "#choice_2" "css_element" should be disabled
And the "#choice_3" "css_element" should be enabled
+ And I log out
+ And I log in as "teacher1"
+ And I am on "Course 1" course homepage
+ And I follow "Choice name"
+ And I follow "View 1 responses"
+ Then I should see "Option 1 (Full)"
+ And I should see "Limit: 1"
+ And I am on "Course 1" course homepage with editing mode on
+ And I follow "Choice name"
+ And I navigate to "Edit settings" in current page administration
+ And I set the following fields to these values:
+ | Limit the number of responses allowed | No |
+ And I press "Save and return to course"
+ And I am on "Course 1" course homepage
+ And I follow "Choice name"
+ And I follow "View 1 responses"
+ Then I should not see "Limit: 1"
+ And I log out
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2020061500; // The current module version (Date: YYYYMMDDXX)
+$plugin->version = 2020061600; // The current module version (Date: YYYYMMDDXX)
$plugin->requires = 2020060900; // Requires this Moodle version
$plugin->component = 'mod_choice'; // Full name of the plugin (used for diagnostics)
$plugin->cron = 0;
// User does not have permission to unsubscribe from this discussion at all.
$discussionsubscribe = true;
} else {
- if (isset($discussion) && \mod_forum\subscriptions::is_subscribed($USER->id, $forum, $discussionid, $cm)) {
+ if (isset($discussionid) && self::is_subscribed($USER->id, $forum, $discussionid, $cm)) {
// User is subscribed to the discussion - continue the subscription.
$discussionsubscribe = true;
} else if (!isset($discussionid) && \mod_forum\subscriptions::is_subscribed($USER->id, $forum, null, $cm)) {
$discussionrenderer = $rendererfactory->get_discussion_renderer($forum, $discussion, $displaymode);
$orderpostsby = $displaymode == FORUM_MODE_FLATNEWEST ? 'created DESC' : 'created ASC';
$replies = $postvault->get_replies_to_post($USER, $post, $capabilitymanager->can_view_any_private_reply($USER), $orderpostsby);
-$postids = array_map(function($post) {
- return $post->get_id();
-}, array_merge([$post], array_values($replies)));
if ($move == -1 and confirm_sesskey()) {
$forumname = format_string($forum->get_name(), true);
echo $OUTPUT->footer();
if ($istracked && !$CFG->forum_usermarksread) {
- forum_tp_mark_posts_read($USER, $postids);
+ if ($displaymode == FORUM_MODE_THREADED) {
+ forum_tp_add_read_record($USER->id, $post->get_id());
+ } else {
+ $postids = array_map(function($post) {
+ return $post->get_id();
+ }, array_merge([$post], array_values($replies)));
+ forum_tp_mark_posts_read($USER, $postids);
+ }
}
global $DB;
$post = $DB->get_record("forum_posts", array("subject" => $postsubject), 'id', MUST_EXIST);
$url = new moodle_url('/mod/forum/post.php', ['reply' => $post->id]);
- $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
+ $this->execute('behat_general::i_visit', [$url]);
}
}
$attrib['width'] = $maxwidth;
}
+ $attrib['class'] = "img-fluid";
$img = html_writer::empty_tag('img', $attrib);
if ($link) {
return html_writer::link($link, $img);
* Set up for every test
*/
public function setUp() {
- global $DB;
$this->resetAfterTest();
+ }
+
+ /**
+ * Sets up some basic test data including course, users, roles, and an lti instance, for use in some tests.
+ * @return array
+ */
+ protected function setup_test_data() {
+ global $DB;
$this->setAdminUser();
// Setup test data.
- $this->course = $this->getDataGenerator()->create_course();
- $this->lti = $this->getDataGenerator()->create_module('lti',
- array('course' => $this->course->id, 'toolurl' => 'http://localhost/not/real/tool.php'));
- $this->context = context_module::instance($this->lti->cmid);
- $this->cm = get_coursemodule_from_instance('lti', $this->lti->id);
+ $course = $this->getDataGenerator()->create_course();
+ $lti = $this->getDataGenerator()->create_module(
+ 'lti',
+ ['course' => $course->id, 'toolurl' => 'http://localhost/not/real/tool.php']
+ );
+ $context = context_module::instance($lti->cmid);
+ $cm = get_coursemodule_from_instance('lti', $lti->id);
// Create users.
- $this->student = self::getDataGenerator()->create_user();
- $this->teacher = self::getDataGenerator()->create_user();
+ $student = self::getDataGenerator()->create_user();
+ $teacher = self::getDataGenerator()->create_user();
// Users enrolments.
- $this->studentrole = $DB->get_record('role', array('shortname' => 'student'));
- $this->teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
- $this->getDataGenerator()->enrol_user($this->student->id, $this->course->id, $this->studentrole->id, 'manual');
- $this->getDataGenerator()->enrol_user($this->teacher->id, $this->course->id, $this->teacherrole->id, 'manual');
+ $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+ $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
+ $this->getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id, 'manual');
+ $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id, 'manual');
+
+ return [
+ 'course' => $course,
+ 'lti' => $lti,
+ 'context' => $context,
+ 'cm' => $cm,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ 'studentrole' => $studentrole,
+ 'teacherrole' => $teacherrole
+ ];
}
/**
- * Test view_lti
+ * Test get_tool_launch_data.
*/
public function test_get_tool_launch_data() {
- global $USER, $SITE;
+ global $USER;
- $result = mod_lti_external::get_tool_launch_data($this->lti->id);
+ [
+ 'course' => $course,
+ 'lti' => $lti
+ ] = $this->setup_test_data();
+
+ $result = mod_lti_external::get_tool_launch_data($lti->id);
$result = external_api::clean_returnvalue(mod_lti_external::get_tool_launch_data_returns(), $result);
&nb