Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f0c7bb53c | |||
| ba03877e5b | |||
| ea6b5194d7 | |||
| 30cc6bc7eb | |||
| 29d7066424 | |||
| 455945a6f5 | |||
| 8e0d7b04d3 | |||
| a50173c979 | |||
| 9cd200777e | |||
| 7e2931d7d3 | |||
| 6ee6c52955 | |||
| 7fcfc7ddf2 | |||
| e22a3415cf | |||
| 4d71278c7e | |||
| 6c53e9589a | |||
| bdcfa3d4ca | |||
| d115be943e | |||
| 1555c047b6 | |||
| 4809272d5f | |||
| e392ed6894 | |||
| ac7b6fb0cc | |||
| be007459d3 | |||
| 4680d53f4a | |||
| b0539cb6a8 | |||
| cf891af9ad | |||
| f60d42d040 | |||
| 8c88838e10 | |||
| 13b1d35b9a | |||
| 44fabe1c3d | |||
| 7a7194c900 | |||
| 003eaca7c0 | |||
| 887281206f | |||
| eab5ecc1f7 | |||
| 3d1aeb5a30 | |||
| b889c63bed | |||
| 9977cb7f33 | |||
| 36201a79cc | |||
| 24dfa3007c | |||
| a83b1500bb | |||
| 91ae94313f | |||
| 699e67e415 | |||
| 09260f5c02 | |||
| b9cb8a3554 | |||
| ccc4940193 | |||
| 4951ac8010 | |||
| 46dd68171a | |||
| 3a9109ffec | |||
| 1fca5f9f42 | |||
| 299bfb23cb | |||
| e4952de1af | |||
| dedff4dca4 | |||
| ea74f93c0d | |||
| dffc990f30 | |||
| 2814e94b5d | |||
| 03bec5f8f5 | |||
| d88721c821 | |||
| d8190aa5ed | |||
| 313e6d060d | |||
| 979494555b | |||
| 5fcd1fef0f | |||
| 2f96721020 | |||
| 7415adb3d3 | |||
| c60bcbe9e4 | |||
| 7a2e33f624 | |||
| 3819bef654 | |||
| d1829095d2 | |||
| d7d6a04388 | |||
| 472bc8e8d7 | |||
| 5aedb477ef | |||
| 137f093797 | |||
| deb4ed1a38 | |||
| 463fe93c8e | |||
| ef03201ae4 | |||
| 1efd85dbd6 | |||
| bcccb84b11 | |||
| dd72667450 | |||
| eccfde8616 | |||
| 5e97d00101 | |||
| a8e0d711e5 | |||
| 00b4dd0e0d | |||
| 2b51032624 | |||
| a9d4123f83 | |||
| b8b7318def | |||
| 282e257c02 | |||
| 4dbbe25ced | |||
| e92fbcf833 | |||
| 8a8223dd36 | |||
| cfcc23c1fc | |||
| 09cc81eee3 | |||
| 9556bca646 | |||
| 3281052fb4 | |||
| 2001a876d7 | |||
| 6bca945d0e | |||
| 154dcc3de5 | |||
| 8d6206acba | |||
| 0c436ba190 | |||
| 9263bbf8de | |||
| 488b0888a7 | |||
| 821336a9fc | |||
| c99cd95fb3 | |||
| 77597208b4 | |||
| 1ccbfbf5db | |||
| 09c0b90348 | |||
| 3f68ca07a2 | |||
| 1498877814 | |||
| b42be8e410 | |||
| 3fc1fd23f0 | |||
| 24dddd0a4a | |||
| 86db1e05f9 | |||
| 21f4f61ac7 | |||
| f501225d0f | |||
| 090ab30bde | |||
| df0f4456bf | |||
| 91e18d9ea2 | |||
| 508a4afaa7 | |||
| 381c4876f0 | |||
| 2968116c9f | |||
| 6e7c716eb3 | |||
| 8dc4480c50 | |||
| e0ed78c080 | |||
| 97bf649bc4 | |||
| 649a58cf99 | |||
| a44e02e755 | |||
| 65f0345ec7 | |||
| 7b759337f1 | |||
| 6375c36911 | |||
| df05a99128 | |||
| 788dbe1cd1 | |||
| 38f8e58402 | |||
| ef3f1c4625 | |||
| 98ac01fd8c | |||
| d79da7066e | |||
| 03e37cf42c | |||
| 770ff6e482 | |||
| 5dfdf96b5d | |||
| d15172627c | |||
| ec1e8dfb18 | |||
| d0f7b7f504 | |||
| 8029fb707a | |||
| ec65d872e3 | |||
| d8bbf50962 | |||
| 2da0c5dae6 | |||
| bf05268f96 | |||
| 61b05eeb97 | |||
| 73cad3a670 | |||
| 536aeb7fac | |||
| 2b1f1b05f0 | |||
| 09d3d56db5 | |||
| a13a005337 | |||
| 4cd5028c1a | |||
| ea6b15e87a | |||
| 588f7b6c0c | |||
| a42e9c1392 | |||
| c89067d1b8 | |||
| f9a2a3332d | |||
| 12f326a61e |
@@ -6,6 +6,38 @@ More detailed information on key changes can be found in the [Developer update n
|
||||
|
||||
The format of this change log follows the advice given at [Keep a CHANGELOG](https://keepachangelog.com).
|
||||
|
||||
## 4.5.4
|
||||
|
||||
### core
|
||||
|
||||
#### Added
|
||||
|
||||
- Behat now supports email content verification using Mailpit.
|
||||
You can check the contents of an email using the step `Then the email to "user@example.com" with subject containing "subject" should contain "content".`
|
||||
To use this feature:
|
||||
1. Ensure that Mailpit is running
|
||||
2. Define the following constants in your `config.php`:
|
||||
- `TEST_EMAILCATCHER_MAIL_SERVER` - The Mailpit server address (e.g. `0.0.0.0:1025`)
|
||||
- `TEST_EMAILCATCHER_API_SERVER` - The Mailpit API server (qe.g. `http://localhost:8025`)
|
||||
|
||||
3. Ensure that the email catcher is set up using the step `Given an email catcher server is configured`.
|
||||
|
||||
For more information see [MDL-75971](https://tracker.moodle.org/browse/MDL-75971)
|
||||
- The public method `get_slashargument` has been added to the `url` class.
|
||||
|
||||
For more information see [MDL-84351](https://tracker.moodle.org/browse/MDL-84351)
|
||||
- A new method, `core_text::trim_ctrl_chars()`, has been introduced to clean control characters from text. This ensures cleaner input handling and prevents issues caused by invisible or non-printable characters
|
||||
|
||||
For more information see [MDL-84907](https://tracker.moodle.org/browse/MDL-84907)
|
||||
|
||||
### core_files
|
||||
|
||||
#### Added
|
||||
|
||||
- A new function `file_clear_draft_area()` has been added to delete the files in a draft area.
|
||||
|
||||
For more information see [MDL-72050](https://tracker.moodle.org/browse/MDL-72050)
|
||||
|
||||
## 4.5.3
|
||||
|
||||
### core_question
|
||||
|
||||
@@ -55,13 +55,14 @@ If courseid is set, course module/s will be added into the course.
|
||||
|
||||
Options:
|
||||
-f, --file=STRING Path to the backup file.
|
||||
-c, --categoryid=INT ID of the course category to restore to.
|
||||
-C, --courseid=INT ID of the course to restore to. This option is ignored if categoryid is set.
|
||||
-c, --categoryid=INT ID of the course category to restore to. This option is ignored when restoring an activity and courseid is set.
|
||||
-C, --courseid=INT ID of the course to restore to. This option is ignored when restoring a course and the categoryid is set.
|
||||
-s, --showdebugging Show developer level debugging information
|
||||
-h, --help Print out this help.
|
||||
|
||||
Example:
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/file.mbz --categoryid=1\n
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/coursebackup.mbz --categoryid=1\n
|
||||
\$sudo -u www-data /usr/bin/php admin/cli/restore_backup.php --file=/path/to/backup/activitybackup.mbz --courseid=1\n
|
||||
EOL;
|
||||
|
||||
echo $help;
|
||||
@@ -84,11 +85,15 @@ if ($options['categoryid']) {
|
||||
if (!$category = $DB->get_record('course_categories', ['id' => $options['categoryid']], 'id')) {
|
||||
throw new \moodle_exception('invalidcategoryid');
|
||||
}
|
||||
} else if ($options['courseid']) {
|
||||
}
|
||||
|
||||
if ($options['courseid']) {
|
||||
if (!$course = $DB->get_record('course', ['id' => $options['courseid']], 'id')) {
|
||||
throw new \moodle_exception('invalidcourseid');
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (empty($category) && empty($course)) {
|
||||
throw new \moodle_exception('invalidoption');
|
||||
}
|
||||
|
||||
@@ -100,28 +105,46 @@ $fp = get_file_packer('application/vnd.moodle.backup');
|
||||
$fp->extract_to_pathname($options['file'], $path);
|
||||
|
||||
cli_heading(get_string('preprocessingbackupfile'));
|
||||
try {
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
|
||||
get_string('restoringcourseshortname', 'backup'));
|
||||
|
||||
if (!empty($course)) {
|
||||
$courseid = $course->id;
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
} else {
|
||||
try {
|
||||
// Create a temporary restore controller to determine the restore type.
|
||||
$tmprc = new restore_controller($backupdir, SITEID, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
// Restore the backup into a new course if:
|
||||
// - It is a course backup and the category is set.
|
||||
// - It is an activity backup and the course is not set.
|
||||
$restoreasnewcourse = ($tmprc->get_type() === backup::TYPE_1COURSE && !empty($category)) ||
|
||||
($tmprc->get_type() !== backup::TYPE_1COURSE && empty($course));
|
||||
// Make sure to clean up the temporary restore controller.
|
||||
$tmprc->destroy();
|
||||
|
||||
if ($restoreasnewcourse) {
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoringcourse', 'backup'),
|
||||
get_string('restoringcourseshortname', 'backup'));
|
||||
$courseid = restore_dbops::create_new_course($fullname, $shortname, $category->id);
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_NEW_COURSE);
|
||||
} else {
|
||||
$courseid = $course->id;
|
||||
$rc = new restore_controller($backupdir, $courseid, backup::INTERACTIVE_NO,
|
||||
backup::MODE_GENERAL, $admin->id, backup::TARGET_EXISTING_ADDING);
|
||||
}
|
||||
$rc->execute_precheck();
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Rename course name if the backup is from course module and restore to category.
|
||||
if (empty($course)) {
|
||||
// Rename the course's full and short names with the backup file's original names if the backup file is an activity backup
|
||||
// that is restored to a new course.
|
||||
if ($restoreasnewcourse && $rc->get_type() !== backup::TYPE_1COURSE) {
|
||||
$course = get_course($courseid);
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(0, get_string('restoretonewcourse', 'backup'),
|
||||
get_string('newcourse'));
|
||||
$backupinfo = $rc->get_info();
|
||||
$tmpfullname = $backupinfo->original_course_fullname ?? get_string('restoretonewcourse', 'backup');
|
||||
$tmpshortname = $backupinfo->original_course_shortname ?? get_string('newcourse');
|
||||
list($fullname, $shortname) = restore_dbops::calculate_course_names(
|
||||
courseid: 0,
|
||||
fullname: $tmpfullname,
|
||||
shortname: $tmpshortname,
|
||||
);
|
||||
$course->fullname = $fullname;
|
||||
$course->shortname = $shortname;
|
||||
$course->visible = 1;
|
||||
|
||||
@@ -4327,6 +4327,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
@@ -4522,6 +4523,7 @@
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="8.1.0" level="required">
|
||||
<RESTRICT function="restrict_php_version_84" message="unsupportedphpversion84" />
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
|
||||
@@ -81,6 +81,8 @@ class behat_admin extends behat_base {
|
||||
|
||||
$this->execute('behat_forms::i_set_the_field_with_xpath_to', [$fieldxpath, $value]);
|
||||
$this->execute("behat_general::i_click_on", [get_string('savechanges'), 'button']);
|
||||
// Wait for the page to be redirected.
|
||||
$this->execute("behat_general::i_wait_to_be_redirected");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ $action = optional_param('action', '', PARAM_ALPHA);
|
||||
// Handle any single operation actions.
|
||||
if ($action == 'requestanalysis') {
|
||||
if ($courseid != 0) {
|
||||
require_sesskey();
|
||||
scheduler::request_course_analysis($courseid);
|
||||
if ($courseid == SITEID) {
|
||||
redirect(accessibility::get_plugin_url());
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -123,6 +123,7 @@ class expired_contexts_manager {
|
||||
$expiredcontext = new expired_context(0, $orphan);
|
||||
$expiredcontext->delete();
|
||||
}
|
||||
$orphaned->close();
|
||||
|
||||
// Delete any child of a user context.
|
||||
$parentpath = $DB->sql_concat('ctxuser.path', "'/%'");
|
||||
@@ -143,6 +144,7 @@ class expired_contexts_manager {
|
||||
$expiredcontext = new expired_context(0, $child);
|
||||
$expiredcontext->delete();
|
||||
}
|
||||
$userchildren->close();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -127,7 +127,6 @@ $string['dporolemapping'] = 'Privacy officer role mapping';
|
||||
$string['dporolemapping_desc'] = 'The privacy officer can manage data requests. The capability tool/dataprivacy:managedatarequests must be allowed for a role to be listed as a privacy officer role mapping option.';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editcategory'] = 'Edit category';
|
||||
$string['editcategories'] = 'Edit categories';
|
||||
$string['editdefaults'] = 'Edit defaults: {$a}';
|
||||
$string['editmoduledefaults'] = 'Edit module defaults';
|
||||
$string['editpurpose'] = 'Edit purpose';
|
||||
|
||||
@@ -41,6 +41,18 @@ class manager {
|
||||
/** @var int */
|
||||
const REDIR_LOOP_THRESHOLD = 5;
|
||||
|
||||
/** @var array These components and related fileareas will not redirect. */
|
||||
const ALLOWED_COMPONENTS = [
|
||||
'core_admin' => [
|
||||
'logocompact',
|
||||
'logo',
|
||||
'favicon',
|
||||
],
|
||||
'tool_mfa' => [
|
||||
'guidance',
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Displays a debug table with current factor information.
|
||||
*
|
||||
@@ -424,6 +436,37 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Ensure we have a moodle_url object if a string is provided.
|
||||
if (is_string($url)) {
|
||||
$url = new \moodle_url($url);
|
||||
}
|
||||
|
||||
// Check for pluginfile.php urls.
|
||||
$pluginfileurl = new \moodle_url('/pluginfile.php');
|
||||
if ($url->compare($pluginfileurl)) {
|
||||
// Get the slash arguments.
|
||||
$args = explode('/', ltrim($url->get_slashargument(), '/'));
|
||||
|
||||
// Remove the contextid because we do not need it for this check.
|
||||
array_shift($args);
|
||||
|
||||
// Get the component and filearea.
|
||||
$component = clean_param(array_shift($args), PARAM_COMPONENT);
|
||||
$filearea = clean_param(array_shift($args), PARAM_AREA);
|
||||
|
||||
// Check allowed components.
|
||||
if (!array_key_exists($component, static::ALLOWED_COMPONENTS)) {
|
||||
return self::REDIRECT;
|
||||
}
|
||||
|
||||
// Check allowed fileareas.
|
||||
if (!in_array($filearea, static::ALLOWED_COMPONENTS[$component])) {
|
||||
return self::REDIRECT;
|
||||
}
|
||||
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Remove all params before comparison.
|
||||
$url->remove_all_params();
|
||||
|
||||
@@ -442,12 +485,6 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
// Dont redirect logo images from pluginfile.php (for example: logo in header).
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
|
||||
if ($url->compare($logourl)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Admin not setup.
|
||||
if (!empty($CFG->adminsetuppending)) {
|
||||
return self::NO_REDIRECT;
|
||||
@@ -469,12 +506,6 @@ class manager {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Enrolment.
|
||||
$enrol = new \moodle_url('/enrol/index.php');
|
||||
if ($enrol->compare($url, URL_MATCH_BASE)) {
|
||||
return self::NO_REDIRECT;
|
||||
}
|
||||
|
||||
// Guest access.
|
||||
if (isguestuser()) {
|
||||
return self::NO_REDIRECT;
|
||||
|
||||
@@ -41,7 +41,8 @@ class factor_email_renderer extends plugin_renderer_base {
|
||||
$authurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
|
||||
['instance' => $instance->id, 'pass' => 1, 'secret' => $instance->secret]);
|
||||
$authurlstring = \html_writer::link($authurl, get_string('email:link', 'factor_email'));
|
||||
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php', ['instance' => $instanceid]);
|
||||
$blockurl = new \moodle_url('/admin/tool/mfa/factor/email/email.php',
|
||||
['instance' => $instance->id, 'secret' => $instance->secret]);
|
||||
$blockurlstring = \html_writer::link($blockurl, get_string('email:stoploginlink', 'factor_email'));
|
||||
$geoinfo = iplookup_find_location($instance->createdfromip);
|
||||
|
||||
|
||||
@@ -42,7 +42,8 @@ $PAGE->set_cacheable(false);
|
||||
$instance = $DB->get_record('tool_mfa', ['id' => $instanceid]);
|
||||
$factor = \tool_mfa\plugininfo\factor::get_factor('email');
|
||||
|
||||
// If pass is set, require login to force $SESSION and user, and pass for that session.
|
||||
// If pass is set, do checks and pass for this session.
|
||||
// Require login to force $SESSION and user, and pass for that session.
|
||||
if (!empty($instance) && $pass != 0 && $secret != 0) {
|
||||
require_login();
|
||||
if ($factor->get_state() === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
|
||||
@@ -69,8 +70,12 @@ $form = new \factor_email\form\email($url);
|
||||
|
||||
if ($form->is_cancelled()) {
|
||||
redirect(new moodle_url('/'));
|
||||
} else if ($fromform = $form->get_data()) {
|
||||
if (empty($instance)) {
|
||||
}
|
||||
|
||||
// If submitted without the pass param, is a cancel request - do checks and revoke email factor.
|
||||
if ($fromform = $form->get_data()) {
|
||||
// Only allow revoke attempts from requests with a valid instance and secret.
|
||||
if (empty($instance) || empty($secret) || $instance->secret != $secret) {
|
||||
$message = get_string('error:badcode', 'factor_email');
|
||||
} else {
|
||||
$user = $DB->get_record('user', ['id' => $instance->userid]);
|
||||
|
||||
@@ -33,7 +33,7 @@ $string['email:loginlink'] = 'Or, if you\'re on the same device, use this {$a}.'
|
||||
$string['email:message'] = 'Here\'s your verification code for {$a->sitename} ({$a->siteurl}).';
|
||||
$string['email:originatingip'] = 'This login request was made from \'{$a}\'';
|
||||
$string['email:revokelink'] = 'If this wasn\'t you, you can {$a}.';
|
||||
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for {$a} have been ended.
|
||||
$string['email:revokesuccess'] = 'This code has been successfully revoked. All sessions for this user have been ended.
|
||||
Email will not be usable as a factor until account security has been verified.';
|
||||
$string['email:subject'] = 'Here\'s your verification code';
|
||||
$string['email:stoploginlink'] = 'stop this login attempt';
|
||||
|
||||
@@ -351,7 +351,10 @@ class factor extends object_factor_base {
|
||||
* @return bool
|
||||
*/
|
||||
public function show_setup_buttons(): bool {
|
||||
return true;
|
||||
if (get_config('factor_sms', 'smsgateway') > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,5 +76,25 @@ function xmldb_factor_sms_upgrade(int $oldversion): bool {
|
||||
// Automatically generated Moodle v4.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2024100702) {
|
||||
// Ensure default values are applied for the MFA SMS factor when upgrading.
|
||||
$config = get_config('factor_sms');
|
||||
|
||||
// Set the weight to the default value (100) if it is misconfigured (e.g. set to 0).
|
||||
$weight = $config->weight ?? null;
|
||||
if (isset($weight) && (int)$weight <= 0) {
|
||||
set_config('weight', 100, 'factor_sms');
|
||||
}
|
||||
|
||||
// Set the duration to the default value (30 minutes) if it is misconfigured (e.g. set to 0).
|
||||
$duration = $config->duration ?? null;
|
||||
if (isset($duration) && (int)$duration <= 0) {
|
||||
set_config('duration', 30 * MINSECS, 'factor_sms');
|
||||
}
|
||||
|
||||
// MFA savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2024100702, 'factor', 'sms');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ $string['settings:gateway_help'] = 'The SMS provider you wish to send messages v
|
||||
$string['settings:heading'] = 'Users will receive an SMS with 6-digit code during login, which they must enter to complete the login process.
|
||||
|
||||
Users will need to register their mobile phone number first.';
|
||||
$string['settings:setupdesc'] = '<br><br>To use SMS as an authentication factor, you first need to <a href="{$a}">set up an SMS gateway</a>.';
|
||||
$string['settings:setupdesc'] = 'To use SMS as an authentication factor, you first need to <a href="{$a}">set up an SMS gateway</a>.';
|
||||
$string['settings:smsgateway'] = 'SMS gateway';
|
||||
$string['settings:smsgateway_help'] = 'Select a gateway from the list, or <a href="{$a}">create a new gateway</a>.';
|
||||
$string['setupfactor'] = 'Set up SMS';
|
||||
|
||||
@@ -51,70 +51,63 @@ if ($ADMIN->fulltree) {
|
||||
),
|
||||
);
|
||||
|
||||
// Get available gateways, or link to gateway creation.
|
||||
$gateways = [0 => new lang_string('none')];
|
||||
if (count($gatewayrecords) > 0) {
|
||||
$gateways = [0 => new lang_string('none')];
|
||||
foreach ($gatewayrecords as $record) {
|
||||
$values = explode('\\', $record->gateway);
|
||||
$gatewayname = new lang_string('pluginname', $values[0]);
|
||||
$gateways[$record->id] = $record->name . ' (' . $gatewayname . ')';
|
||||
}
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configselect(
|
||||
'factor_sms/smsgateway',
|
||||
new lang_string('settings:smsgateway', 'factor_sms'),
|
||||
new lang_string('settings:smsgateway_help', 'factor_sms', $smsconfigureurl),
|
||||
0,
|
||||
$gateways,
|
||||
),
|
||||
);
|
||||
|
||||
$enabled = new admin_setting_configcheckbox(
|
||||
'factor_sms/enabled',
|
||||
new lang_string('settings:enablefactor', 'tool_mfa'),
|
||||
new lang_string('settings:enablefactor_help', 'tool_mfa'),
|
||||
0,
|
||||
);
|
||||
$enabled->set_updatedcallback(function () {
|
||||
\tool_mfa\manager::do_factor_action(
|
||||
'sms',
|
||||
get_config('factor_sms', 'enabled') ? 'enable' : 'disable',
|
||||
);
|
||||
});
|
||||
$settings->add($enabled);
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configtext(
|
||||
'factor_sms/weight',
|
||||
new lang_string('settings:weight', 'tool_mfa'),
|
||||
new lang_string('settings:weight_help', 'tool_mfa'),
|
||||
100,
|
||||
PARAM_INT,
|
||||
),
|
||||
);
|
||||
$settings->hide_if('factor_sms/weight', 'factor_sms/enabled');
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configduration(
|
||||
'factor_sms/duration',
|
||||
new lang_string('settings:duration', 'tool_mfa'),
|
||||
new lang_string('settings:duration_help', 'tool_mfa'),
|
||||
30 * MINSECS,
|
||||
MINSECS,
|
||||
),
|
||||
);
|
||||
$settings->hide_if('factor_sms/duration', 'factor_sms/enabled');
|
||||
} else {
|
||||
$settings->add(
|
||||
new admin_setting_description(
|
||||
'factor_sms/setupdesc',
|
||||
'',
|
||||
new lang_string(
|
||||
'settings:setupdesc',
|
||||
'factor_sms',
|
||||
$smsconfigureurl,
|
||||
),
|
||||
),
|
||||
$notify = new \core\output\notification(
|
||||
get_string('settings:setupdesc', 'factor_sms', $smsconfigureurl),
|
||||
\core\output\notification::NOTIFY_WARNING
|
||||
);
|
||||
$settings->add(new admin_setting_heading('factor_sms/setupdesc', '', $OUTPUT->render($notify)));
|
||||
}
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configselect(
|
||||
'factor_sms/smsgateway',
|
||||
new lang_string('settings:smsgateway', 'factor_sms'),
|
||||
new lang_string('settings:smsgateway_help', 'factor_sms', $smsconfigureurl),
|
||||
0,
|
||||
$gateways,
|
||||
),
|
||||
);
|
||||
|
||||
$enabled = new admin_setting_configcheckbox(
|
||||
'factor_sms/enabled',
|
||||
new lang_string('settings:enablefactor', 'tool_mfa'),
|
||||
new lang_string('settings:enablefactor_help', 'tool_mfa'),
|
||||
0,
|
||||
);
|
||||
$enabled->set_updatedcallback(function () {
|
||||
\tool_mfa\manager::do_factor_action(
|
||||
'sms',
|
||||
get_config('factor_sms', 'enabled') ? 'enable' : 'disable',
|
||||
);
|
||||
});
|
||||
$settings->add($enabled);
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configtext(
|
||||
'factor_sms/weight',
|
||||
new lang_string('settings:weight', 'tool_mfa'),
|
||||
new lang_string('settings:weight_help', 'tool_mfa'),
|
||||
100,
|
||||
PARAM_INT,
|
||||
),
|
||||
);
|
||||
|
||||
$settings->add(
|
||||
new admin_setting_configduration(
|
||||
'factor_sms/duration',
|
||||
new lang_string('settings:duration', 'tool_mfa'),
|
||||
new lang_string('settings:duration_help', 'tool_mfa'),
|
||||
30 * MINSECS,
|
||||
MINSECS,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2024100700; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2024100702; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2024100100; // Requires this Moodle version.
|
||||
$plugin->component = 'factor_sms'; // Full name of the plugin (used for diagnostics).
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
|
||||
@@ -29,7 +29,7 @@ require_once(__DIR__ . '/../../../config.php');
|
||||
$PAGE->set_context(\context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/admin/tool/mfa/guide.php'));
|
||||
$PAGE->set_title(get_string('guidance', 'tool_mfa'));
|
||||
$PAGE->set_pagelayout('standard');
|
||||
$PAGE->set_pagelayout('secure');
|
||||
|
||||
// If guidance page isn't enabled, just redir back to home.
|
||||
if (!get_config('tool_mfa', 'guidance')) {
|
||||
|
||||
@@ -126,8 +126,8 @@ function tool_mfa_bulk_user_actions(): array {
|
||||
/**
|
||||
* Serves any files for the guidance page.
|
||||
*
|
||||
* @param stdClass $course
|
||||
* @param stdClass $cm
|
||||
* @param stdClass|null $course
|
||||
* @param stdClass|null $cm
|
||||
* @param context $context
|
||||
* @param string $filearea
|
||||
* @param array $args
|
||||
@@ -135,7 +135,7 @@ function tool_mfa_bulk_user_actions(): array {
|
||||
* @param array $options
|
||||
* @return bool
|
||||
*/
|
||||
function tool_mfa_pluginfile(stdClass $course, stdClass $cm, context $context, string $filearea,
|
||||
function tool_mfa_pluginfile(stdClass|null $course, stdClass|null $cm, context $context, string $filearea,
|
||||
array $args, bool $forcedownload, array $options = []): bool {
|
||||
// Hardcode to only send guidance files from the top level.
|
||||
$fs = get_file_storage();
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
{
|
||||
}
|
||||
}}
|
||||
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php'>
|
||||
<a href='{{config.wwwroot}}/admin/tool/mfa/guide.php' target='_blank'>
|
||||
{{#str}} guidance, tool_mfa {{/str}}
|
||||
</a>
|
||||
|
||||
@@ -37,8 +37,17 @@ Feature: Set up and manage user factors
|
||||
Scenario: I can revoke a factor only when there is more than one active factor
|
||||
Given the following config values are set as admin:
|
||||
| enabled | 1 | factor_webauthn |
|
||||
And I navigate to "Plugins > SMS > Manage SMS gateways" in site administration
|
||||
And I follow "Create new SMS gateway"
|
||||
And I set the following fields to these values:
|
||||
| SMS gateway provider | AWS |
|
||||
| Gateway name | Dummy gateway |
|
||||
| Access key | key123 |
|
||||
| Secret access key | secret456 |
|
||||
And I press "Save changes"
|
||||
And the following config values are set as admin:
|
||||
| enabled | 1 | factor_sms |
|
||||
| smsgateway | Dummy gateway (AWS) | factor_sms |
|
||||
And the following "tool_mfa > User factors" exist:
|
||||
| username | factor | label |
|
||||
| admin | sms | +409111222 |
|
||||
|
||||
@@ -241,11 +241,6 @@ final class manager_test extends \advanced_testcase {
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setUser($user);
|
||||
|
||||
// Enrolment.
|
||||
$enrolurl = new \moodle_url('/enrol/index.php');
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($enrolurl, false));
|
||||
|
||||
// Guest User.
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setGuestUser();
|
||||
@@ -266,6 +261,27 @@ final class manager_test extends \advanced_testcase {
|
||||
\core\session\manager::loginas($user2->id, $syscontext, false);
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($badurl, false));
|
||||
$this->setUser($user);
|
||||
|
||||
// Access logocompact via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logocompact/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access logo via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/logo/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access favicon via pluginfile.
|
||||
$logourl = new \moodle_url('/pluginfile.php/1/core_admin/favicon/');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($logourl, false));
|
||||
|
||||
// Access guidance files.
|
||||
$guideurl = new \moodle_url('/pluginfile.php/1/tool_mfa/guidance/0/capybara.png');
|
||||
$this->assertEquals(\tool_mfa\manager::NO_REDIRECT, \tool_mfa\manager::should_require_mfa($guideurl, false));
|
||||
|
||||
// Access private area.
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$privateurl = new \moodle_url("/pluginfile.php/{$user3->id}/user/private/privatefile.png");
|
||||
$this->assertEquals(\tool_mfa\manager::REDIRECT, \tool_mfa\manager::should_require_mfa($privateurl, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,7 +42,7 @@ $string['activate'] = 'Set status to "Active"';
|
||||
$string['activating'] = 'Activating a policy';
|
||||
$string['activateconfirm'] = '<p>You are about to activate policy <em>\'{$a->name}\'</em> and make the version <em>\'{$a->revision}\'</em> the current one.</p><p>All users will be required to agree to this new policy version to be able to use the site.</p>';
|
||||
$string['activateconfirmyes'] = 'Activate';
|
||||
$string['agreepolicies'] = 'Please agree to the following policies';
|
||||
$string['agreepolicies'] = 'Please agree to the following policies:';
|
||||
$string['backtoprevious'] = 'Go back to previous page';
|
||||
$string['backtotop'] = 'Back to top';
|
||||
$string['cachedef_policy_optional'] = 'Cache of the optional/compulsory flag for policy versions';
|
||||
@@ -74,8 +74,8 @@ $string['filterplaceholder'] = 'Search keyword or select filter';
|
||||
$string['filterpolicy'] = 'Policy: {$a}';
|
||||
$string['guestconsent:continue'] = 'Continue';
|
||||
$string['guestconsentmessage'] = 'If you continue browsing this website, you agree to our policies:';
|
||||
$string['iagree'] = 'I agree to the {$a}';
|
||||
$string['idontagree'] = 'No thanks, I decline {$a}';
|
||||
$string['iagree'] = 'I agree to the {$a}.';
|
||||
$string['idontagree'] = 'I don\'t agree to the {$a}.';
|
||||
$string['iagreetothepolicy'] = 'Give consent';
|
||||
$string['inactivate'] = 'Set status to "Inactive"';
|
||||
$string['inactivating'] = 'Inactivating a policy';
|
||||
@@ -94,9 +94,9 @@ $string['newpolicy'] = 'New policy';
|
||||
$string['newversion'] = 'New version';
|
||||
$string['noactivepolicies'] = 'There are no policies with an active version.';
|
||||
$string['nofiltersapplied'] = 'No filters applied';
|
||||
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies';
|
||||
$string['nopermissiontoagreedocs'] = 'No permission to agree to the policies.';
|
||||
$string['nopermissiontoagreedocs_desc'] = 'Sorry, you do not have the required permissions to agree to the policies.<br />You will not be able to use this site until the following policies are agreed:';
|
||||
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user';
|
||||
$string['nopermissiontoagreedocsbehalf'] = 'No permission to agree to the policies on behalf of this user.';
|
||||
$string['nopermissiontoagreedocsbehalf_desc'] = 'Sorry, you do not have the required permission to agree to the following policies on behalf of {$a}:';
|
||||
$string['nopermissiontoagreedocscontact'] = 'For more help:';
|
||||
$string['nopermissiontoviewpolicyversion'] = 'You do not have permissions to view this policy version.';
|
||||
|
||||
@@ -34,13 +34,13 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
Then I should see "This site policy"
|
||||
And I should not see "Course overview"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And "Accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -53,7 +53,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "1 of 4 (25%)" "link" in the "This site policy" "table_row"
|
||||
@@ -76,7 +76,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
@@ -104,16 +104,16 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And "Accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -142,8 +142,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Accept This site policy" "link" in the "User One" "table_row"
|
||||
@@ -176,8 +176,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Accept This site policy" "link" in the "User One" "table_row"
|
||||
@@ -198,7 +198,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
Scenario: Policies and agreements profile link visible for current user
|
||||
Given I log in as "user1"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
When I follow "Profile" in the user menu
|
||||
# User can see his own agreements link in the profile.
|
||||
@@ -217,7 +217,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
# User can see agreements link in other user profiles because has the capability for accepting on behalf of them.
|
||||
When I am on "Course1" course homepage
|
||||
@@ -231,7 +231,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
| tool/policy:acceptbehalf | allow |
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I log out
|
||||
And I am on the "manager" "user > profile" page logged in as "admin"
|
||||
@@ -267,8 +267,8 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
And I click on "Select" "checkbox" in the "User One" "table_row"
|
||||
@@ -290,7 +290,7 @@ Feature: Viewing acceptances reports and accepting on behalf of other users
|
||||
And I should see "This site policy"
|
||||
And I should not see "Course overview"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "Calendar"
|
||||
And I log out
|
||||
|
||||
@@ -4,7 +4,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
As a user
|
||||
I need to be able to accept site policy during sign up
|
||||
|
||||
Scenario: Accept policy on sign up, no site policy
|
||||
Scenario: Accept policy on sign up manage by plugin, no site policy
|
||||
Given the following config values are set as admin:
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
@@ -16,13 +16,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -50,13 +50,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -85,23 +85,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -137,7 +137,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
@@ -147,20 +147,20 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I should not see "full text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -197,23 +197,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -244,19 +244,19 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# Confirm that a notification is displayed if none of the policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "0"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
When I set the field "I agree to the This site policy." to "0"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
And I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that a notification is displayed if only some policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
Then I should see "Please agree to the following policies"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
|
||||
Scenario: Accept policy on login, do not accept all policies
|
||||
@@ -276,23 +276,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# Confirm that a notification is displayed if none of the policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "0"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
When I set the field "I agree to the This site policy." to "0"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
And I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that a notification is displayed if only some policies are accepted.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "0"
|
||||
Then I should see "Please agree to the following policies"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "0"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that user can not browse the site (edit their profile).
|
||||
When I follow "Profile" in the user menu
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
|
||||
Scenario: Accept policy on login, accept all policies
|
||||
Given the following config values are set as admin:
|
||||
@@ -311,14 +311,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# User accepts all policies.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
And I should not see "Before continuing you need to acknowledge all these policies."
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
@@ -328,7 +328,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
When I log in as "user1"
|
||||
Then I should not see "This site policy"
|
||||
And I should not see "This privacy policy"
|
||||
And I should not see "Please agree to the following policies"
|
||||
And I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -350,14 +350,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I press "Next"
|
||||
And I should see "This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "This privacy policy"
|
||||
# User accepts all policies.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -385,7 +385,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This privacy policy"
|
||||
Then I should see "This third parties policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This third parties policy"
|
||||
And I set the field "This third parties policy" to "1"
|
||||
And I press "Next"
|
||||
@@ -407,12 +407,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I log in as "user1"
|
||||
And I should see "This site policy"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
# User accepts policy.
|
||||
When I set the field "I agree to the This site policy" to "1"
|
||||
When I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -437,13 +437,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2 new version"
|
||||
And I should see "full text2 new version"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy new version"
|
||||
And I should see "short text2 new version"
|
||||
# User accepts policy.
|
||||
And I set the field "I agree to the This site policy new version" to "1"
|
||||
And I set the field "I agree to the This site policy new version." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "Please agree to the following policies"
|
||||
Then I should not see "Please agree to the following policies:"
|
||||
# Confirm that user can login and browse the site (edit their profile).
|
||||
When I open my profile in edit mode
|
||||
Then the field "First name" matches value "User"
|
||||
@@ -504,28 +504,28 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should see "This privacy policy" in the "region-main" "region"
|
||||
And I should see "short text3"
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "I understand and agree"
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
When I press "Create my new account"
|
||||
Then I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I follow "Log in"
|
||||
When I click on "Create new account" "link"
|
||||
# Confirm that the user can view and accept policies when attempting to create another account.
|
||||
@@ -537,7 +537,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text3"
|
||||
And I should see "full text3"
|
||||
When I press "Next"
|
||||
Then I should see "Please agree to the following policies"
|
||||
Then I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy" in the "region-main" "region"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
@@ -547,8 +547,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should not see "This guests policy" in the "region-main" "region"
|
||||
And I should not see "short text4"
|
||||
And I should not see "full text4"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
When I press "Next"
|
||||
Then I should not see "I understand and agree"
|
||||
And I should see "New account"
|
||||
@@ -572,8 +572,8 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "Please read our This privacy policy"
|
||||
And I press "Next"
|
||||
And I should see "Viewing this page on behalf of User 1"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I log out
|
||||
And I log in as "user1"
|
||||
@@ -599,15 +599,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
When I log in as "manager"
|
||||
And I press "Next"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This privacy policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I set the field "I agree to the This privacy policy." to "1"
|
||||
And I press "Next"
|
||||
And I am on the "user1" "user > profile" page
|
||||
And I follow "Log in as"
|
||||
Then I should see "You are logged in as User 1"
|
||||
And I press "Continue"
|
||||
And I should see "Policies and agreements"
|
||||
And I should see "No permission to agree to the policies on behalf of this user"
|
||||
And I should see "No permission to agree to the policies on behalf of this user."
|
||||
And I should see "Sorry, you do not have the required permission to agree to the following policies on behalf of User 1"
|
||||
|
||||
Scenario: Accept policy on sign up as a guest, one policy
|
||||
@@ -631,23 +631,23 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "short text2"
|
||||
And I should see "full text2"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "This site policy"
|
||||
And I should see "short text2"
|
||||
And I should not see "full text2"
|
||||
And I set the field "I agree to the This site policy" to "1"
|
||||
And I set the field "I agree to the This site policy." to "1"
|
||||
And I press "Next"
|
||||
And I should not see "I understand and agree"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -680,12 +680,12 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
# The second policy with the agreement style "on its own page" must be accepted now.
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
# Only now we see the remaining consent page policies.
|
||||
And I should see "Policy 1 out of 2"
|
||||
And I should see "Privacy policy" in the "region-main" "region"
|
||||
@@ -697,26 +697,26 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I should see "Here goes content."
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "Privacy policy"
|
||||
And I should see "Cookies policy"
|
||||
And I should not see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should not see "Terms of Service" in the "region-main" "region"
|
||||
And I should not see "Here goes content."
|
||||
And I set the field "I agree to the Privacy policy" to "1"
|
||||
And I set the field "I agree to the Cookies policy" to "1"
|
||||
And I set the field "I agree to the Privacy policy." to "1"
|
||||
And I set the field "I agree to the Cookies policy." to "1"
|
||||
And I press "Next"
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -751,7 +751,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
# The second policy with the agreement style "on its own page" must be accepted now.
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
@@ -762,7 +762,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
# Only now we see the remaining consent page policies.
|
||||
And I should see "Policy 1 out of 2"
|
||||
And I should see "Privacy policy" in the "region-main" "region"
|
||||
@@ -774,14 +774,14 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I should see "Here goes content."
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I should see "Privacy policy"
|
||||
And I should see "Cookies policy"
|
||||
And I should not see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should not see "Terms of Service" in the "region-main" "region"
|
||||
And I should not see "Here goes content."
|
||||
And I set the field "I agree to the Privacy policy" to "1"
|
||||
And I set the field "I agree to the Cookies policy" to "1"
|
||||
And I set the field "I agree to the Privacy policy." to "1"
|
||||
And I set the field "I agree to the Cookies policy." to "1"
|
||||
And I press "Next"
|
||||
And I follow "Profile" in the user menu
|
||||
# User can see his own agreements in the profile.
|
||||
@@ -809,15 +809,15 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Privacy policy" in the "region-main" "region"
|
||||
And I should see "We scan your thoughts"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Privacy policy"
|
||||
And I press "I agree to the Privacy policy."
|
||||
And I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
And I should see "Terms of Service" in the "region-main" "region"
|
||||
And I should see "We teach, you learn"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Terms of Service"
|
||||
And I press "I agree to the Terms of Service."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "Privacy policy" "table_row"
|
||||
@@ -844,21 +844,21 @@ Feature: User must accept policy managed by this plugin when logging in and sign
|
||||
Then I should see "Digital maturity declaration" in the "region-main" "region"
|
||||
And I should see "You declare be old enough"
|
||||
And I should see "Here goes content."
|
||||
And I press "I agree to the Digital maturity declaration"
|
||||
And I press "I agree to the Digital maturity declaration."
|
||||
And I should see "Cookies policy" in the "region-main" "region"
|
||||
And I should see "We eat cookies, srsly"
|
||||
And I press "I agree to the Cookies policy"
|
||||
And I press "I agree to the Cookies policy."
|
||||
And I should see "New account"
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
|
||||
@@ -61,7 +61,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -83,7 +83,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -101,7 +101,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Edit" "link" in the "Policy1" "table_row"
|
||||
@@ -180,7 +180,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
|
||||
@@ -218,7 +218,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "manager"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I navigate to "Users > Privacy and policies > Manage policies" in site administration
|
||||
And I click on "Set status to \"Inactive\"" "link" in the "Policy1" "table_row"
|
||||
@@ -247,7 +247,7 @@ Feature: Manage policies
|
||||
| Policy1 | v1 | full text2 | short text2 | active |
|
||||
And I log in as "user1"
|
||||
And I press "Next"
|
||||
And I set the field "I agree to the Policy1" to "1"
|
||||
And I set the field "I agree to the Policy1." to "1"
|
||||
And I press "Next"
|
||||
And I follow "Preferences" in the user menu
|
||||
And I should see "Preferences"
|
||||
|
||||
@@ -70,26 +70,26 @@ Feature: Optional policies
|
||||
And I should see "OwnPageCompulsory1" in the "region-main" "region"
|
||||
And I should see "short text4" in the "region-main" "region"
|
||||
And I should see "full text4" in the "region-main" "region"
|
||||
And I press "I agree to the OwnPageCompulsory1"
|
||||
And I press "I agree to the OwnPageCompulsory1."
|
||||
# Compulsory policies displayed on the consent page are shown next and must be agreed.
|
||||
And I should see "ConsentPageCompulsory1"
|
||||
And I should see "short text3" in the "region-main" "region"
|
||||
And I should see "full text3" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "I agree to the ConsentPageCompulsory1" to "1"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I agree to the ConsentPageCompulsory1." to "1"
|
||||
And I press "Next"
|
||||
# The signup form can be submitted and a new account created.
|
||||
And I set the following fields to these values:
|
||||
| Username | user3 |
|
||||
| Password | user3 |
|
||||
| Email address | user3@address.invalid |
|
||||
| Email (again) | user3@address.invalid |
|
||||
| Email address | user3@example.com |
|
||||
| Email (again) | user3@example.com |
|
||||
| First name | User3 |
|
||||
| Last name | L3 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user3@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user3@example.com"
|
||||
And I confirm email for "user3"
|
||||
And I should see "Thanks, User3 L3"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -99,7 +99,7 @@ Feature: Optional policies
|
||||
Then I should see "OwnPageOptional1"
|
||||
And I should see "short text5" in the "region-main" "region"
|
||||
And I should see "full text5" in the "region-main" "region"
|
||||
And I press "No thanks, I decline OwnPageOptional1"
|
||||
And I press "I don't agree to the OwnPageOptional1"
|
||||
# Then come policies displayed on the consent page.
|
||||
And I should see "ConsentPageOptional1" in the "region-main" "region"
|
||||
And I should see "short text1" in the "region-main" "region"
|
||||
@@ -109,9 +109,9 @@ Feature: Optional policies
|
||||
And I should see "short text2" in the "region-main" "region"
|
||||
And I should see "full text2" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "I agree to the ConsentPageOptional1" to "1"
|
||||
And I set the field "No thanks, I decline ConsentPageOptional2" to "0"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I agree to the ConsentPageOptional1." to "1"
|
||||
And I set the field "I don't agree to the ConsentPageOptional2" to "0"
|
||||
And I press "Next"
|
||||
# Accepted and declined policies are shown in the profile.
|
||||
And I follow "Profile" in the user menu
|
||||
@@ -132,14 +132,14 @@ Feature: Optional policies
|
||||
Then I should see "OwnPageOptional1"
|
||||
And I should see "short text5" in the "region-main" "region"
|
||||
And I should see "full text5" in the "region-main" "region"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
# Then come policies displayed on the consent page.
|
||||
And I should see "ConsentPageOptional1" in the "region-main" "region"
|
||||
And I should see "short text1" in the "region-main" "region"
|
||||
And I should see "full text1" in the "region-main" "region"
|
||||
And I press "Next"
|
||||
And I should see "Please agree to the following policies"
|
||||
And I set the field "No thanks, I decline ConsentPageOptional1" to "0"
|
||||
And I should see "Please agree to the following policies:"
|
||||
And I set the field "I don't agree to the ConsentPageOptional1" to "0"
|
||||
And I press "Next"
|
||||
# Accepted and declined policies are shown in the profile.
|
||||
And I follow "Profile" in the user menu
|
||||
@@ -152,7 +152,7 @@ Feature: Optional policies
|
||||
| Name | Content | Summary | Agreementstyle | Optional |
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
|
||||
@@ -177,7 +177,7 @@ Feature: Optional policies
|
||||
| Name | Content | Summary | Agreementstyle | Optional |
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I follow "Profile" in the user menu
|
||||
And I follow "Policies and agreements"
|
||||
And "Accepted" "text" should exist in the "OwnPageOptional1" "table_row"
|
||||
@@ -202,12 +202,12 @@ Feature: Optional policies
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "No thanks, I decline OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I don't agree to the OwnPageOptional2"
|
||||
And I log out
|
||||
And I log in as "manager"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "I agree to the OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I agree to the OwnPageOptional2."
|
||||
When I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
# User One has accepted just some policies.
|
||||
Then "Partially accepted" "text" should exist in the "User One" "table_row"
|
||||
@@ -228,8 +228,8 @@ Feature: Optional policies
|
||||
| OwnPageOptional1 | full text1 | short text1 | 1 | 1 |
|
||||
| OwnPageOptional2 | full text2 | short text2 | 1 | 1 |
|
||||
And I log in as "user1"
|
||||
And I press "I agree to the OwnPageOptional1"
|
||||
And I press "No thanks, I decline OwnPageOptional2"
|
||||
And I press "I agree to the OwnPageOptional1."
|
||||
And I press "I don't agree to the OwnPageOptional2"
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
When I navigate to "Users > Privacy and policies > User agreements" in site administration
|
||||
|
||||
@@ -208,12 +208,11 @@ class helper {
|
||||
* @return \moodle_url The URL.
|
||||
*/
|
||||
public static function get_duplicate_tour_link($tourid) {
|
||||
$link = new \moodle_url('/admin/tool/usertours/configure.php', [
|
||||
return new \moodle_url('/admin/tool/usertours/configure.php', [
|
||||
'action' => manager::ACTION_DUPLICATETOUR,
|
||||
'id' => $tourid,
|
||||
'sesskey' => sesskey(),
|
||||
]);
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -503,8 +503,9 @@ class manager {
|
||||
* @param int $tourid The ID of the tour to duplicate.
|
||||
*/
|
||||
protected function duplicate_tour($tourid) {
|
||||
$tour = helper::get_tour($tourid);
|
||||
require_sesskey();
|
||||
|
||||
$tour = helper::get_tour($tourid);
|
||||
$export = $tour->to_record();
|
||||
// Remove the id.
|
||||
unset($export->id);
|
||||
|
||||
@@ -102,7 +102,6 @@ $string['name'] = 'Name';
|
||||
$string['name_help'] = 'The name of a tour may be added as plain text, enclosed in multilang tags (for use with the multi-language content filter) if required.
|
||||
|
||||
Alternatively, a language string ID may be entered in the format identifier,component (with no brackets or space after the comma).';
|
||||
$string['newstep'] = 'Create step';
|
||||
$string['newstep'] = 'New step';
|
||||
$string['newtour'] = 'Create a new tour';
|
||||
$string['next'] = 'Next';
|
||||
|
||||
@@ -15,13 +15,13 @@ Feature: User must accept policy when logging in and signing up
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | student1@example.com |
|
||||
| Email (again) | student1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at student1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -45,14 +45,14 @@ Feature: User must accept policy when logging in and signing up
|
||||
And I set the following fields to these values:
|
||||
| Username | user1 |
|
||||
| Password | user1 |
|
||||
| Email address | user1@address.invalid |
|
||||
| Email (again) | user1@address.invalid |
|
||||
| Email address | user1@example.com |
|
||||
| Email (again) | user1@example.com |
|
||||
| First name | User1 |
|
||||
| Last name | L1 |
|
||||
| I understand and agree | 1 |
|
||||
And I press "Create my new account"
|
||||
And I should see "Confirm your account"
|
||||
And I should see "An email should have been sent to your address at user1@address.invalid"
|
||||
And I should see "An email should have been sent to your address at user1@example.com"
|
||||
And I confirm email for "user1"
|
||||
And I should see "Thanks, User1 L1"
|
||||
And I should see "Your registration has been confirmed"
|
||||
@@ -67,8 +67,8 @@ Feature: User must accept policy when logging in and signing up
|
||||
Scenario Outline: Email validation during email registration
|
||||
Given the following config values are set as admin:
|
||||
| allowaccountssameemail | <allowsameemail> |
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
| registerauth | email |
|
||||
| passwordpolicy | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| s1 | John | Doe | s1@example.com |
|
||||
|
||||
@@ -197,8 +197,7 @@ abstract class restore_qtype_plugin extends restore_plugin {
|
||||
$this->questionanswercacheid = $newquestionid;
|
||||
// Cache all cleaned answers for a simple text match.
|
||||
foreach ($answers as $answer) {
|
||||
// MDL-30018: Clean in the same way as {@link xml_writer::xml_safe_utf8()}.
|
||||
$clean = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $answer->answer); // Clean CTRL chars.
|
||||
$clean = core_text::trim_ctrl_chars($answer->answer); // Clean CTRL chars.
|
||||
$clean = preg_replace("/\r\n|\r/", "\n", $clean); // Normalize line ending.
|
||||
$this->questionanswercache[$clean] = $answer->id;
|
||||
}
|
||||
|
||||
@@ -1859,9 +1859,13 @@ class restore_section_structure_step extends restore_structure_step {
|
||||
[$this->get_courseid()],
|
||||
'section DESC', 'id, section'
|
||||
);
|
||||
// Here we add the new section to the end of the list so we make sure that all delegated sections are really
|
||||
// all located after the normal sections. We can have case where delegated sections are located before the
|
||||
// normal sections, so we need to move them to the end (mostly in the restore process more than in the duplicate
|
||||
// process in which the order sections => delegated section is mostly there).
|
||||
$sectionnum = $sectionnum + count($sectionstomove);
|
||||
foreach ($sectionstomove as $section) {
|
||||
$sectionnum++;
|
||||
$section->section = $sectionnum;
|
||||
$section->section = $sectionnum--;
|
||||
$DB->update_record('course_sections', $section);
|
||||
}
|
||||
}
|
||||
@@ -5352,8 +5356,7 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$potentialhints = $DB->get_records('question_hints',
|
||||
array('questionid' => $newquestionid), '', 'id, hint');
|
||||
foreach ($potentialhints as $potentialhint) {
|
||||
// Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
|
||||
$cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
|
||||
$cleanhint = core_text::trim_ctrl_chars($potentialhint->hint); // Clean CTRL chars.
|
||||
$cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
|
||||
if ($cleanhint === $data->hint) {
|
||||
$newitemid = $data->id;
|
||||
|
||||
@@ -253,14 +253,14 @@ class xml_writer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some UTF-8 cleaning, stripping the control chars (\x0-\x1f)
|
||||
* but tabs (\x9), newlines (\xa) and returns (\xd). The delete control
|
||||
* Perform some UTF-8 cleaning, stripping the control chars (\x00-\x1f)
|
||||
* but tabs (\x09), newlines (\xa) and returns (\xd). The delete control
|
||||
* char (\x7f) is also included. All them are forbiden in XML 1.0 specs.
|
||||
* The expression below seems to be UTF-8 safe too because it simply
|
||||
* ignores the rest of characters. Also normalize linefeeds and return chars.
|
||||
*/
|
||||
protected function xml_safe_utf8($content) {
|
||||
$content = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is', '', $content ?? ''); // clean CTRL chars.
|
||||
$content = core_text::trim_ctrl_chars($content ?? '');
|
||||
$content = preg_replace("/\r\n|\r/", "\n", $content); // Normalize line&return=>line
|
||||
return fix_utf8($content);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Script to let a user edit the properties of a particular RSS feed.
|
||||
* Script to let a user view the output of a particular RSS feed.
|
||||
*
|
||||
* @package block_rss_client
|
||||
* @copyright 2009 Tim Hunt
|
||||
@@ -26,9 +26,6 @@ require_once(__DIR__ . '/../../config.php');
|
||||
require_once($CFG->libdir .'/simplepie/moodle_simplepie.php');
|
||||
|
||||
require_login();
|
||||
if (isguestuser()) {
|
||||
throw new \moodle_exception('guestsarenotallowed');
|
||||
}
|
||||
|
||||
$returnurl = optional_param('returnurl', '', PARAM_LOCALURL);
|
||||
$courseid = optional_param('courseid', 0, PARAM_INT);
|
||||
@@ -46,6 +43,11 @@ if ($courseid) {
|
||||
$PAGE->set_context($context);
|
||||
}
|
||||
|
||||
$managesharedfeeds = has_capability('block/rss_client:manageanyfeeds', $context);
|
||||
if (!$managesharedfeeds) {
|
||||
require_capability('block/rss_client:manageownfeeds', $context);
|
||||
}
|
||||
|
||||
$urlparams = array('rssid' => $rssid);
|
||||
if ($courseid) {
|
||||
$urlparams['courseid'] = $courseid;
|
||||
@@ -56,10 +58,18 @@ if ($returnurl) {
|
||||
$PAGE->set_url('/blocks/rss_client/viewfeed.php', $urlparams);
|
||||
$PAGE->set_pagelayout('popup');
|
||||
|
||||
$rssrecord = $DB->get_record('block_rss_client', array('id' => $rssid), '*', MUST_EXIST);
|
||||
if ($managesharedfeeds) {
|
||||
$select = 'id = :id AND (userid = :userid OR shared = 1)';
|
||||
} else {
|
||||
$select = 'id = :id AND userid = :userid';
|
||||
}
|
||||
|
||||
$rssrecord = $DB->get_record_select('block_rss_client', $select, [
|
||||
'id' => $rssid,
|
||||
'userid' => $USER->id,
|
||||
], '*', MUST_EXIST);
|
||||
|
||||
$rss = new moodle_simplepie($rssrecord->url);
|
||||
|
||||
if ($rss->error()) {
|
||||
debugging($rss->error());
|
||||
throw new \moodle_exception('errorfetchingrssfeed');
|
||||
|
||||
@@ -111,7 +111,7 @@ class block_section_links extends block_base {
|
||||
$showsectionname = !empty($config->showsectionname) ? true : false;
|
||||
|
||||
// Prepare an array of sections to create links for.
|
||||
$sections = array();
|
||||
$sections = [];
|
||||
$canviewhidden = has_capability('moodle/course:update', $context);
|
||||
$coursesections = $courseformat->get_sections();
|
||||
$coursesectionscount = count($coursesections);
|
||||
@@ -121,12 +121,16 @@ class block_section_links extends block_base {
|
||||
continue;
|
||||
}
|
||||
$section = $coursesections[$i];
|
||||
// Delegated sections (like subsections) are not listed in the block.
|
||||
if ($section->get_component_instance() !== null) {
|
||||
continue;
|
||||
}
|
||||
if ($section->section && ($section->visible || $canviewhidden)) {
|
||||
$sections[$i] = (object)array(
|
||||
$sections[$i] = (object) [
|
||||
'section' => $section->section,
|
||||
'visible' => $section->visible,
|
||||
'highlight' => false
|
||||
);
|
||||
];
|
||||
if ($courseformat->is_section_current($section)) {
|
||||
$sections[$i]->highlight = true;
|
||||
$sectiontojumpto = $section->section;
|
||||
|
||||
@@ -60,3 +60,17 @@ Feature: The section links block allows users to quickly navigate around a moodl
|
||||
And I should see "5" in the "Section links" "block"
|
||||
When I follow "5"
|
||||
Then I should see "Test assignment 1"
|
||||
|
||||
Scenario: Subsections numbers are not displayed in the Section links block
|
||||
Given I enable "subsection" "mod" plugin
|
||||
And the following "activity" exists:
|
||||
| activity | subsection |
|
||||
| name | Subsection1 |
|
||||
| course | C1 |
|
||||
| idnumber | subsection1 |
|
||||
| section | 1 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| section_links | Course | C1 | course-view-* | side-pre |
|
||||
When I am on "Course 1" course homepage
|
||||
Then "21" "link" should not exist in the "Section links" "block"
|
||||
|
||||
@@ -44,3 +44,14 @@ Feature: The Section links block can be configured to display section name in ad
|
||||
And I should see "7" in the "Section links" "block"
|
||||
And I follow "7"
|
||||
And I should see "First assignment"
|
||||
|
||||
Scenario: Subsections names are not displayed in the Section links block
|
||||
Given I enable "subsection" "mod" plugin
|
||||
And the following "activity" exists:
|
||||
| activity | subsection |
|
||||
| name | Subsection1 |
|
||||
| course | C1 |
|
||||
| idnumber | subsection1 |
|
||||
| section | 1 |
|
||||
When I am on the "Course 1" course page logged in as student1
|
||||
Then I should not see "Subsection1" in the "Section links" "block"
|
||||
|
||||
@@ -73,11 +73,11 @@ class cohorts extends system_report {
|
||||
});
|
||||
|
||||
// Check if report needs to show a specific category.
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
|
||||
if (!$showall) {
|
||||
if (!$this->get_context() instanceof context_system || !$this->get_parameter('showall', false, PARAM_BOOL)) {
|
||||
$paramcontextid = database::generate_param_name();
|
||||
$this->add_base_condition_sql("{$entitymainalias}.contextid = :$paramcontextid", [$paramcontextid => $contextid]);
|
||||
$this->add_base_condition_sql("{$entitymainalias}.contextid = :{$paramcontextid}", [
|
||||
$paramcontextid => $this->get_context()->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Now we can call our helper methods to add the content we want to include in the report.
|
||||
@@ -95,14 +95,7 @@ class cohorts extends system_report {
|
||||
* @return bool
|
||||
*/
|
||||
protected function can_view(): bool {
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
if ($contextid) {
|
||||
$context = context::instance_by_id($contextid, MUST_EXIST);
|
||||
} else {
|
||||
$context = context_system::instance();
|
||||
}
|
||||
|
||||
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $context);
|
||||
return has_any_capability(['moodle/cohort:manage', 'moodle/cohort:view'], $this->get_context());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +108,8 @@ class cohorts extends system_report {
|
||||
$cohortentity = $this->get_entity('cohort');
|
||||
$entitymainalias = $cohortentity->get_table_alias('cohort');
|
||||
|
||||
$showall = $this->get_parameter('showall', false, PARAM_BOOL);
|
||||
|
||||
// Category column. An extra callback is appended in order to extend the current column formatting.
|
||||
if ($showall) {
|
||||
if ($this->get_context() instanceof context_system && $this->get_parameter('showall', false, PARAM_BOOL)) {
|
||||
$this->add_column_from_entity('cohort:context')
|
||||
->add_callback(static function(string $value, stdClass $cohort): string {
|
||||
$context = context::instance_by_id($cohort->contextid);
|
||||
@@ -198,10 +189,11 @@ class cohorts extends system_report {
|
||||
*/
|
||||
protected function add_actions(): void {
|
||||
|
||||
$contextid = $this->get_parameter('contextid', 0, PARAM_INT);
|
||||
$showall = $this->get_parameter('showall', true, PARAM_BOOL);
|
||||
$returnurl = (new moodle_url('/cohort/index.php',
|
||||
['id' => ':id', 'contextid' => $contextid, 'showall' => $showall]))->out(false);
|
||||
$returnurl = (new moodle_url('/cohort/index.php', [
|
||||
'id' => ':id',
|
||||
'contextid' => $this->get_context()->id,
|
||||
'showall' => $this->get_parameter('showall', false, PARAM_BOOL),
|
||||
]))->out(false);
|
||||
|
||||
// Hide action. It will be only shown if the property 'visible' is true and user has 'moodle/cohort:manage' capabillity.
|
||||
$this->add_action((new action(
|
||||
|
||||
+1
-2
@@ -104,8 +104,7 @@ if ($editcontrols = cohort_edit_controls($context, $baseurl)) {
|
||||
echo $OUTPUT->render($editcontrols);
|
||||
}
|
||||
|
||||
$reportparams = ['contextid' => $context->id, 'showall' => $showall];
|
||||
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, $reportparams);
|
||||
$report = system_report_factory::create(cohorts::class, $context, '', '', 0, ['showall' => $showall]);
|
||||
|
||||
// Check if it needs to search by name.
|
||||
if (!empty($searchquery)) {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,10 +1,10 @@
|
||||
define("core_course/resetcourse",["exports","core/notification","core/prefetch","core/str"],(function(_exports,_notification,_prefetch,_str){var obj;
|
||||
define("core_course/resetcourse",["exports","core/notification","core/prefetch","core/str","core_form/changechecker"],(function(_exports,_notification,_prefetch,_str,FormChangeChecker){var obj;
|
||||
/**
|
||||
* Javascript module for resetting a course
|
||||
*
|
||||
* @module core_course/resetcourse
|
||||
* @copyright 2024 Sara Arjona <sara@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj};const selectors_resetCoursetButton='[data-action="resetcourse"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("core",["resetcourseconfirm","resetcoursewarning","resetcourse"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const resetButton=event.target.closest(selectors_resetCoursetButton);resetButton&&(event.preventDefault(),resetCourseConfirm(resetButton))}))},resetCourseConfirm=async resetButton=>{const courseName=resetButton.dataset.coursename;_notification.default.deleteCancelPromise((0,_str.getString)("resetcourseconfirm"),(0,_str.getString)("resetcoursewarning","core",courseName),(0,_str.getString)("resetcourse")).then((()=>{resetButton.closest("form").submit()})).catch((()=>{}))}}));
|
||||
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_notification=(obj=_notification)&&obj.__esModule?obj:{default:obj},FormChangeChecker=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FormChangeChecker);const selectors_resetCoursetButton='[data-action="resetcourse"]';_exports.init=()=>{(0,_prefetch.prefetchStrings)("core",["resetcourseconfirm","resetcoursewarning","resetcourse"]),registerEventListeners()};const registerEventListeners=()=>{document.addEventListener("click",(event=>{const resetButton=event.target.closest(selectors_resetCoursetButton);resetButton&&(event.preventDefault(),resetCourseConfirm(resetButton))}))},resetCourseConfirm=async resetButton=>{const courseName=resetButton.dataset.coursename;_notification.default.deleteCancelPromise((0,_str.getString)("resetcourseconfirm"),(0,_str.getString)("resetcoursewarning","core",courseName),(0,_str.getString)("resetcourse")).then((()=>{FormChangeChecker.markFormSubmitted(resetButton.closest("form")),resetButton.closest("form").submit()})).catch((()=>{}))}}));
|
||||
|
||||
//# sourceMappingURL=resetcourse.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"resetcourse.min.js","sources":["../src/resetcourse.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript module for resetting a course\n *\n * @module core_course/resetcourse\n * @copyright 2024 Sara Arjona <sara@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\n\nconst selectors = {\n resetCoursetButton: '[data-action=\"resetcourse\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n prefetchStrings('core', [\n 'resetcourseconfirm',\n 'resetcoursewarning',\n 'resetcourse',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register events for course reset button.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const resetButton = event.target.closest(selectors.resetCoursetButton);\n if (resetButton) {\n event.preventDefault();\n resetCourseConfirm(resetButton);\n }\n });\n};\n\n/**\n * Show the confirmation modal to reset the course.\n *\n * @param {HTMLElement} resetButton the element to delete.\n *\n */\nconst resetCourseConfirm = async(resetButton) => {\n const courseName = resetButton.dataset.coursename;\n\n Notification.deleteCancelPromise(\n getString('resetcourseconfirm'),\n getString('resetcoursewarning', 'core', courseName),\n getString('resetcourse'),\n ).then(() => {\n resetButton.closest('form').submit();\n return;\n }).catch(() => {\n return;\n });\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","resetButton","target","closest","preventDefault","resetCourseConfirm","async","courseName","dataset","coursename","deleteCancelPromise","then","submit","catch"],"mappings":";;;;;;;4JA2BMA,6BACkB,4CAMJ,mCACA,OAAQ,CACpB,qBACA,qBACA,gBAGJC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,YAAcD,MAAME,OAAOC,QAAQP,8BACrCK,cACAD,MAAMI,iBACNC,mBAAmBJ,kBAWzBI,mBAAqBC,MAAAA,oBACjBC,WAAaN,YAAYO,QAAQC,iCAE1BC,qBACT,kBAAU,uBACV,kBAAU,qBAAsB,OAAQH,aACxC,kBAAU,gBACZI,MAAK,KACHV,YAAYE,QAAQ,QAAQS,YAE7BC,OAAM"}
|
||||
{"version":3,"file":"resetcourse.min.js","sources":["../src/resetcourse.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript module for resetting a course\n *\n * @module core_course/resetcourse\n * @copyright 2024 Sara Arjona <sara@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Notification from 'core/notification';\nimport {prefetchStrings} from 'core/prefetch';\nimport {getString} from 'core/str';\nimport * as FormChangeChecker from 'core_form/changechecker';\n\nconst selectors = {\n resetCoursetButton: '[data-action=\"resetcourse\"]',\n};\n\n/**\n * Initialize module\n */\nexport const init = () => {\n prefetchStrings('core', [\n 'resetcourseconfirm',\n 'resetcoursewarning',\n 'resetcourse',\n ]);\n\n registerEventListeners();\n};\n\n/**\n * Register events for course reset button.\n */\nconst registerEventListeners = () => {\n document.addEventListener('click', (event) => {\n const resetButton = event.target.closest(selectors.resetCoursetButton);\n if (resetButton) {\n event.preventDefault();\n resetCourseConfirm(resetButton);\n }\n });\n};\n\n/**\n * Show the confirmation modal to reset the course.\n *\n * @param {HTMLElement} resetButton the element to delete.\n *\n */\nconst resetCourseConfirm = async(resetButton) => {\n const courseName = resetButton.dataset.coursename;\n\n Notification.deleteCancelPromise(\n getString('resetcourseconfirm'),\n getString('resetcoursewarning', 'core', courseName),\n getString('resetcourse'),\n ).then(() => {\n FormChangeChecker.markFormSubmitted(resetButton.closest('form'));\n resetButton.closest('form').submit();\n return;\n }).catch(() => {\n return;\n });\n};\n"],"names":["selectors","registerEventListeners","document","addEventListener","event","resetButton","target","closest","preventDefault","resetCourseConfirm","async","courseName","dataset","coursename","deleteCancelPromise","then","FormChangeChecker","markFormSubmitted","submit","catch"],"mappings":";;;;;;;imCA4BMA,6BACkB,4CAMJ,mCACA,OAAQ,CACpB,qBACA,qBACA,gBAGJC,gCAMEA,uBAAyB,KAC3BC,SAASC,iBAAiB,SAAUC,cAC1BC,YAAcD,MAAME,OAAOC,QAAQP,8BACrCK,cACAD,MAAMI,iBACNC,mBAAmBJ,kBAWzBI,mBAAqBC,MAAAA,oBACjBC,WAAaN,YAAYO,QAAQC,iCAE1BC,qBACT,kBAAU,uBACV,kBAAU,qBAAsB,OAAQH,aACxC,kBAAU,gBACZI,MAAK,KACHC,kBAAkBC,kBAAkBZ,YAAYE,QAAQ,SACxDF,YAAYE,QAAQ,QAAQW,YAE7BC,OAAM"}
|
||||
@@ -65,7 +65,7 @@ define(
|
||||
const componentActions = [
|
||||
'moveSection', 'moveCm', 'addSection', 'deleteSection', 'cmDelete', 'cmDuplicate', 'sectionHide', 'sectionShow',
|
||||
'cmHide', 'cmShow', 'cmStealth', 'sectionHighlight', 'sectionUnhighlight', 'cmMoveRight', 'cmMoveLeft',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups', 'addModule',
|
||||
'cmNoGroups', 'cmVisibleGroups', 'cmSeparateGroups', 'addModule', 'newModule',
|
||||
];
|
||||
|
||||
// The course reactive instance.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import Notification from 'core/notification';
|
||||
import {prefetchStrings} from 'core/prefetch';
|
||||
import {getString} from 'core/str';
|
||||
import * as FormChangeChecker from 'core_form/changechecker';
|
||||
|
||||
const selectors = {
|
||||
resetCoursetButton: '[data-action="resetcourse"]',
|
||||
@@ -69,6 +70,7 @@ const resetCourseConfirm = async(resetButton) => {
|
||||
getString('resetcoursewarning', 'core', courseName),
|
||||
getString('resetcourse'),
|
||||
).then(() => {
|
||||
FormChangeChecker.markFormSubmitted(resetButton.closest('form'));
|
||||
resetButton.closest('form').submit();
|
||||
return;
|
||||
}).catch(() => {
|
||||
|
||||
+12
-6
@@ -4316,22 +4316,28 @@ class core_course_external extends external_api {
|
||||
* @param int $groupid Group id from which the users will be obtained
|
||||
* @param bool $onlyactive Whether to return only the active enrolled users or all enrolled users in the course.
|
||||
* @return array List of users
|
||||
* @throws invalid_parameter_exception
|
||||
*/
|
||||
public static function get_enrolled_users_by_cmid(int $cmid, int $groupid = 0, bool $onlyactive = false) {
|
||||
global $PAGE;
|
||||
global $PAGE;
|
||||
|
||||
$warnings = [];
|
||||
|
||||
self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
[
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
|
||||
'cmid' => $cmid,
|
||||
'groupid' => $groupid,
|
||||
'onlyactive' => $onlyactive,
|
||||
]);
|
||||
|
||||
list($course, $cm) = get_course_and_cm_from_cmid($cmid);
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
self::validate_context($coursecontext);
|
||||
|
||||
course_require_view_participants($coursecontext);
|
||||
|
||||
$enrolledusers = get_enrolled_users($coursecontext, '', $groupid, 'u.*', null, 0, 0, $onlyactive);
|
||||
|
||||
$users = array_map(function ($user) use ($PAGE) {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -469,6 +469,17 @@ export default class extends BaseComponent {
|
||||
this.reactive.dispatch('addModule', target.dataset.modname, target.dataset.sectionnum, target.dataset.beforemod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a new create subsection request.
|
||||
*
|
||||
* @param {Element} target the dispatch action element
|
||||
* @param {Event} event the triggered event
|
||||
*/
|
||||
async _requestNewModule(target, event) {
|
||||
event.preventDefault();
|
||||
this.reactive.dispatch('newModule', target.dataset.modname, target.dataset.sectionid, target.dataset.beforemod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a delete section request.
|
||||
*
|
||||
|
||||
@@ -75,21 +75,46 @@ export default class {
|
||||
* @param {number} targetSectionNum target section number
|
||||
* @param {number} targetCmId optional target cm id
|
||||
*/
|
||||
async _callAddModuleWebservice(courseId, modName, targetSectionNum, targetCmId) {
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionnum: targetSectionNum,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_create_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
async _callAddModuleWebservice(courseId, modName, targetSectionNum, targetCmId) {
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionnum: targetSectionNum,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_create_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to call core_courseformat_new_module webservice.
|
||||
*
|
||||
* @method _callEditWebservice
|
||||
* @param {number} courseId
|
||||
* @param {string} modName module name
|
||||
* @param {number} targetSectionId target section number
|
||||
* @param {number} targetCmId optional target cm id
|
||||
*/
|
||||
async _callNewModuleWebservice(courseId, modName, targetSectionId, targetCmId) {
|
||||
const args = {
|
||||
courseid: courseId,
|
||||
modname: modName,
|
||||
targetsectionid: targetSectionId,
|
||||
};
|
||||
if (targetCmId) {
|
||||
args.targetcmid = targetCmId;
|
||||
}
|
||||
let ajaxresult = await ajax.call([{
|
||||
methodname: 'core_courseformat_new_module',
|
||||
args,
|
||||
}])[0];
|
||||
return JSON.parse(ajaxresult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a basic section state action.
|
||||
@@ -439,6 +464,29 @@ export default class {
|
||||
stateManager.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new module to a specific course section.
|
||||
*
|
||||
* @param {StateManager} stateManager the current state manager
|
||||
* @param {string} modName the modulename to add
|
||||
* @param {number} targetSectionId the target section id
|
||||
* @param {number} targetCmId optional the target cm id
|
||||
*/
|
||||
async newModule(stateManager, modName, targetSectionId, targetCmId) {
|
||||
if (!modName) {
|
||||
throw new Error(`Mutation newModule requires moduleName`);
|
||||
}
|
||||
if (!targetSectionId) {
|
||||
throw new Error(`Mutation newModule requires targetSectionId`);
|
||||
}
|
||||
if (!targetCmId) {
|
||||
targetCmId = 0;
|
||||
}
|
||||
const course = stateManager.get('course');
|
||||
const updates = await this._callNewModuleWebservice(course.id, modName, targetSectionId, targetCmId);
|
||||
stateManager.processUpdates(updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark or unmark course modules as dragging.
|
||||
*
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
<?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/>.
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_value;
|
||||
use moodle_exception;
|
||||
use coding_exception;
|
||||
use context_course;
|
||||
use core_courseformat\base as course_format;
|
||||
|
||||
/**
|
||||
* External service to create a new module instance in the course.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2024 Mikel Martín <mikel@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class new_module extends external_api {
|
||||
|
||||
/**
|
||||
* Webservice parameters.
|
||||
*
|
||||
* @return external_function_parameters
|
||||
*/
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters(
|
||||
[
|
||||
'courseid' => new external_value(PARAM_INT, 'course id', VALUE_REQUIRED),
|
||||
'modname' => new external_value(PARAM_ALPHANUMEXT, 'module name', VALUE_REQUIRED),
|
||||
'targetsectionid' => new external_value(PARAM_INT, 'target section id', VALUE_REQUIRED, null),
|
||||
'targetcmid' => new external_value(PARAM_INT, 'Optional target cm id', VALUE_DEFAULT, null),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This webservice will execute the create_module action from the course editor.
|
||||
*
|
||||
* The action will register in a {@see \core_courseformat\stateupdates} all the affected
|
||||
* sections, cms and course attribute. This object (in JSON) will be sent back to the
|
||||
* frontend editor to refresh the updated state elements.
|
||||
*
|
||||
* By default, {@see \core_courseformat\stateupdates} will register only create, delete and update events
|
||||
* on cms, sections and the general course data. However, if some plugin needs adhoc messages for
|
||||
* its own mutation module, extend this class in format_XXX\course.
|
||||
*
|
||||
* @param int $courseid the course id
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid the target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
* @return string Course state in JSON
|
||||
*/
|
||||
public static function execute(
|
||||
int $courseid,
|
||||
string $modname,
|
||||
int $targetsectionid,
|
||||
?int $targetcmid = null
|
||||
): string {
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
|
||||
[
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionid' => $targetsectionid,
|
||||
'targetcmid' => $targetcmid,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'courseid' => $courseid,
|
||||
'modname' => $modname,
|
||||
'targetsectionid' => $targetsectionid,
|
||||
'targetcmid' => $targetcmid,
|
||||
]);
|
||||
|
||||
self::validate_context(context_course::instance($courseid));
|
||||
|
||||
// Plugin needs to support quick creation and the course format needs to support components.
|
||||
// Formats using YUI modules should not be able to quick-create because the front end cannot react to the change.
|
||||
if (!plugin_supports('mod', $modname, FEATURE_QUICKCREATE) || !course_get_format($courseid)->supports_components()) {
|
||||
throw new moodle_exception("Module $modname does not support quick creation");
|
||||
}
|
||||
|
||||
$courseformat = course_get_format($courseid);
|
||||
|
||||
// Create a course changes tracker object.
|
||||
$defaultupdatesclass = 'core_courseformat\\stateupdates';
|
||||
$updatesclass = 'format_' . $courseformat->get_format() . '\\courseformat\\stateupdates';
|
||||
if (!class_exists($updatesclass)) {
|
||||
$updatesclass = $defaultupdatesclass;
|
||||
}
|
||||
$updates = new $updatesclass($courseformat);
|
||||
|
||||
if (!is_a($updates, $defaultupdatesclass)) {
|
||||
throw new coding_exception("The \"$updatesclass\" class must extend \"$defaultupdatesclass\"");
|
||||
}
|
||||
|
||||
// Get the actions class from the course format.
|
||||
$actionsclass = 'format_'. $courseformat->get_format().'\\courseformat\\stateactions';
|
||||
if (!class_exists($actionsclass)) {
|
||||
$actionsclass = 'core_courseformat\\stateactions';
|
||||
}
|
||||
/** @var \core_courseformat\stateactions $actions */
|
||||
$actions = new $actionsclass();
|
||||
|
||||
$action = 'new_module';
|
||||
if (!is_callable([$actions, $action])) {
|
||||
throw new moodle_exception("Invalid course state action $action in ".get_class($actions));
|
||||
}
|
||||
|
||||
$course = $courseformat->get_course();
|
||||
|
||||
// Execute the action.
|
||||
$actions->$action($updates, $course, $modname, $targetsectionid, $targetcmid);
|
||||
|
||||
// Any state action mark the state cache as dirty.
|
||||
course_format::session_cache_reset($course);
|
||||
|
||||
return json_encode($updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Webservice returns.
|
||||
*
|
||||
* @return external_value
|
||||
*/
|
||||
public static function execute_returns(): external_value {
|
||||
return new external_value(PARAM_RAW, 'Encoded course update JSON');
|
||||
}
|
||||
}
|
||||
@@ -366,6 +366,9 @@ class stateactions {
|
||||
// We need to get the latest modinfo on each iteration because the section numbers change.
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$section = $modinfo->get_section_info_by_id($sectionid, MUST_EXIST);
|
||||
if (!course_can_delete_section($course, $section)) {
|
||||
continue;
|
||||
}
|
||||
// Send all activity deletions.
|
||||
if (!empty($modinfo->sections[$section->section])) {
|
||||
foreach ($modinfo->sections[$section->section] as $modnumber) {
|
||||
@@ -1211,4 +1214,38 @@ class stateactions {
|
||||
// Adding module affects section structure, and if the module has a delegated section even the course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new course module.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function new_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
int $targetsectionid,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/modlib.php');
|
||||
|
||||
$coursecontext = context_course::instance($course->id);
|
||||
require_capability('moodle/course:update', $coursecontext);
|
||||
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$section = $modinfo->get_section_info_by_id($targetsectionid, MUST_EXIST);
|
||||
|
||||
// Method "can_add_moduleinfo" called in "prepare_new_moduleinfo_data" will handle the capability checks.
|
||||
[, , , , $moduleinfo] = prepare_new_moduleinfo_data($course, $modname, $section->sectionnum);
|
||||
$moduleinfo->beforemod = $targetcmid;
|
||||
create_module((object) $moduleinfo);
|
||||
|
||||
// Adding module affects section structure, and if the module has a delegated section even the course structure.
|
||||
$this->course_state($updates, $course);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{{#str}} nobulkaction, core_courseformat {{/str}}
|
||||
{{/hasactions}}
|
||||
{{#hasactions}}
|
||||
<ul class="actions nav" data-for="bulkactions">
|
||||
<ul class="actions nav flex-nowrap" data-for="bulkactions">
|
||||
{{#actions}}
|
||||
<li class="nav-item">
|
||||
<button
|
||||
|
||||
@@ -566,6 +566,45 @@ final class base_test extends advanced_testcase {
|
||||
$this->assertEquals($originalmodcount, $newmodcount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicate_section() with delegated section
|
||||
* @covers ::duplicate_section
|
||||
*/
|
||||
public function test_duplicate_section_with_delegated_sections(): void {
|
||||
global $DB;
|
||||
|
||||
$this->setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
// Add subsection.
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin('subsection', 1);
|
||||
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 1]);
|
||||
$subsection1 = $this->getDataGenerator()->create_module(
|
||||
'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection1']);
|
||||
$subsection2 = $this->getDataGenerator()->create_module(
|
||||
'subsection', ['course' => $course, 'section' => 1, 'name' => 'subsection2']);
|
||||
$format = course_get_format($course);
|
||||
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$sectioninfo = $modinfo->get_section_info(1, MUST_EXIST);
|
||||
$originalsectioncount = $DB->count_records('course_sections', ['course' => $course->id]);
|
||||
$this->assertEquals(4, $originalsectioncount);
|
||||
|
||||
$originalsection = $DB->get_record('course_sections',
|
||||
['course' => $course->id, 'section' => 0],
|
||||
'*',
|
||||
MUST_EXIST);
|
||||
$newsection = $format->duplicate_section($sectioninfo);
|
||||
foreach ($originalsection as $prop => $value) {
|
||||
if ($prop == 'id' || $prop == 'sequence' || $prop == 'section' || $prop == 'timemodified') {
|
||||
continue;
|
||||
}
|
||||
$this->assertEquals($value, $newsection->$prop);
|
||||
}
|
||||
$sectioncount = $DB->count_records('course_sections', ['course' => $course->id]);
|
||||
$this->assertEquals(7, $sectioncount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for the default delete format data behaviour.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_courseformat\stateactions;
|
||||
use core_courseformat\stateupdates;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Tests for the delete section test class.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2025 Laurent David <laurent.david@moodle.com>
|
||||
* @category test
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \core_courseformat\stateactions
|
||||
*/
|
||||
final class delete_section_test extends \externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setupBeforeClass(): void { // phpcs:ignore
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittestdelete.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the section_delete action.
|
||||
*
|
||||
* @covers ::section_delete
|
||||
* @dataProvider section_delete_provider
|
||||
* @param int $sectionum
|
||||
* @param string $format
|
||||
* @param array $formatoptions
|
||||
* @param int $expectedsectionum
|
||||
*
|
||||
* @throws \moodle_exception
|
||||
*/
|
||||
public function test_delete_section(int $sectionum, string $format, array $formatoptions, int $expectedsectionum): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course =
|
||||
$this->getDataGenerator()->create_course(array_merge(
|
||||
['numsections' => $sectionum, 'format' => $format],
|
||||
$formatoptions,
|
||||
));
|
||||
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
|
||||
// Execute the method.
|
||||
$courseformat = course_get_format($course->id);
|
||||
$updates = new stateupdates($courseformat);
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
$sectionsid = array_map(function ($section) {
|
||||
return $section->id;
|
||||
}, $sections);
|
||||
$actions = new stateactions();
|
||||
$this->setUser($teacher);
|
||||
$actions->section_delete(
|
||||
$updates,
|
||||
$course,
|
||||
$sectionsid
|
||||
);
|
||||
// Check result.
|
||||
$modinfo = get_fast_modinfo($course);
|
||||
$sections = $modinfo->get_section_info_all();
|
||||
$this->assertCount($expectedsectionum, $sections);
|
||||
if ($format == 'theunittestdelete') {
|
||||
$this->assertDebuggingCalled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for the test_delete_section method.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function section_delete_provider(): array {
|
||||
return [
|
||||
'format topic' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'topics',
|
||||
'formatoptions' => [],
|
||||
'expectedsectionum' => 1,
|
||||
],
|
||||
'format theunittestdelete' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'theunittestdelete',
|
||||
'formatoptions' => [],
|
||||
'expectedsectionum' => 5,
|
||||
],
|
||||
'format theunittestdelete can delete' => [
|
||||
'sectionum' => 4,
|
||||
'format' => 'theunittestdelete',
|
||||
'formatoptions' => ['can_delete_sections' => true],
|
||||
'expectedsectionum' => 1,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<?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/>.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_courseformat\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use moodle_exception;
|
||||
use stdClass;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Tests for the new_module class.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @category test
|
||||
* @copyright 2024 Mikel Martín <mikel@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \core_courseformat\external\new_module
|
||||
*/
|
||||
final class new_module_test extends \externallib_advanced_testcase {
|
||||
|
||||
/**
|
||||
* Setup to ensure that fixtures are loaded.
|
||||
*/
|
||||
public static function setupBeforeClass(): void { // phpcs:ignore
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest.php');
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_output_course_format_state.php');
|
||||
require_once($CFG->dirroot . '/course/format/tests/fixtures/format_theunittest_stateactions.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with an activity.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$activity = $this->getDataGenerator()->create_module('book', ['course' => $course->id]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Execute course action.
|
||||
$results = json_decode(
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id, (int)$activity->id),
|
||||
);
|
||||
|
||||
// Check result.
|
||||
$cmupdate = $this->find_update_by_fieldname($results, 'put', 'cm', get_string('quickcreatename', 'mod_' . $modname));
|
||||
$this->assertNotEmpty($cmupdate);
|
||||
$this->assertEquals($modname, $cmupdate->fields->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->fields->sectionid);
|
||||
$this->assertEquals(1, $cmupdate->fields->sectionnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action with a format override.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute_with_format_override(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin('subsection', 1);
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course(['format' => 'theunittest', 'numsections' => 1, 'initsections' => 1]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Execute course action.
|
||||
$modname = 'subsection';
|
||||
$results = json_decode(
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id),
|
||||
);
|
||||
|
||||
// Some course formats doesn't have the renderer file, so a debugging message will be displayed.
|
||||
$this->assertDebuggingCalled();
|
||||
|
||||
// Check result.
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the webservice can execute the new_module action with an invalid module.
|
||||
*
|
||||
* @covers ::execute
|
||||
*/
|
||||
public function test_execute_with_invalid_module(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Create a course.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$targetsection = get_fast_modinfo($course->id)->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Expect exception. Book module doesn't support quickcreate feature.
|
||||
$this->expectException(moodle_exception::class);
|
||||
|
||||
// Execute course action.
|
||||
$modname = 'book';
|
||||
new_module::execute((int)$course->id, $modname, (int)$targetsection->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to find a specific update in the updadelist.
|
||||
*
|
||||
* @param array $updatelist the update list
|
||||
* @param string $action the action to find
|
||||
* @param string $name the element name to find
|
||||
* @param string $fieldname the element identifiername
|
||||
* @return stdClass|null the object found, if any.
|
||||
*/
|
||||
private function find_update_by_fieldname(
|
||||
array $updatelist,
|
||||
string $action,
|
||||
string $name,
|
||||
string $fieldname,
|
||||
|
||||
): ?stdClass {
|
||||
foreach ($updatelist as $update) {
|
||||
if ($update->action != $action || $update->name != $name) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($update->fields->name)) {
|
||||
continue;
|
||||
}
|
||||
if ($update->fields->name == $fieldname) {
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -54,10 +54,28 @@ class stateactions extends core_actions {
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int $targetsectionnum target section number
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function create_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
int $targetsectionnum,
|
||||
?int $targetcmid = null
|
||||
): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternative new_module state action for testing.
|
||||
*
|
||||
* @param stateupdates $updates the affected course elements track
|
||||
* @param stdClass $course the course object
|
||||
* @param string $modname the module name
|
||||
* @param int $targetsectionid target section id
|
||||
* @param int|null $targetcmid optional target cm id
|
||||
*/
|
||||
public function new_module(
|
||||
stateupdates $updates,
|
||||
stdClass $course,
|
||||
string $modname,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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/>.
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
require_once(__DIR__ . '/format_theunittest.php');
|
||||
|
||||
/**
|
||||
* Fixture for fake course format testing course format API.
|
||||
*
|
||||
* @package core_courseformat
|
||||
* @copyright 2025 Laurent David <laurent.david@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class format_theunittestdelete extends format_theunittest {
|
||||
|
||||
/**
|
||||
* Definitions of the additional options that format uses
|
||||
*
|
||||
* @param bool $foreditform
|
||||
* @return array of options
|
||||
*/
|
||||
public function course_format_options($foreditform = false) {
|
||||
static $courseformatoptions = false;
|
||||
if ($courseformatoptions === false) {
|
||||
$courseformatoptions = parent::course_format_options(true);
|
||||
$courseformatoptionsadditional = [
|
||||
'can_delete_sections' => [
|
||||
'default' => false,
|
||||
'type' => PARAM_BOOL,
|
||||
],
|
||||
];
|
||||
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsadditional);
|
||||
}
|
||||
return $courseformatoptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this format allows to delete sections
|
||||
*
|
||||
* Here for test purpose we just can delete one section every two sections
|
||||
*
|
||||
* Do not call this function directly, instead use course_can_delete_section()
|
||||
*
|
||||
* @param int|stdClass|section_info $section
|
||||
* @return bool
|
||||
*/
|
||||
public function can_delete_section($section) {
|
||||
return $this->get_format_options()['can_delete_sections'] ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1712,8 +1712,6 @@ final class stateactions_test extends \advanced_testcase {
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
@@ -1734,11 +1732,39 @@ final class stateactions_test extends \advanced_testcase {
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for create_module public method with no capabilities.
|
||||
*
|
||||
* @covers ::create_module
|
||||
*/
|
||||
public function test_create_module_no_capabilities(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Change to a user without permission.
|
||||
$this->setUser($student);
|
||||
|
||||
// Validate that the method throws an exception.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
|
||||
$this->expectException(moodle_exception::class);
|
||||
$actions->create_module($updates, $course, $modname, $targetsection->sectionnum);
|
||||
}
|
||||
@@ -1792,4 +1818,125 @@ final class stateactions_test extends \advanced_testcase {
|
||||
$modinfo->get_section_info(1)->sequence
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Execute given method.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id);
|
||||
|
||||
// Validate cm was created and updates were generated.
|
||||
$results = $this->summarize_updates($updates);
|
||||
$cmupdate = reset($results['put']['cm']);
|
||||
$this->assertCount(1, $courseformat->get_modinfo()->get_cms());
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method with no capabilities.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module_no_capabilities(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with 1 section and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertEmpty($courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Change to a user without permission.
|
||||
$this->setUser($student);
|
||||
|
||||
// Validate that the method throws an exception.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
|
||||
$this->expectException(moodle_exception::class);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for new_module public method with targetcmid parameter.
|
||||
*
|
||||
* @covers ::new_module
|
||||
*/
|
||||
public function test_new_module_with_targetcmid(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$modname = 'subsection';
|
||||
$manager = \core_plugin_manager::resolve_plugininfo_class('mod');
|
||||
$manager::enable_plugin($modname, 1);
|
||||
|
||||
// Create a course with 1 section, 2 modules (forum and page) and 1 student.
|
||||
$course = $this->getDataGenerator()->create_course(['numsections' => 1]);
|
||||
$forum = $this->getDataGenerator()->create_module('forum', ['course' => $course], ['section' => 1]);
|
||||
$page = $this->getDataGenerator()->create_module('page', ['course' => $course], ['section' => 1]);
|
||||
$student = $this->getDataGenerator()->create_user();
|
||||
$this->getDataGenerator()->enrol_user($student->id, $course->id, 'student');
|
||||
$courseformat = course_get_format($course->id);
|
||||
$targetsection = $courseformat->get_modinfo()->get_section_info(1);
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Sanity check.
|
||||
$this->assertCount(2, $courseformat->get_modinfo()->get_cms());
|
||||
|
||||
// Execute given method.
|
||||
$actions = new stateactions();
|
||||
$updates = new stateupdates($courseformat);
|
||||
$actions->new_module($updates, $course, $modname, $targetsection->id, $page->cmid);
|
||||
|
||||
$modinfo = $courseformat->get_modinfo();
|
||||
$cms = $modinfo->get_cms();
|
||||
$results = $this->summarize_updates($updates);
|
||||
$cmupdate = reset($results['put']['cm']);
|
||||
|
||||
// Validate updates were generated.
|
||||
$this->assertEquals($modname, $cmupdate->module);
|
||||
$this->assertEquals($targetsection->id, $cmupdate->sectionid);
|
||||
$this->assertEquals(get_string('quickcreatename', 'mod_' . $modname), $cmupdate->name);
|
||||
|
||||
// Validate that the new module was created between both modules.
|
||||
$this->assertCount(3, $cms);
|
||||
$this->assertArrayHasKey($cmupdate->id, $cms);
|
||||
$this->assertEquals(
|
||||
implode(',', [$forum->cmid, $cmupdate->id, $page->cmid]),
|
||||
$modinfo->get_section_info(1)->sequence
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4067,6 +4067,12 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
|
||||
$this->assertEquals(2, count($users['users']));
|
||||
$this->assertEquals($expectedusers, $users);
|
||||
|
||||
// Prohibit the capability for viewing course participants.
|
||||
$this->unassignUserCapability('moodle/course:viewparticipants', null, null, $course1->id);
|
||||
$this->expectException(required_capability_exception::class);
|
||||
$this->expectExceptionMessage('Sorry, but you do not currently have permissions to do that (View participants)');
|
||||
core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# core_files (subsystem) Upgrade notes
|
||||
|
||||
## 4.5.4
|
||||
|
||||
### Added
|
||||
|
||||
- A new function `file_clear_draft_area()` has been added to delete the files in a draft area.
|
||||
|
||||
For more information see [MDL-72050](https://tracker.moodle.org/browse/MDL-72050)
|
||||
|
||||
## 4.5
|
||||
|
||||
### Added
|
||||
|
||||
+10
-1
@@ -84,11 +84,20 @@ function filter_tex_sanitize_formula(string $texexp): string {
|
||||
'\afterassignment', '\expandafter', '\noexpand', '\special',
|
||||
'\let', '\futurelet', '\else', '\fi', '\chardef', '\makeatletter', '\afterground',
|
||||
'\noexpand', '\line', '\mathcode', '\item', '\section', '\mbox', '\declarerobustcommand',
|
||||
'\ExplSyntaxOn', '\pdffiledump',
|
||||
'\ExplSyntaxOn', '\pdffiledump', '\mathtex',
|
||||
];
|
||||
|
||||
$allowlist = ['inputenc'];
|
||||
|
||||
// Add encoded backslash (\) versions of backslashed items to deny list.
|
||||
$encodedslashdenylist = array_map(function($value) {
|
||||
$encoded = str_replace('\\', '\', $value);
|
||||
// Return an encoded slash version if a slash is found, otherwise null so we can filter it off.
|
||||
return $encoded != $value ? $encoded : null;
|
||||
}, $denylist);
|
||||
$encodedslashdenylist = array_filter($encodedslashdenylist);
|
||||
$denylist = array_merge($denylist, $encodedslashdenylist);
|
||||
|
||||
// Prepare the denylist for regular expression.
|
||||
$denylist = array_map(function($value){
|
||||
return '/' . preg_quote($value, '/') . '/i';
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ class fetch extends external_api {
|
||||
$definitionid,
|
||||
'remark',
|
||||
$filling['remark'],
|
||||
(int) $filling['remarkformat']
|
||||
(int) FORMAT_HTML
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -857,16 +857,19 @@ class gradingform_guide_instance extends gradingform_instance {
|
||||
parent::update($data);
|
||||
|
||||
foreach ($data['criteria'] as $criterionid => $record) {
|
||||
// Hardcoding/defaulting to html format for new/existing record
|
||||
$record['remarkformat'] = FORMAT_HTML;
|
||||
|
||||
if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
|
||||
$newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid,
|
||||
'score' => $record['score'], 'remarkformat' => FORMAT_MOODLE);
|
||||
'score' => $record['score'], 'remarkformat' => $record['remarkformat']);
|
||||
if (isset($record['remark'])) {
|
||||
$newrecord['remark'] = $record['remark'];
|
||||
}
|
||||
$DB->insert_record('gradingform_guide_fillings', $newrecord);
|
||||
} else {
|
||||
$newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
|
||||
foreach (array('score', 'remark'/*, 'remarkformat' TODO */) as $key) {
|
||||
foreach (array('score', 'remark', 'remarkformat') as $key) {
|
||||
if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
|
||||
$newrecord[$key] = $record[$key];
|
||||
}
|
||||
|
||||
+1
-1
@@ -196,7 +196,7 @@ class fetch extends external_api {
|
||||
$definitionid,
|
||||
'remark',
|
||||
$filling['remark'],
|
||||
(int) $filling['remarkformat']
|
||||
(int) FORMAT_HTML
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -863,16 +863,19 @@ class gradingform_rubric_instance extends gradingform_instance {
|
||||
$currentgrade = $this->get_rubric_filling();
|
||||
parent::update($data);
|
||||
foreach ($data['criteria'] as $criterionid => $record) {
|
||||
// Hardcoding/defaulting to html format for new/existing record
|
||||
$record['remarkformat'] = FORMAT_HTML;
|
||||
|
||||
if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
|
||||
$newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid,
|
||||
'levelid' => $record['levelid'], 'remarkformat' => FORMAT_MOODLE);
|
||||
'levelid' => $record['levelid'], 'remarkformat' => $record['remarkformat']);
|
||||
if (isset($record['remark'])) {
|
||||
$newrecord['remark'] = $record['remark'];
|
||||
}
|
||||
$DB->insert_record('gradingform_rubric_fillings', $newrecord);
|
||||
} else {
|
||||
$newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
|
||||
foreach (array('levelid', 'remark'/*, 'remarkformat' */) as $key) {
|
||||
foreach (array('levelid', 'remark', 'remarkformat') as $key) {
|
||||
// TODO MDL-31235 format is not supported yet
|
||||
if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
|
||||
$newrecord[$key] = $record[$key];
|
||||
|
||||
@@ -77,6 +77,7 @@ class grade_import_form extends moodleform {
|
||||
$mform->addElement('text', 'iprestriction', get_string('keyiprestriction', 'userkey'), array('size'=>80));
|
||||
$mform->addHelpButton('iprestriction', 'keyiprestriction', 'userkey');
|
||||
$mform->setDefault('iprestriction', getremoteaddr()); // own IP - just in case somebody does not know what user key is
|
||||
$mform->setType('iprestriction', PARAM_RAW_TRIMMED);
|
||||
|
||||
$mform->addElement('date_time_selector', 'validuntil', get_string('keyvaliduntil', 'userkey'), array('optional'=>true));
|
||||
$mform->addHelpButton('validuntil', 'keyvaliduntil', 'userkey');
|
||||
|
||||
@@ -107,6 +107,7 @@ switch ($itemtype) {
|
||||
// display the user select zero state.
|
||||
if (is_null($itemid) || !array_key_exists($itemid, grade_report::get_gradable_users($courseid, $currentgroup))) {
|
||||
$itemtype = 'user_select';
|
||||
unset($SESSION->gradereport_singleview["useritem-{$context->id}"]);
|
||||
}
|
||||
break;
|
||||
case 'grade_select':
|
||||
@@ -129,6 +130,7 @@ switch ($itemtype) {
|
||||
// The item id (grade item id) cannot be defined, display the grade select zero state.
|
||||
if (is_null($itemid) || !array_key_exists($itemid, $gtree->get_items())) {
|
||||
$itemtype = 'grade_select';
|
||||
unset($SESSION->gradereport_singleview["gradeitem-{$context->id}"]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,6 +237,9 @@ Feature: We can use Single view
|
||||
When I navigate to "View > Single view" in the course gradebook
|
||||
Then I should see "Search for a user to view all their grades" in the "region-main" "region"
|
||||
And I should not see "Gronya,Beecham" in the "region-main" "region"
|
||||
# edge case coverage: to ensure session variable not causing error upon second visit
|
||||
And I reload the page
|
||||
And I should see "Search for a user to view all their grades" in the "region-main" "region"
|
||||
|
||||
Scenario: Teacher does not see his last viewed user report if that user is no longer enrolled in the course.
|
||||
Given I navigate to "View > Single view" in the course gradebook
|
||||
@@ -247,6 +250,9 @@ Feature: We can use Single view
|
||||
When I am on the "Course 1" "grades > Single view > View" page
|
||||
Then I should see "Search for a user to view all their grades" in the "region-main" "region"
|
||||
And I should not see "Gronya,Beecham" in the "region-main" "region"
|
||||
# edge case coverage: to ensure session variable not causing error upon second visit
|
||||
And I reload the page
|
||||
And I should see "Search for a user to view all their grades" in the "region-main" "region"
|
||||
|
||||
Scenario: Teacher does not see his last viewed grade item report if the item no longer exists in the course.
|
||||
Given I navigate to "View > Single view" in the course gradebook
|
||||
@@ -258,3 +264,6 @@ Feature: We can use Single view
|
||||
When I navigate to "View > Single view" in the course gradebook
|
||||
Then I should see "Select a grade item above" in the "region-main" "region"
|
||||
And I should not see "Test grade item" in the "region-main" "region"
|
||||
# edge case coverage: to ensure session variable not causing error upon second visit
|
||||
And I reload the page
|
||||
And I should see "Select a grade item above" in the "region-main" "region"
|
||||
|
||||
@@ -71,7 +71,7 @@ $string['pathserrcreatedataroot'] = 'Instalatzaileak ezin du datu-direktorioa ({
|
||||
$string['pathshead'] = 'Egiaztatu bideak';
|
||||
$string['pathsrodataroot'] = 'Dataroot direktorioa ez da idazteko modukoa.';
|
||||
$string['pathsroparentdataroot'] = 'Goragoko direktorioa ({$a->parent}) ez da idazteko modukoa. Instalatzaileak ezin du datu-direktorioa ({$a->dataroot}) sortu.';
|
||||
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodleren kudeatze-orrien lehenetsitako kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin direktorioa berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodleko admin estekak konponduko du.';
|
||||
$string['pathssubadmindir'] = 'Web ostalari gutxi batzuk /admin URL berezi gisa erabiltzen dute kontrol-panel edo antzekora sarbidea emateko. Zoritxarrez, honek Moodleko kudeatze-orrien lehenetsitako kokapenarekin gatazka sortzen du. Hau konpondu dezakezu zure instalazioko admin direktorioa berrizendatuz, eta izen berria hemen sartuta. Adibidez <em>moodleadmin</em>. Honek Moodleko admin estekak konponduko du.';
|
||||
$string['pathssubdataroot'] = '<p>Moodlek erabiltzaileek igotako fitxategien edukiak bilduko dituen direktorio bat.</p>
|
||||
<p>Direktorio honetan web-zerbitzariaren erabiltzaileak irakurtzeko eta idazteko baimena izan beharko ditu (normalean \'www-data\', \'nobody\', edo \'apache\').</p>
|
||||
<p>Ez litzateke web bidez eskuragarri egon beharko.</p>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['language'] = 'Hizkuntza';
|
||||
$string['moodlelogo'] = 'Moodleren logoa';
|
||||
$string['moodlelogo'] = 'Moodleko logoa';
|
||||
$string['next'] = 'Hurrengoa';
|
||||
$string['previous'] = 'Aurrekoa';
|
||||
$string['reload'] = 'Berriz kargatu';
|
||||
|
||||
@@ -84,10 +84,10 @@ $string['phpversionhelp'] = '<p>Moodleには少なくとも5.6.5または7.1のP
|
||||
<p>現在、あなたはバージョン {$a} を動作させています。</p>
|
||||
<p>PHPをアップグレードするか新しいバージョンのPHPがインストールされているホストに移動する必要があります。</p>';
|
||||
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
$string['welcomep20'] = 'インストールが正常に完了して、あなたのコンピュータで <strong>{$a->packname} {$a->packversion}</strong> パッケージが起動されたため、このページが表示されています。おめでとうございます!';
|
||||
$string['welcomep30'] = 'このリリース <strong>{$a->installername}</strong> には<strong>Moodle</strong>で環境を作成するアプリケーションが含まれています。すなわち:';
|
||||
$string['welcomep40'] = 'パッケージには <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong> も含まれています。';
|
||||
$string['welcomep50'] = 'このパッケージ内のすべてのアプリケーションの使用は個別のライセンスによって規定されています。全体の<strong>{$a->installername}</strong>パッケージは<a href="https://www.opensource.org/docs/definition_plain.html">オープンソース</a>であり、<a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>ライセンスの下で配布されています。';
|
||||
$string['welcomep20'] = 'インストール正常完了後、あなたのコンピュータで<strong>{$a->packname} {$a->packversion}</strong>パッケージが起動されたため、このページが表示されています。おめでとうございます!';
|
||||
$string['welcomep30'] = 'このリリース<strong>{$a->installername}</strong>には<strong>Moodle</strong>で環境を作成するアプリケーションが含まれています。すなわち:';
|
||||
$string['welcomep40'] = 'パッケージには<strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong>も含まれています。';
|
||||
$string['welcomep50'] = 'このパッケージ内すべてのアプリケーションの使用は個別のライセンスにより規定されています。全体の<strong>{$a->installername}</strong>パッケージは<a href="https://www.opensource.org/docs/definition_plain.html">オープンソース</a>であり、<a href="https://www.gnu.org/copyleft/gpl.html">GPL</a>ライセンスの下で配布されています。';
|
||||
$string['welcomep60'] = '次からのページはあなたのコンピュータに<strong>Moodle</strong>を簡単に設定およびセットアップする手順にしたがって進みます。あなたはデフォルトの設定を使用することも、必要に応じて任意で設定を変更することもできます。';
|
||||
$string['welcomep70'] = '<strong>Moodle</strong>のセットアップを続けるには「次へ」ボタンをクリックしてください。';
|
||||
$string['wwwroot'] = 'ウェブアドレス';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://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 <https://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['language'] = 'Fiteny';
|
||||
$string['moodlelogo'] = 'Sarim-pamantarana ny Moodle';
|
||||
$string['next'] = 'Manaraka';
|
||||
$string['previous'] = 'Teo aloha';
|
||||
$string['reload'] = 'Avereno';
|
||||
+2
-1
@@ -1160,7 +1160,7 @@ $string['requiremodintro_desc'] = 'If enabled, users will be forced to enter a d
|
||||
$string['required'] = 'Required';
|
||||
$string['requires'] = 'Requires';
|
||||
$string['purgecaches'] = 'Purge all caches';
|
||||
$string['purgecachesconfirm'] = 'Moodle can cache themes, javascript, language strings, filtered text, rss feeds and many other pieces of calculated data. Purging these caches will delete that data from the server and force browsers to refetch data, so that you can be sure you are seeing the most up-to-date values produced by the current code. There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
|
||||
$string['purgecachesconfirm'] = 'Moodle can cache themes, JavaScript, language strings, filtered text, RSS feeds and other pieces of calculated data. Purging caches will delete data from the server and force browsers to re-fetch data, so you can be sure you are seeing the most up-to-date values produced by the current code. There is no danger in purging caches, but your site may appear slower for a while until the server and clients calculate new information and cache it.';
|
||||
$string['purgecachesfinished'] = 'All caches were purged.';
|
||||
$string['purgecachesnoneselected'] = 'Select one or more caches to purge';
|
||||
$string['purgecachespage'] = 'Purge caches';
|
||||
@@ -1509,6 +1509,7 @@ $string['unsupportedphpversion80'] = 'PHP version 8.0 and higher are not support
|
||||
$string['unsupportedphpversion81'] = 'PHP version 8.1 and higher are not supported.';
|
||||
$string['unsupportedphpversion82'] = 'PHP version 8.2 and higher are not supported.';
|
||||
$string['unsupportedphpversion83'] = 'PHP version 8.3 and higher are not supported.';
|
||||
$string['unsupportedphpversion84'] = 'PHP version 8.4 and higher are not supported.';
|
||||
$string['unsuspenduser'] = 'Activate user account';
|
||||
$string['updateaccounts'] = 'Update existing accounts';
|
||||
$string['updatecomponent'] = 'Update component';
|
||||
|
||||
@@ -310,7 +310,6 @@ $string['error:invalidparambadge'] = 'Badge does not exist. ';
|
||||
$string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.';
|
||||
$string['error:nobadges'] = 'There are currently no badges with access enabled to be added as criteria. A site badge can only have other site badges as criteria. A course badge can have other course badges or site badges as criteria.';
|
||||
$string['error:invalidparamcohort'] = 'Cohort does not exist. ';
|
||||
$string['error:noactivities'] = 'There are no activities with completion criteria enabled in this course.';
|
||||
$string['error:nocohorts'] = 'No cohorts';
|
||||
$string['error:nocourses'] = 'Course completion is not enabled for any of the courses in this site, so none can be displayed. Course completion may be enabled in the course settings.';
|
||||
$string['error:nogroupssummary'] = '<p>There are no public collections of badges available in your backpack. </p>';
|
||||
|
||||
@@ -163,7 +163,6 @@ $string['newpicture_help'] = 'Select an image in JPG or PNG format. The image wi
|
||||
$string['noallocation'] = 'No allocation';
|
||||
$string['nogrouping'] = 'No grouping';
|
||||
$string['nogroup'] = 'No group';
|
||||
$string['nogrouping'] = 'No grouping';
|
||||
$string['nogroups'] = 'There are no groups set up in this course yet';
|
||||
$string['nogroupsassigned'] = 'No groups assigned';
|
||||
$string['nopermissionforcreation'] = 'Can\'t create group "{$a}" as you don\'t have the required permissions';
|
||||
|
||||
@@ -111,7 +111,6 @@ $string['messagedrawerviewgroupinfo'] = 'Group details for {$a}';
|
||||
$string['messagedrawerviewoverview'] = 'Messages overview';
|
||||
$string['messagedrawerviewsearch'] = 'Messages search results for {$a}';
|
||||
$string['messagedrawerviewsettings'] = 'Message settings';
|
||||
$string['messagepreferences'] = 'Message preferences';
|
||||
$string['messages'] = 'Messages';
|
||||
$string['messagesselected:'] = 'Messages selected:';
|
||||
$string['messagetextrequired'] = 'Please enter message text';
|
||||
|
||||
+8
-9
@@ -194,7 +194,7 @@ $string['backupdaterecordtype'] = '<br />{$a->recordtype} - {$a->recordname}<br
|
||||
$string['backupdetails'] = 'Backup details';
|
||||
$string['backuperrorinvaliddestination'] = 'The backup destination folder does not exist or is not writable.';
|
||||
$string['backupexecuteathelp'] = 'Choose what time automated backups should run at.';
|
||||
$string['backupfailed'] = 'Some of your courses weren\'t saved!!';
|
||||
$string['backupfailed'] = 'Some of your courses weren\'t saved.';
|
||||
$string['backupfilename'] = 'backup';
|
||||
$string['backupfinished'] = 'Backup completed successfully';
|
||||
$string['backupfromthissite'] = 'Backup was made on this site?';
|
||||
@@ -349,7 +349,7 @@ $string['courseadministration'] = 'Course administration';
|
||||
$string['courseapprovedemail'] = 'Your requested course is approved, <a href="{$a->url}">{$a->name}</a>, and you are a {$a->teacher}.';
|
||||
$string['courseapprovedemail2'] = 'Your requested course is approved: <a href="{$a->url}">{$a->name}</a>.';
|
||||
$string['courseapprovedfailed'] = 'Failed to save the course as approved!';
|
||||
$string['courseapprovedsubject'] = 'Your course has been approved!';
|
||||
$string['courseapprovedsubject'] = 'Your course has been approved.';
|
||||
$string['courseavailable'] = 'This course is available to students';
|
||||
$string['courseavailablenot'] = 'This course is not available to students';
|
||||
$string['coursebackup'] = 'Course backup';
|
||||
@@ -536,7 +536,7 @@ $string['deletecoursecheck'] = 'Are you absolutely sure you want to completely d
|
||||
$string['deleted'] = 'Deleted';
|
||||
$string['deletedactivity'] = 'Deleted {$a}';
|
||||
$string['deletedcourse'] = '{$a} has been completely deleted';
|
||||
$string['deletednot'] = 'Could not delete {$a} !';
|
||||
$string['deletednot'] = 'Could not delete {$a}.';
|
||||
$string['deletecheck'] = 'Delete {$a} ?';
|
||||
$string['deletechecktype'] = 'Are you sure that you want to delete this {$a->type}?';
|
||||
$string['deletechecktypename'] = 'Are you sure that you want to delete the {$a->type} "{$a->name}"?';
|
||||
@@ -1093,7 +1093,6 @@ $string['courseduration_desc'] = 'The course duration is used to calculate the d
|
||||
$string['courseenddateenabled'] = 'Course end date enabled by default';
|
||||
$string['courseenddateenabled_desc'] = 'This setting determines if the course end date will be enabled by default for new courses and set to the date calculated from the course duration.';
|
||||
$string['eventcontentviewed'] = 'Content viewed';
|
||||
$string['filter'] = 'Filter';
|
||||
$string['filteroption'] = '{$a->criteria}: {$a->value}';
|
||||
$string['filterrowlegend'] = 'Filter {$a}';
|
||||
$string['filters'] = 'Filters';
|
||||
@@ -1193,7 +1192,7 @@ $string['labelvalue'] = '{$a->label}: {$a->value}';
|
||||
$string['langltr'] = 'Language direction left-to-right';
|
||||
$string['langrtl'] = 'Language direction right-to-left';
|
||||
$string['language'] = 'Language';
|
||||
$string['languagegood'] = 'This language pack is up-to-date! :-)';
|
||||
$string['languagegood'] = 'This language pack is up-to-date.';
|
||||
$string['languageselector'] = 'Language selector';
|
||||
$string['last'] = 'Last';
|
||||
$string['lastaccess'] = 'Last access';
|
||||
@@ -1652,13 +1651,13 @@ $string['passwordsdiffer'] = 'These passwords do not match';
|
||||
$string['passwordset'] = 'Your password has been set.';
|
||||
$string['path'] = 'Path';
|
||||
$string['pathnotexists'] = 'Path doesn\'t exist in your server!';
|
||||
$string['pathslasherror'] = 'Path can\'t end with a slash!!';
|
||||
$string['paymentinstant'] = 'Use the button below to pay and be enrolled within minutes!';
|
||||
$string['pathslasherror'] = 'The path can\'t end with a slash.';
|
||||
$string['paymentinstant'] = 'Use the button below to pay and be enrolled within minutes.';
|
||||
$string['paymentpending'] = '(<small><b><u>{$a}</u></b> pending</small>)';
|
||||
$string['paymentrequired'] = 'This course requires a payment for entry.';
|
||||
$string['payments'] = 'Payments';
|
||||
$string['paymentsorry'] = 'Thank you for your payment! Unfortunately your payment has not yet been fully processed, and you are not yet registered to enter the course "{$a->fullname}". Please try continuing to the course in a few seconds, but if you continue to have trouble then please alert the {$a->teacher} or the site administrator';
|
||||
$string['paymentthanks'] = 'Thank you for your payment! You are now enrolled in your course:<br />"{$a}"';
|
||||
$string['paymentsorry'] = 'Thanks for your payment. When it is processed, you will be able to enter the course {$a->fullname}. Please wait then try again. If you continue to have trouble, contact the {$a->teacher} or the site administrator.';
|
||||
$string['paymentthanks'] = 'Thanks for your payment. You are now enrolled in the course {$a}.';
|
||||
$string['pendingrequests'] = 'Pending requests';
|
||||
$string['percents'] = '{$a}%';
|
||||
$string['periodending'] = 'Period ending ({$a})';
|
||||
|
||||
@@ -342,8 +342,6 @@ $string['answerx'] = 'Answer {$a}';
|
||||
$string['attemptfinished'] = 'Attempt finished';
|
||||
$string['attemptfinishedsubmitting'] = 'Attempt finished submitting: {$a}';
|
||||
$string['behaviourbeingused'] = 'Behaviour being used: {$a}';
|
||||
$string['cannotloadquestion'] = 'Could not load question';
|
||||
$string['cannotpreview'] = 'You can\'t preview these questions!';
|
||||
$string['category'] = 'Category';
|
||||
$string['changeoptions'] = 'Change options';
|
||||
$string['attemptoptions'] = 'Attempt options';
|
||||
@@ -358,7 +356,6 @@ $string['commentormark'] = 'Make comment or override mark';
|
||||
$string['comments'] = 'Comments';
|
||||
$string['commentx'] = 'Comment: {$a}';
|
||||
$string['complete'] = 'Complete';
|
||||
$string['contexterror'] = 'You shouldn\'t have got here if you\'re not moving a category to another context.';
|
||||
$string['correct'] = 'Correct';
|
||||
$string['correctfeedback'] = 'For any correct response';
|
||||
$string['correctfeedbackdefault'] = 'Your answer is correct.';
|
||||
@@ -455,7 +452,6 @@ $string['questionediting'] = 'Question editing';
|
||||
$string['questionediting_desc'] = 'These settings apply to edit question forms.';
|
||||
$string['questionidmismatch'] = 'Question ids mismatch';
|
||||
$string['questionformtagheader'] = '{$a} tags';
|
||||
$string['questionname'] = 'Question name';
|
||||
$string['questionnamecopy'] = '{$a} (copy)';
|
||||
$string['questionpreviewdefaults'] = 'Question preview defaults';
|
||||
$string['questionpreviewdefaults_desc'] = 'These defaults are used when a user first previews a question in the question bank. Once a user has previewed a question, their personal preferences are stored as user preferences.';
|
||||
@@ -508,7 +504,6 @@ $string['unknownbehaviour'] = 'Unknown behaviour: {$a}.';
|
||||
$string['unknownorunhandledtype'] = 'Unknown or unhandled question type: {$a}';
|
||||
$string['unknownquestion'] = 'Unknown question: {$a}.';
|
||||
$string['unknownquestioncatregory'] = 'Unknown question category: {$a}.';
|
||||
$string['unknownquestiontype'] = 'Unknown question type: {$a}.';
|
||||
$string['unusedcategorydeleted'] = 'This category has been deleted because, after deleting the course, its questions weren\'t used any more.';
|
||||
$string['updatedisplayoptions'] = 'Update display options';
|
||||
$string['whethercorrect'] = 'Whether correct';
|
||||
|
||||
@@ -114,6 +114,7 @@ $string['america/chicago'] = 'America/Chicago';
|
||||
$string['america/chihuahua'] = 'America/Chihuahua';
|
||||
$string['america/ciudad_juarez'] = 'America/Ciudad_Juarez';
|
||||
$string['america/costa_rica'] = 'America/Costa_Rica';
|
||||
$string['america/coyhaique'] = 'America/Coyhaique';
|
||||
$string['america/creston'] = 'America/Creston';
|
||||
$string['america/cuiaba'] = 'America/Cuiaba';
|
||||
$string['america/curacao'] = 'America/Curacao';
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# core (subsystem) Upgrade notes
|
||||
|
||||
## 4.5.4
|
||||
|
||||
### Added
|
||||
|
||||
- Behat now supports email content verification using Mailpit.
|
||||
You can check the contents of an email using the step `Then the email to "user@example.com" with subject containing "subject" should contain "content".`
|
||||
To use this feature:
|
||||
1. Ensure that Mailpit is running
|
||||
2. Define the following constants in your `config.php`:
|
||||
- `TEST_EMAILCATCHER_MAIL_SERVER` - The Mailpit server address (e.g. `0.0.0.0:1025`)
|
||||
- `TEST_EMAILCATCHER_API_SERVER` - The Mailpit API server (qe.g. `http://localhost:8025`)
|
||||
|
||||
3. Ensure that the email catcher is set up using the step `Given an email catcher server is configured`.
|
||||
|
||||
For more information see [MDL-75971](https://tracker.moodle.org/browse/MDL-75971)
|
||||
- The public method `get_slashargument` has been added to the `url` class.
|
||||
|
||||
For more information see [MDL-84351](https://tracker.moodle.org/browse/MDL-84351)
|
||||
- A new method, `core_text::trim_ctrl_chars()`, has been introduced to clean control characters from text. This ensures cleaner input handling and prevents issues caused by invisible or non-printable characters
|
||||
|
||||
For more information see [MDL-84907](https://tracker.moodle.org/browse/MDL-84907)
|
||||
|
||||
## 4.5.2
|
||||
|
||||
### Added
|
||||
|
||||
Vendored
+1
-1
@@ -5,6 +5,6 @@ define("core/drawer",["exports","jquery","core/pubsub","core/aria","core/drawer_
|
||||
* @module core/drawer
|
||||
* @copyright 2019 Jun Pataleta <jun@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),PubSub=_interopRequireWildcard(PubSub),Aria=_interopRequireWildcard(Aria),_drawer_events=_interopRequireDefault(_drawer_events);const show=root=>{root=(0,_jquery.default)(root),Aria.unhide(root.get()),root.removeClass("hidden"),root.attr("aria-expanded",!0),root.focus(),PubSub.publish(_drawer_events.default.DRAWER_SHOWN,root)},hide=root=>{(root=(0,_jquery.default)(root)).addClass("hidden"),root.attr("aria-expanded",!1),Aria.hide(root.get()),PubSub.publish(_drawer_events.default.DRAWER_HIDDEN,root)},isVisible=root=>!root.hasClass("hidden"),toggle=root=>{isVisible(root)?hide(root):show(root)};var _default={hide:hide,show:show,isVisible:isVisible,toggle:toggle,registerToggles:(root,toggleElements)=>{let openTrigger=null;toggleElements.attr("aria-expanded",isVisible(root)),toggleElements.on("click",(e=>{e.preventDefault();const wasVisible=isVisible(root);toggle(root),toggleElements.attr("aria-expanded",!wasVisible),wasVisible?openTrigger&&(openTrigger.focus(),openTrigger=null):openTrigger=toggleElements.filter(((index,element)=>element==e.target||element.contains(e.target)))}))},getDrawerRoot:contentRoot=>(contentRoot=(0,_jquery.default)(contentRoot)).closest('[data-region="right-hand-drawer"]')};return _exports.default=_default,_exports.default}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=_interopRequireDefault(_jquery),PubSub=_interopRequireWildcard(PubSub),Aria=_interopRequireWildcard(Aria),_drawer_events=_interopRequireDefault(_drawer_events);const show=root=>{root=(0,_jquery.default)(root),Aria.unhide(root.get()),root.removeClass("hidden"),root.focus(),PubSub.publish(_drawer_events.default.DRAWER_SHOWN,root)},hide=root=>{(root=(0,_jquery.default)(root)).addClass("hidden"),Aria.hide(root.get()),PubSub.publish(_drawer_events.default.DRAWER_HIDDEN,root)},isVisible=root=>!root.hasClass("hidden"),toggle=root=>{isVisible(root)?hide(root):show(root)};var _default={hide:hide,show:show,isVisible:isVisible,toggle:toggle,registerToggles:(root,toggleElements)=>{let openTrigger=null;toggleElements.attr("aria-expanded",isVisible(root)),toggleElements.on("click",(e=>{e.preventDefault();const wasVisible=isVisible(root);toggle(root),toggleElements.attr("aria-expanded",!wasVisible),wasVisible?openTrigger&&(openTrigger.focus(),openTrigger=null):openTrigger=toggleElements.filter(((index,element)=>element==e.target||element.contains(e.target)))}))},getDrawerRoot:contentRoot=>(contentRoot=(0,_jquery.default)(contentRoot)).closest('[data-region="right-hand-drawer"]')};return _exports.default=_default,_exports.default}));
|
||||
|
||||
//# sourceMappingURL=drawer.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"drawer.min.js","sources":["../src/drawer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Controls the drawer.\n *\n * @module core/drawer\n * @copyright 2019 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport * as PubSub from 'core/pubsub';\nimport * as Aria from 'core/aria';\nimport DrawerEvents from 'core/drawer_events';\n\n/**\n * Show the drawer.\n *\n * @param {Object} root The drawer container.\n */\nconst show = root => {\n // Ensure that it is a jQuery.\n root = $(root);\n\n Aria.unhide(root.get());\n root.removeClass('hidden');\n root.attr('aria-expanded', true);\n root.focus();\n\n PubSub.publish(DrawerEvents.DRAWER_SHOWN, root);\n};\n\n/**\n * Hide the drawer.\n *\n * @param {Object} root The drawer container.\n */\nconst hide = root => {\n // Ensure that it is a jQuery.\n root = $(root);\n\n root.addClass('hidden');\n root.attr('aria-expanded', false);\n Aria.hide(root.get());\n\n PubSub.publish(DrawerEvents.DRAWER_HIDDEN, root);\n};\n\n/**\n * Check if the drawer is visible.\n *\n * @param {Object} root The drawer container.\n * @return {boolean}\n */\nconst isVisible = (root) => {\n let isHidden = root.hasClass('hidden');\n return !isHidden;\n};\n\n/**\n * Toggle the drawer visibility.\n *\n * @param {Object} root The drawer container.\n */\nconst toggle = (root) => {\n if (isVisible(root)) {\n hide(root);\n } else {\n show(root);\n }\n};\n\n/**\n * Add event listeners to toggle the drawer.\n *\n * @param {Object} root The drawer container.\n * @param {Object} toggleElements The toggle elements.\n */\nconst registerToggles = (root, toggleElements) => {\n let openTrigger = null;\n toggleElements.attr('aria-expanded', isVisible(root));\n\n toggleElements.on('click', (e) => {\n e.preventDefault();\n const wasVisible = isVisible(root);\n toggle(root);\n toggleElements.attr('aria-expanded', !wasVisible);\n\n if (!wasVisible) {\n // Remember which trigger element opened the drawer.\n openTrigger = toggleElements.filter((index, element) => {\n return element == e.target || element.contains(e.target);\n });\n } else if (openTrigger) {\n // The drawer has gone from open to close so we need to set the focus back\n // to the element that openend it.\n openTrigger.focus();\n openTrigger = null;\n }\n });\n};\n\n/**\n * Find the root element of the drawer based on the using the drawer content root's ID.\n *\n * @param {Object} contentRoot The drawer content's root element.\n * @returns {*|jQuery}\n */\nconst getDrawerRoot = (contentRoot) => {\n contentRoot = $(contentRoot);\n return contentRoot.closest('[data-region=\"right-hand-drawer\"]');\n};\n\nexport default {\n hide: hide,\n show: show,\n isVisible: isVisible,\n toggle: toggle,\n registerToggles: registerToggles,\n getDrawerRoot: getDrawerRoot\n};\n"],"names":["show","root","Aria","unhide","get","removeClass","attr","focus","PubSub","publish","DrawerEvents","DRAWER_SHOWN","hide","addClass","DRAWER_HIDDEN","isVisible","hasClass","toggle","registerToggles","toggleElements","openTrigger","on","e","preventDefault","wasVisible","filter","index","element","target","contains","getDrawerRoot","contentRoot","closest"],"mappings":";;;;;;;mQAgCMA,KAAOC,OAETA,MAAO,mBAAEA,MAETC,KAAKC,OAAOF,KAAKG,OACjBH,KAAKI,YAAY,UACjBJ,KAAKK,KAAK,iBAAiB,GAC3BL,KAAKM,QAELC,OAAOC,QAAQC,uBAAaC,aAAcV,OAQxCW,KAAOX,QAETA,MAAO,mBAAEA,OAEJY,SAAS,UACdZ,KAAKK,KAAK,iBAAiB,GAC3BJ,KAAKU,KAAKX,KAAKG,OAEfI,OAAOC,QAAQC,uBAAaI,cAAeb,OASzCc,UAAad,OACAA,KAAKe,SAAS,UAS3BC,OAAUhB,OACRc,UAAUd,MACVW,KAAKX,MAELD,KAAKC,oBA6CE,CACXW,KAAMA,KACNZ,KAAMA,KACNe,UAAWA,UACXE,OAAQA,OACRC,gBAxCoB,CAACjB,KAAMkB,sBACvBC,YAAc,KAClBD,eAAeb,KAAK,gBAAiBS,UAAUd,OAE/CkB,eAAeE,GAAG,SAAUC,IACxBA,EAAEC,uBACIC,WAAaT,UAAUd,MAC7BgB,OAAOhB,MACPkB,eAAeb,KAAK,iBAAkBkB,YAEjCA,WAKMJ,cAGPA,YAAYb,QACZa,YAAc,MAPdA,YAAcD,eAAeM,QAAO,CAACC,MAAOC,UACjCA,SAAWL,EAAEM,QAAUD,QAAQE,SAASP,EAAEM,cA4B7DE,cAXmBC,cACnBA,aAAc,mBAAEA,cACGC,QAAQ"}
|
||||
{"version":3,"file":"drawer.min.js","sources":["../src/drawer.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Controls the drawer.\n *\n * @module core/drawer\n * @copyright 2019 Jun Pataleta <jun@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport $ from 'jquery';\nimport * as PubSub from 'core/pubsub';\nimport * as Aria from 'core/aria';\nimport DrawerEvents from 'core/drawer_events';\n\n/**\n * Show the drawer.\n *\n * @param {Object} root The drawer container.\n */\nconst show = root => {\n // Ensure that it is a jQuery.\n root = $(root);\n\n Aria.unhide(root.get());\n root.removeClass('hidden');\n root.focus();\n\n PubSub.publish(DrawerEvents.DRAWER_SHOWN, root);\n};\n\n/**\n * Hide the drawer.\n *\n * @param {Object} root The drawer container.\n */\nconst hide = root => {\n // Ensure that it is a jQuery.\n root = $(root);\n\n root.addClass('hidden');\n Aria.hide(root.get());\n\n PubSub.publish(DrawerEvents.DRAWER_HIDDEN, root);\n};\n\n/**\n * Check if the drawer is visible.\n *\n * @param {Object} root The drawer container.\n * @return {boolean}\n */\nconst isVisible = (root) => {\n let isHidden = root.hasClass('hidden');\n return !isHidden;\n};\n\n/**\n * Toggle the drawer visibility.\n *\n * @param {Object} root The drawer container.\n */\nconst toggle = (root) => {\n if (isVisible(root)) {\n hide(root);\n } else {\n show(root);\n }\n};\n\n/**\n * Add event listeners to toggle the drawer.\n *\n * @param {Object} root The drawer container.\n * @param {Object} toggleElements The toggle elements.\n */\nconst registerToggles = (root, toggleElements) => {\n let openTrigger = null;\n toggleElements.attr('aria-expanded', isVisible(root));\n\n toggleElements.on('click', (e) => {\n e.preventDefault();\n const wasVisible = isVisible(root);\n toggle(root);\n toggleElements.attr('aria-expanded', !wasVisible);\n\n if (!wasVisible) {\n // Remember which trigger element opened the drawer.\n openTrigger = toggleElements.filter((index, element) => {\n return element == e.target || element.contains(e.target);\n });\n } else if (openTrigger) {\n // The drawer has gone from open to close so we need to set the focus back\n // to the element that openend it.\n openTrigger.focus();\n openTrigger = null;\n }\n });\n};\n\n/**\n * Find the root element of the drawer based on the using the drawer content root's ID.\n *\n * @param {Object} contentRoot The drawer content's root element.\n * @returns {*|jQuery}\n */\nconst getDrawerRoot = (contentRoot) => {\n contentRoot = $(contentRoot);\n return contentRoot.closest('[data-region=\"right-hand-drawer\"]');\n};\n\nexport default {\n hide: hide,\n show: show,\n isVisible: isVisible,\n toggle: toggle,\n registerToggles: registerToggles,\n getDrawerRoot: getDrawerRoot\n};\n"],"names":["show","root","Aria","unhide","get","removeClass","focus","PubSub","publish","DrawerEvents","DRAWER_SHOWN","hide","addClass","DRAWER_HIDDEN","isVisible","hasClass","toggle","registerToggles","toggleElements","openTrigger","attr","on","e","preventDefault","wasVisible","filter","index","element","target","contains","getDrawerRoot","contentRoot","closest"],"mappings":";;;;;;;mQAgCMA,KAAOC,OAETA,MAAO,mBAAEA,MAETC,KAAKC,OAAOF,KAAKG,OACjBH,KAAKI,YAAY,UACjBJ,KAAKK,QAELC,OAAOC,QAAQC,uBAAaC,aAAcT,OAQxCU,KAAOV,QAETA,MAAO,mBAAEA,OAEJW,SAAS,UACdV,KAAKS,KAAKV,KAAKG,OAEfG,OAAOC,QAAQC,uBAAaI,cAAeZ,OASzCa,UAAab,OACAA,KAAKc,SAAS,UAS3BC,OAAUf,OACRa,UAAUb,MACVU,KAAKV,MAELD,KAAKC,oBA6CE,CACXU,KAAMA,KACNX,KAAMA,KACNc,UAAWA,UACXE,OAAQA,OACRC,gBAxCoB,CAAChB,KAAMiB,sBACvBC,YAAc,KAClBD,eAAeE,KAAK,gBAAiBN,UAAUb,OAE/CiB,eAAeG,GAAG,SAAUC,IACxBA,EAAEC,uBACIC,WAAaV,UAAUb,MAC7Be,OAAOf,MACPiB,eAAeE,KAAK,iBAAkBI,YAEjCA,WAKML,cAGPA,YAAYb,QACZa,YAAc,MAPdA,YAAcD,eAAeO,QAAO,CAACC,MAAOC,UACjCA,SAAWL,EAAEM,QAAUD,QAAQE,SAASP,EAAEM,cA4B7DE,cAXmBC,cACnBA,aAAc,mBAAEA,cACGC,QAAQ"}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -36,7 +36,6 @@ const show = root => {
|
||||
|
||||
Aria.unhide(root.get());
|
||||
root.removeClass('hidden');
|
||||
root.attr('aria-expanded', true);
|
||||
root.focus();
|
||||
|
||||
PubSub.publish(DrawerEvents.DRAWER_SHOWN, root);
|
||||
@@ -52,7 +51,6 @@ const hide = root => {
|
||||
root = $(root);
|
||||
|
||||
root.addClass('hidden');
|
||||
root.attr('aria-expanded', false);
|
||||
Aria.hide(root.get());
|
||||
|
||||
PubSub.publish(DrawerEvents.DRAWER_HIDDEN, root);
|
||||
|
||||
@@ -126,7 +126,7 @@ define(['jquery', 'core/str', 'core/custom_interaction_events'],
|
||||
}
|
||||
|
||||
this.root.addClass('collapsed');
|
||||
this.menuContainer.attr('aria-expanded', 'false');
|
||||
this.menuToggle.attr('aria-expanded', 'false');
|
||||
this.menuContainer.attr('aria-hidden', 'true');
|
||||
this.updateButtonAriaLabel();
|
||||
this.updateFocusItemTabIndex();
|
||||
@@ -147,7 +147,7 @@ define(['jquery', 'core/str', 'core/custom_interaction_events'],
|
||||
}
|
||||
|
||||
this.root.removeClass('collapsed');
|
||||
this.menuContainer.attr('aria-expanded', 'true');
|
||||
this.menuToggle.attr('aria-expanded', 'true');
|
||||
this.menuContainer.attr('aria-hidden', 'false');
|
||||
this.updateButtonAriaLabel();
|
||||
this.updateFocusItemTabIndex();
|
||||
|
||||
@@ -1038,14 +1038,25 @@ EOF;
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for any DOM element with deprecated message in before pseudo-element.
|
||||
// Look for DOM elements with deprecated message in before pseudo-element.
|
||||
$js = <<<EOF
|
||||
[...document.querySelectorAll('*')].some(
|
||||
el => window.getComputedStyle(el, ':before').content === '"Deprecated style in use"'
|
||||
);
|
||||
[...document.querySelectorAll('*')].flatMap(el => {
|
||||
const beforeContent = window.getComputedStyle(el, ':before').content;
|
||||
if (beforeContent.startsWith('"Deprecated style in use')) {
|
||||
const deprecatedClass = beforeContent.match(/\(([^)]+)\)/)?.[1] ?? 'unknown';
|
||||
return [deprecatedClass + ' (found in: ' + el.classList + ')'];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
EOF;
|
||||
if ($this->evaluate_script($js)) {
|
||||
throw new \Exception(html_entity_decode("Deprecated style in use", ENT_COMPAT));
|
||||
|
||||
$deprecations = $this->evaluate_script($js);
|
||||
if ($deprecations) {
|
||||
$deprecationdata = "Deprecated styles found:\n";
|
||||
foreach ($deprecations as $deprecation) {
|
||||
$deprecationdata .= " {$deprecation}\n";
|
||||
}
|
||||
throw new \Exception(html_entity_decode($deprecationdata, ENT_COMPAT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1719,6 +1730,32 @@ EOF;
|
||||
return !empty($matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user object from an identifier.
|
||||
*
|
||||
* The user username and email fields are checked.
|
||||
*
|
||||
* @param string $identifier The user's username or email.
|
||||
* @return stdClass|null The user id or null if not found.
|
||||
*/
|
||||
protected function get_user_by_identifier(string $identifier): ?stdClass {
|
||||
global $DB;
|
||||
|
||||
$sql = <<<EOF
|
||||
SELECT *
|
||||
FROM {user}
|
||||
WHERE username = :username
|
||||
OR email = :email
|
||||
EOF;
|
||||
|
||||
$result = $DB->get_record_sql($sql, [
|
||||
'username' => $identifier,
|
||||
'email' => $identifier,
|
||||
]);
|
||||
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user id from an identifier.
|
||||
*
|
||||
|
||||
@@ -233,6 +233,12 @@ function behat_clean_init_config() {
|
||||
unset($CFG->{$key});
|
||||
}
|
||||
}
|
||||
|
||||
// Allow email catcher settings.
|
||||
if (defined('TEST_EMAILCATCHER_MAIL_SERVER')) {
|
||||
$CFG->noemailever = false;
|
||||
$CFG->smtphosts = TEST_EMAILCATCHER_MAIL_SERVER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -757,6 +757,7 @@ class core_date {
|
||||
if ($intltz === null) {
|
||||
// Where intl doesn't know about a recent timezone, map it to an equivalent existing zone.
|
||||
$intltzname = strtr($tz->getName(), [
|
||||
'America/Coyhaique' => 'America/Santiago',
|
||||
'America/Ciudad_Juarez' => 'America/Denver',
|
||||
'America/Nuuk' => 'America/Godthab',
|
||||
'Europe/Kyiv' => 'Europe/Kiev',
|
||||
|
||||
@@ -586,6 +586,12 @@ class secondary extends view {
|
||||
$activenode = $this->find_active_node();
|
||||
$incourseadmin = false;
|
||||
|
||||
$activeleafnode = $this->page->settingsnav->find_active_node();
|
||||
$parentnode = $activeleafnode->parent ?? null;
|
||||
if ($issingleactivitycourse && $parentnode && $parentnode->key === 'quiz_report') {
|
||||
$activenode = $parentnode;
|
||||
}
|
||||
|
||||
if (!$activenode || ($issingleactivitycourse && $activenode->key === 'course')) {
|
||||
// Could be in the course admin section.
|
||||
$courseadmin = $this->page->settingsnav->find('courseadmin', navigation_node::TYPE_COURSE);
|
||||
|
||||
@@ -331,7 +331,7 @@ class core_renderer extends renderer_base {
|
||||
$timeleft = $CFG->maintenance_later - time();
|
||||
// If timeleft less than 30 sec, set the class on block to error to highlight.
|
||||
$errorclass = ($timeleft < 30) ? 'alert-error alert-danger' : 'alert-warning';
|
||||
$output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning m-3 alert');
|
||||
$output .= $this->box_start($errorclass . ' moodle-has-zindex maintenancewarning alert');
|
||||
$a = new stdClass();
|
||||
$a->hour = (int)($timeleft / 3600);
|
||||
$a->min = (int)(floor($timeleft / 60) % 60);
|
||||
|
||||
@@ -114,7 +114,7 @@ class icon_system_fontawesome extends icon_system_font {
|
||||
'core:e/insert_file' => 'fa-regular fa-file',
|
||||
'core:e/insert_horizontal_ruler' => 'fa-ruler-horizontal',
|
||||
'core:e/insert_nonbreaking_space' => 'fa-regular fa-square',
|
||||
'core:e/insert_page_break' => 'fa-arrows-left-right-to-line',
|
||||
'core:e/insert_page_break' => 'fa-plus',
|
||||
'core:e/insert_row_after' => 'fa-plus',
|
||||
'core:e/insert_row_before' => 'fa-plus',
|
||||
'core:e/insert' => 'fa-plus',
|
||||
|
||||
@@ -358,7 +358,7 @@ class page_requirements_manager {
|
||||
protected function get_api_base(): string {
|
||||
global $CFG;
|
||||
|
||||
if (!empty($CFG->router_configured)) {
|
||||
if (!empty($CFG->routerconfigured)) {
|
||||
return sprintf(
|
||||
"%s/api",
|
||||
$CFG->wwwroot,
|
||||
|
||||
@@ -1067,8 +1067,9 @@ enum param: string {
|
||||
// Absolute, and matches our wwwroot.
|
||||
} else {
|
||||
// Relative - let's make sure there are no tricks.
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
|
||||
// Looks ok.
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') &&
|
||||
!preg_match('/javascript(?:.*\/{2,})?:/i', rawurldecode($param))) {
|
||||
// Valid relative local URL.
|
||||
} else {
|
||||
$param = '';
|
||||
}
|
||||
|
||||
@@ -56,11 +56,15 @@ class core_shutdown_manager {
|
||||
// 'pcntl_async_signals'
|
||||
// 'pcntl_signal'
|
||||
// The 'pcntl' extension is optional and not available on Windows.
|
||||
if (extension_loaded('pcntl') && function_exists('pcntl_async_signals')) {
|
||||
// We capture and handle SIGINT (Ctrl+C) and SIGTERM (termination requested).
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
|
||||
pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
|
||||
if (extension_loaded('pcntl')) {
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
// We capture and handle SIGINT (Ctrl+C) and SIGTERM (termination requested).
|
||||
pcntl_async_signals(true);
|
||||
}
|
||||
if (function_exists('pcntl_signal')) {
|
||||
pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
|
||||
pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -630,15 +630,29 @@ abstract class scheduled_task extends task_base {
|
||||
$currenthour = (int)date('H', $now);
|
||||
$nextvalidhour = $this->next_in_list($currenthour, $validhours);
|
||||
if ($nextvalidhour != $currenthour) {
|
||||
if ($nextvalidhour < $currenthour) {
|
||||
$offset = ' +1 day';
|
||||
} else {
|
||||
$offset = '';
|
||||
$keepcurrent = false;
|
||||
$currentdate = new \DateTimeImmutable($currentyear . '-' . $currentmonth . '-' . $currentday . ' ' . $currenthour .
|
||||
':00');
|
||||
$lasthour = (int)date('H', $currentdate->sub(new \DateInterval('PT1S'))->getTimestamp());
|
||||
$nextafterlast = $this->next_in_list($lasthour, $validhours);
|
||||
// Special case for when the clocks go forward. If the next scheduled time would fall in an hour
|
||||
// that doesn't exist due to the clock change, then we use the next existing hour so that we don't
|
||||
// skip a run. However, the replacement hour may not appear in the valid hours list, so we check
|
||||
// whether we skipped a valid hour here to avoid recursing again and skipping its replacement.
|
||||
if (($lasthour + 1) % 24 <= $nextafterlast && $nextafterlast < $currenthour) {
|
||||
$keepcurrent = true;
|
||||
}
|
||||
$newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday . ' ' . $nextvalidhour .
|
||||
if (!$keepcurrent) {
|
||||
if ($nextvalidhour < $currenthour) {
|
||||
$offset = ' +1 day';
|
||||
} else {
|
||||
$offset = '';
|
||||
}
|
||||
$newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday . ' ' . $nextvalidhour .
|
||||
':00' . $offset);
|
||||
return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
|
||||
$validdaysofweek, $validmonths, $firstyear);
|
||||
return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays,
|
||||
$validdaysofweek, $validmonths, $firstyear);
|
||||
}
|
||||
}
|
||||
|
||||
// Round time down to an exact minute because we need to use numeric calculations on it now.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user