Compare commits
224 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d48741ed7 | |||
| 199974b45d | |||
| 637c6fbbca | |||
| e2d9a7fcee | |||
| b115058dc5 | |||
| fd5832fde9 | |||
| 8f375b3923 | |||
| 2970dfb52d | |||
| a174a570a6 | |||
| 917d7e1003 | |||
| 83a8cfb9e4 | |||
| 947b6f202c | |||
| 5ab6d18354 | |||
| e36332c01a | |||
| 5f399aa81a | |||
| 5b02f3bcf7 | |||
| 692657ffc4 | |||
| 753373d6e6 | |||
| 43e181f391 | |||
| 0cdb291d9e | |||
| 031004097b | |||
| 869c717e72 | |||
| 0536942919 | |||
| 5e5b39ee2c | |||
| 055fff8005 | |||
| e7094cdb75 | |||
| 3a5f610034 | |||
| 44e3680153 | |||
| 11db1b0193 | |||
| c4f95593a9 | |||
| 5957b238ef | |||
| 51a7a0aa3e | |||
| 9bb6ed7c1c | |||
| 1a01e66976 | |||
| 899e7e2103 | |||
| 92509cdeae | |||
| af84b0ea22 | |||
| 10ad537723 | |||
| a576209dd1 | |||
| 1a76f2752a | |||
| 8e0da918ab | |||
| ff3fd8a222 | |||
| 0127abe30e | |||
| a0aaa3cacf | |||
| d02d01aa71 | |||
| b9e3bcb748 | |||
| 447067d874 | |||
| 282e60115d | |||
| d3f3da7277 | |||
| 7228428479 | |||
| 53a493030c | |||
| 3fc5d0dec3 | |||
| b6ccf35285 | |||
| 4af82f5154 | |||
| 5855059608 | |||
| 9a7bdedc5a | |||
| b276bfce77 | |||
| 8d2f2e7d37 | |||
| 705e6c5614 | |||
| 48f1e6165d | |||
| 917369b8ef | |||
| 81e7828600 | |||
| a5776a4bbe | |||
| 7917f9a509 | |||
| c15ffa0e88 | |||
| 36c1a657b4 | |||
| 9fc20a6fef | |||
| c07e077f04 | |||
| 0e9eddf51e | |||
| c0462feec8 | |||
| 711e2b006f | |||
| 45e81f4818 | |||
| ac822d9655 | |||
| 0907ff86f8 | |||
| a52378726e | |||
| a2dd8127f0 | |||
| e9cf945617 | |||
| 64bc864e2e | |||
| e34a301fd0 | |||
| 339c473b9c | |||
| 62dec781bb | |||
| a76e8e9fbe | |||
| 1a9cb490ba | |||
| ceef8c7e1b | |||
| f8a83d8a67 | |||
| a6773e6664 | |||
| fb9fdc4754 | |||
| 4194bdf0fd | |||
| 710099242f | |||
| aee71c329d | |||
| b72e73d6de | |||
| 9a75af7805 | |||
| a0653230c5 | |||
| 802f6b3fa1 | |||
| 55198f851c | |||
| a4c654d67c | |||
| 6e7c7a7e2e | |||
| eb3a220af8 | |||
| d3b25ce4d9 | |||
| 7e29aee395 | |||
| 556236134a | |||
| b1bad66d46 | |||
| 660b5b0045 | |||
| d28f836536 | |||
| 6b8ae1c193 | |||
| 2a606b6a10 | |||
| 96cbdb8452 | |||
| 1a9775cb82 | |||
| 7806534af2 | |||
| f994a89ce5 | |||
| ae1a4cf5a7 | |||
| e1d2f92bda | |||
| 0faeb73989 | |||
| 54ca52c917 | |||
| f3ea59897e | |||
| d9a3ef29ce | |||
| 224038fc65 | |||
| 3ab09c775d | |||
| 065bcc9a3a | |||
| 962f75ec53 | |||
| 982e4df1f1 | |||
| d1d96d4902 | |||
| 5c2be62c94 | |||
| cc05ca22da | |||
| f5b9056370 | |||
| 03a003289a | |||
| a8011f89c8 | |||
| 8a9fea33e2 | |||
| 9d827a29a4 | |||
| 2c2686d66e | |||
| 0d05e7696f | |||
| 56846834e4 | |||
| 5e1cee2bd4 | |||
| e34536fc05 | |||
| 54ecc0d0d7 | |||
| 419f6d6d5c | |||
| 49171c9f6e | |||
| 53a2efa29f | |||
| 8e19d73c2b | |||
| 432bd19d5a | |||
| 7eee5bff29 | |||
| 60d07529d0 | |||
| 81d104300b | |||
| 7fc6677853 | |||
| f8037d326d | |||
| baba2c573e | |||
| 08b121599e | |||
| 2532fc2a15 | |||
| f31b3f4d55 | |||
| b6327f4435 | |||
| 7745c451fd | |||
| 290ee98681 | |||
| 12c5859ad2 | |||
| b364d2b8fe | |||
| 3dd97076d9 | |||
| 4a61fc1e76 | |||
| ced37870e6 | |||
| 008443efdd | |||
| a01201ec4c | |||
| 25f22a3cb7 | |||
| 7c585543e9 | |||
| 039953f01a | |||
| 66165e081d | |||
| 5a8d478e3d | |||
| 81da9e8ab2 | |||
| 39d832251f | |||
| 6f5dd17b43 | |||
| a15d2100b4 | |||
| 7eb557e695 | |||
| 1e2c2c5a2a | |||
| 0b463d4203 | |||
| c74979a798 | |||
| 7bd1ca51c9 | |||
| b463787e70 | |||
| dd1cd6cd8b | |||
| cd112cf93f | |||
| 5ab43fb1b3 | |||
| 08e4bc556c | |||
| 8a5bea2bc3 | |||
| 263a17676b | |||
| 3f6999f42e | |||
| 8b7d96ca68 | |||
| 4f5bdfcc0c | |||
| 77655b1ba3 | |||
| 2fae9b9849 | |||
| e9827edb0a | |||
| bce68bd0c4 | |||
| 8c34c3ed08 | |||
| 7da1d94956 | |||
| 125b689a5c | |||
| 8e2b50aec5 | |||
| 74f0fe1e77 | |||
| 55e1312f88 | |||
| 0c6b884060 | |||
| 7d764cde8b | |||
| 576020ce6a | |||
| 22fb553178 | |||
| 820cac7677 | |||
| 1dcfa8fffe | |||
| ba53bf48a7 | |||
| 7b8c92106a | |||
| b25dce473b | |||
| 911441bf43 | |||
| 8dc128313a | |||
| e14a534ce1 | |||
| 5aacd4ba77 | |||
| ed0dacbb5d | |||
| 62a909f91a | |||
| 1f85802f86 | |||
| 109bac00c8 | |||
| d789be92ae | |||
| ba26cf7c90 | |||
| f0170da7ec | |||
| 2fe08752e6 | |||
| 42c4d51f7c | |||
| b635c658c8 | |||
| c604750339 | |||
| 87a6da0d59 | |||
| 37b3397bea | |||
| f8ebec2da5 | |||
| 8af6174946 | |||
| 767d46be4c | |||
| c64dc2e545 | |||
| 4c53ddf46b |
@@ -51,3 +51,4 @@ moodle-plugin-ci.phar
|
||||
.eslintignore
|
||||
.stylelintignore
|
||||
/jsdoc
|
||||
.phpcs.xml
|
||||
|
||||
@@ -21,6 +21,43 @@
|
||||
*/
|
||||
|
||||
module.exports = grunt => {
|
||||
|
||||
/**
|
||||
* Generate the PHPCS configuration.
|
||||
*
|
||||
* @param {Object} thirdPartyPaths
|
||||
*/
|
||||
const phpcsIgnore = (thirdPartyPaths) => {
|
||||
const {toXML} = require('jstoxml');
|
||||
|
||||
const config = {
|
||||
_name: 'ruleset',
|
||||
_attrs: {
|
||||
name: "MoodleCore",
|
||||
},
|
||||
_content: [
|
||||
{
|
||||
rule: {
|
||||
_attrs: {
|
||||
ref: './.phpcs.xml.dist',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
thirdPartyPaths.forEach(library => {
|
||||
config._content.push({
|
||||
'exclude-pattern': library,
|
||||
});
|
||||
});
|
||||
|
||||
grunt.file.write('.phpcs.xml', toXML(config, {
|
||||
header: true,
|
||||
indent: ' ',
|
||||
}) + "\n");
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate ignore files (utilising thirdpartylibs.xml data)
|
||||
*/
|
||||
@@ -52,6 +89,8 @@ module.exports = grunt => {
|
||||
'jsdoc/styles/*.css',
|
||||
].concat(thirdPartyPaths);
|
||||
grunt.file.write('.stylelintignore', stylelintIgnores.join('\n') + '\n');
|
||||
|
||||
phpcsIgnore(thirdPartyPaths);
|
||||
};
|
||||
|
||||
grunt.registerTask('ignorefiles', 'Generate ignore files for linters', handler);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ruleset name="MoodleCore">
|
||||
<rule ref="moodle"/>
|
||||
</ruleset>
|
||||
@@ -26,7 +26,7 @@ require_once($CFG->libdir . '/adminlib.php');
|
||||
|
||||
admin_externalpage_setup('searchareas');
|
||||
|
||||
$areaid = optional_param('areaid', null, PARAM_ALPHAEXT);
|
||||
$areaid = optional_param('areaid', null, PARAM_ALPHANUMEXT);
|
||||
$action = optional_param('action', null, PARAM_ALPHA);
|
||||
$indexingenabled = \core_search\manager::is_indexing_enabled(); // This restricts many of the actions on this page.
|
||||
|
||||
|
||||
@@ -308,6 +308,13 @@ if ($hassiteconfig) {
|
||||
$temp->add(new admin_setting_managedataformats());
|
||||
$ADMIN->add('dataformatsettings', $temp);
|
||||
|
||||
$plugins = core_plugin_manager::instance()->get_plugins_of_type('dataformat');
|
||||
core_collator::asort_objects_by_property($plugins, 'displayname');
|
||||
foreach ($plugins as $plugin) {
|
||||
/** @var \core\plugininfo\dataformat $plugin */
|
||||
$plugin->load_settings($ADMIN, 'dataformatsettings', $hassiteconfig);
|
||||
}
|
||||
|
||||
//== Portfolio settings ==
|
||||
require_once($CFG->libdir. '/portfoliolib.php');
|
||||
$catname = new lang_string('portfolios', 'portfolio');
|
||||
|
||||
@@ -18,6 +18,10 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
|
||||
And the following "cohorts" exist:
|
||||
| name | idnumber |
|
||||
| Cohort 1 | CH1 |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| user2 | CH1 |
|
||||
| user3 | CH1 |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
@@ -27,8 +31,6 @@ Feature: An administrator can filter user accounts by role, cohort and other pro
|
||||
| user2 | C1 | student |
|
||||
| user3 | C1 | student |
|
||||
And I log in as "admin"
|
||||
And I add "User Two (two@example.com)" user to "CH1" cohort members
|
||||
And I add "User Three (three@example.com)" user to "CH1" cohort members
|
||||
And I navigate to "Users > Accounts > Browse list of users" in site administration
|
||||
|
||||
Scenario: Filter user accounts by role and cohort
|
||||
|
||||
@@ -477,8 +477,6 @@ class accessibility {
|
||||
public static function get_summary_data(int $id): \stdClass {
|
||||
global $CFG, $DB;
|
||||
|
||||
$components = $DB->get_records(manager::DB_AREAS);
|
||||
|
||||
$summarydata = new \stdClass();
|
||||
$summarydata->siteurl = (substr($CFG->wwwroot, -1) !== '/') ? $CFG->wwwroot . '/' : $CFG->wwwroot;
|
||||
$summarydata->moodlerelease = (preg_match('/^(\d+\.\d.*?)[. ]/', $CFG->release, $matches)) ? $matches[1] : $CFG->release;
|
||||
@@ -488,7 +486,7 @@ class accessibility {
|
||||
$summarydata->numfactivities = $DB->count_records('course_modules');
|
||||
$summarydata->mobileservice = (int)$CFG->enablemobilewebservice === 1 ? true : false;
|
||||
$summarydata->usersmobileregistered = $DB->count_records('user_devices');
|
||||
$summarydata->contenttyperesults = static::get_contenttyperesults($id, $components);
|
||||
$summarydata->contenttyperesults = static::get_contenttyperesults($id);
|
||||
$summarydata->contenttypeerrors = static::get_contenttypeerrors();
|
||||
$summarydata->percheckerrors = static::get_percheckerrors();
|
||||
return $summarydata;
|
||||
@@ -497,22 +495,24 @@ class accessibility {
|
||||
/**
|
||||
* Get content type results.
|
||||
* @param int $id
|
||||
* @param array $components
|
||||
* @return \stdClass
|
||||
*/
|
||||
private static function get_contenttyperesults(int $id, array $components): \stdClass {
|
||||
private static function get_contenttyperesults(int $id): \stdClass {
|
||||
global $DB;
|
||||
$sql = 'SELECT component, COUNT(id) AS count
|
||||
FROM {' . manager::DB_AREAS . '}
|
||||
GROUP BY component';
|
||||
$components = $DB->get_recordset_sql($sql);
|
||||
$contenttyperesults = new \stdClass();
|
||||
$contenttyperesults->id = $id;
|
||||
$datacomponents = array();
|
||||
foreach ($components as $component) {
|
||||
$datacomponents[$component->component][] = $component;
|
||||
}
|
||||
$contenttyperesults->contenttype = new \stdClass();
|
||||
foreach ($datacomponents as $key => $component) {
|
||||
$contenttyperesults->contenttype->$key = count($component);
|
||||
foreach ($components as $component) {
|
||||
$componentname = $component->component;
|
||||
$contenttyperesults->contenttype->$componentname = $component->count;
|
||||
}
|
||||
$components->close();
|
||||
$contenttyperesults->summarydatastorage = static::get_summary_data_storage();
|
||||
$contenttyperesults->datachecked = time(); // Correct??
|
||||
$contenttyperesults->datachecked = time();
|
||||
return $contenttyperesults;
|
||||
}
|
||||
|
||||
|
||||
+24
-6
@@ -25,7 +25,7 @@ namespace tool_brickfield\local\htmlchecker\common;
|
||||
*/
|
||||
class brickfield_accessibility_color_test extends brickfield_accessibility_test {
|
||||
|
||||
/** @var string[] Mapping of colours to hex codes. */
|
||||
/** @var string[] Define colour codes. */
|
||||
public $colornames = [
|
||||
'aliceblue' => 'f0f8ff',
|
||||
'antiquewhite' => 'faebd7',
|
||||
@@ -183,6 +183,12 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
}
|
||||
$forergb = $this->get_rgb($foreground);
|
||||
$backrgb = $this->get_rgb($background);
|
||||
|
||||
// If get_rgb returns null for either, return 0.
|
||||
if ($forergb === null || $backrgb === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->luminosity($forergb['r'], $backrgb['r'],
|
||||
$forergb['g'], $backrgb['g'],
|
||||
$forergb['b'], $backrgb['b']);
|
||||
@@ -227,15 +233,15 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
|
||||
|
||||
/**
|
||||
* Returns the decimal equivalents for a HEX color
|
||||
* Returns the decimal equivalents for a HEX color. Returns null if it cannot be determined.
|
||||
* @param string $color The hex color value
|
||||
* @return array An array where 'r' is the Red value, 'g' is Green, and 'b' is Blue
|
||||
* @return array|null An array where 'r' is the Red value, 'g' is Green, and 'b' is Blue
|
||||
*/
|
||||
public function get_rgb(string $color) {
|
||||
public function get_rgb(string $color): ?array {
|
||||
$color = $this->convert_color($color);
|
||||
$c = str_split($color, 2);
|
||||
if (count($c) != 3) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
$results = ['r' => hexdec($c[0]), 'g' => hexdec($c[1]), 'b' => hexdec($c[2])];
|
||||
return $results;
|
||||
@@ -307,6 +313,12 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
public function get_wai_ert_contrast(string $foreground, string $background): array {
|
||||
$forergb = $this->get_rgb($foreground);
|
||||
$backrgb = $this->get_rgb($background);
|
||||
|
||||
// If get_rgb returns null for either, return 0.
|
||||
if ($forergb === null || $backrgb === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$diffs = $this->get_wai_diffs($forergb, $backrgb);
|
||||
|
||||
return $diffs['red'] + $diffs['green'] + $diffs['blue'];
|
||||
@@ -321,12 +333,18 @@ class brickfield_accessibility_color_test extends brickfield_accessibility_test
|
||||
public function get_wai_ert_brightness(string $foreground, string $background): float {
|
||||
$forergb = $this->get_rgb($foreground);
|
||||
$backrgb = $this->get_rgb($background);
|
||||
|
||||
// If get_rgb returns null for either, return 0.
|
||||
if ($forergb === null || $backrgb === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$color = $this->get_wai_diffs($forergb, $backrgb);
|
||||
return (($color['red'] * 299) + ($color['green'] * 587) + ($color['blue'] * 114)) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wai diffs.
|
||||
* Get the wai differences.
|
||||
* @param array $forergb
|
||||
* @param array $backrgb
|
||||
* @return array
|
||||
|
||||
@@ -248,7 +248,7 @@ $string['printable:toolname'] = 'Summary report';
|
||||
$string['printable:toolshortname'] = 'Summary report';
|
||||
$string['printable:downloadpdf'] = 'Download PDF';
|
||||
$string['printable:printreport'] = 'Printable report';
|
||||
$string['error:nocoursespecified'] = 'This summary report requires a valid course id. Please access the accessibility toolkit from within a course, via the actions menu, which will then supply this required course id.';
|
||||
$string['error:nocoursespecified'] = 'The summary report requires a valid course ID. Please access the accessibility toolkit from within a course.';
|
||||
$string['pdf:filename'] = 'Brickfield_Summaryreport_CourseID-{$a}';
|
||||
|
||||
// Advanced page.
|
||||
|
||||
+171
-7
@@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* tool_brickfield check test.
|
||||
* Class test_css_text_has_contrast test
|
||||
*
|
||||
* @package tool_brickfield
|
||||
* @copyright 2020 onward: Brickfield Education Labs, https://www.brickfield.ie
|
||||
@@ -29,14 +29,79 @@ defined('MOODLE_INTERNAL') || die();
|
||||
require_once('all_checks.php');
|
||||
|
||||
/**
|
||||
* Class test_css_text_has_contrast_testcase
|
||||
* Class test_css_text_has_contrast_test
|
||||
*/
|
||||
class css_text_has_contrast_test extends all_checks {
|
||||
/** @var string Check type */
|
||||
/** @var string The check type. */
|
||||
protected $checktype = 'css_text_has_contrast';
|
||||
|
||||
/** @var string Html fail */
|
||||
private $htmlfail = <<<EOD
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail1 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:#333333; background-color:#000000; font-weight: bold;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail2 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:#333333; background-color:#000000; font-size: 18px;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail3 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:#333333; background-color:#000000; font-size: 18%;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail4 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:#333333; background-color:#000000; font-size: 18em;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail5 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:#333333; background-color:#000000; font-size: 18ex;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $htmlfail6 = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -48,7 +113,7 @@ class css_text_has_contrast_test extends all_checks {
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string Html pass */
|
||||
/** @var string HTML that should not get flagged. */
|
||||
private $htmlpass = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
@@ -61,14 +126,113 @@ EOD;
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML that should get flagged. */
|
||||
private $namecolours = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color: red; background-color: blue;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with invalid colour names. */
|
||||
private $invalidcolours = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color: grog; background-color: numpi;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with invalid colour numeric values. */
|
||||
private $invalidvalue = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color: 10000500; background-color: -10234;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/** @var string HTML with empty colour values. */
|
||||
private $emptyvalue = <<<EOD
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN""http://www.w3.org/TR/REC-html40/loose.dtd">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OAC Testfile - Check #6 - Positive</title>
|
||||
</head>
|
||||
<body>
|
||||
<p style="color:; background-color:;">This is not contrasty enough.</p>
|
||||
</body>
|
||||
</html>
|
||||
EOD;
|
||||
|
||||
/**
|
||||
* Test for the area assign intro
|
||||
*/
|
||||
public function test_check() {
|
||||
$results = $this->get_checker_results($this->htmlfail);
|
||||
$results = $this->get_checker_results($this->htmlfail1);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlfail2);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlfail3);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlfail4);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlfail5);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlfail6);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
|
||||
$results = $this->get_checker_results($this->htmlpass);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with valid colour names.
|
||||
*/
|
||||
public function test_check_for_namedcolours() {
|
||||
$results = $this->get_checker_results($this->namecolours);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with invalid colour names.
|
||||
*/
|
||||
public function test_check_for_invalidcolours() {
|
||||
$results = $this->get_checker_results($this->invalidcolours);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with invalid colour numeric values.
|
||||
*/
|
||||
public function test_check_for_invalidvalues() {
|
||||
$results = $this->get_checker_results($this->invalidvalue);
|
||||
$this->assertTrue($results[0]->element->tagName == 'p');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test with empty colour values.
|
||||
*/
|
||||
public function test_check_for_emptyvalues() {
|
||||
$results = $this->get_checker_results($this->emptyvalue);
|
||||
$this->assertEmpty($results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
This files describes API changes in /admin/tool/brickfield/*.
|
||||
|
||||
=== 3.11.7 ===
|
||||
|
||||
* classes/local/htmlchecker/common/brickfield_accessibility_color_test::get_rgb() has been modified to return either an
|
||||
array or null. Previously it returned either an array or false.
|
||||
@@ -36,7 +36,7 @@ class tool_customlang_utils {
|
||||
* Rough number of strings that are being processed during a full checkout.
|
||||
* This is used to estimate the progress of the checkout.
|
||||
*/
|
||||
const ROUGH_NUMBER_OF_STRINGS = 16500;
|
||||
const ROUGH_NUMBER_OF_STRINGS = 32000;
|
||||
|
||||
/** @var array cache of {@link self::list_components()} results */
|
||||
private static $components = null;
|
||||
@@ -91,9 +91,22 @@ class tool_customlang_utils {
|
||||
public static function checkout($lang, progress_bar $progressbar = null) {
|
||||
global $DB;
|
||||
|
||||
// For behat executions we are going to load only a few components in the
|
||||
// language customisation structures. Using the whole "en" langpack is
|
||||
// too much slow (leads to Selenium 30s timeouts, especially on slow
|
||||
// environments) and we don't really need the whole thing for tests. So,
|
||||
// apart from escaping from the timeouts, we are also saving some good minutes
|
||||
// in tests. See MDL-70014 and linked issues for more info.
|
||||
$behatneeded = ['core', 'core_langconfig', 'tool_customlang'];
|
||||
|
||||
// make sure that all components are registered
|
||||
$current = $DB->get_records('tool_customlang_components', null, 'name', 'name,version,id');
|
||||
foreach (self::list_components() as $component) {
|
||||
// Filter out unwanted components when running behat.
|
||||
if (defined('BEHAT_SITE_RUNNING') && !in_array($component, $behatneeded)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($current[$component])) {
|
||||
$record = new stdclass();
|
||||
$record->name = $component;
|
||||
@@ -103,7 +116,7 @@ class tool_customlang_utils {
|
||||
$record->version = $version;
|
||||
}
|
||||
$DB->insert_record('tool_customlang_components', $record);
|
||||
} elseif ($version = get_component_version($component)) {
|
||||
} else if ($version = get_component_version($component)) {
|
||||
if (is_null($current[$component]->version) or ($version > $current[$component]->version)) {
|
||||
$DB->set_field('tool_customlang_components', 'version', $version, array('id' => $current[$component]->id));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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 tool_moodlenet\task;
|
||||
|
||||
use core\message\message;
|
||||
|
||||
/**
|
||||
* Ad-hoc task to send a notification to admin stating that the user data related to the linked MoodleNet profiles has
|
||||
* been removed.
|
||||
*
|
||||
* @package tool_moodlenet
|
||||
* @copyright 2022 Mihail Geshoski <mihail@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class send_mnet_profiles_data_removed_notification extends \core\task\adhoc_task {
|
||||
public function execute(): void {
|
||||
$message = new message();
|
||||
$message->component = 'moodle';
|
||||
$message->name = 'notices';
|
||||
$message->userfrom = \core_user::get_noreply_user();
|
||||
$message->userto = get_admin();
|
||||
$message->notification = 1;
|
||||
$message->subject = get_string('removedmnetprofilenotification_subject', 'tool_moodlenet');
|
||||
$message->fullmessageformat = FORMAT_HTML;
|
||||
$message->fullmessagehtml = get_string('removedmnetprofilenotification', 'tool_moodlenet');
|
||||
$message->smallmessage = strip_tags($message->fullmessagehtml);
|
||||
message_send($message);
|
||||
}
|
||||
}
|
||||
@@ -129,5 +129,24 @@ function xmldb_tool_moodlenet_upgrade(int $oldversion) {
|
||||
// Automatically generated Moodle v3.11.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2021051701) {
|
||||
|
||||
$selectsql = "moodlenetprofile IS NOT NULL AND moodlenetprofile != ''";
|
||||
|
||||
// If there are any users with MoodleNet profile set.
|
||||
if ($DB->count_records_select('user', $selectsql)) {
|
||||
// Remove the value set for the MoodleNet profile as this format can no longer be used to authenticate
|
||||
// MoodleNet users.
|
||||
$DB->set_field_select('user', 'moodlenetprofile', '', $selectsql);
|
||||
|
||||
// Use an adhoc task to send a notification to admin stating that the user data related to the linked
|
||||
// MoodleNet profiles has been removed.
|
||||
$notificationtask = new tool_moodlenet\task\send_mnet_profiles_data_removed_notification();
|
||||
core\task\manager::queue_adhoc_task($notificationtask);
|
||||
}
|
||||
|
||||
upgrade_plugin_savepoint(true, 2021051701, 'tool', 'moodlenet');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['addingaresource'] = 'Adding content from MoodleNet';
|
||||
$string['aria:enterprofile'] = "Enter your MoodleNet profile URL";
|
||||
$string['aria:enterprofile'] = "Enter your MoodleNet profile ID";
|
||||
$string['aria:footermessage'] = "Browse for content on MoodleNet";
|
||||
$string['browsecontentmoodlenet'] = "Or browse for content on MoodleNet";
|
||||
$string['clearsearch'] = "Clear search";
|
||||
@@ -39,11 +39,11 @@ $string['defaultmoodlenetname_desc'] = 'The name of the MoodleNet instance avail
|
||||
$string['enablemoodlenet'] = 'Enable MoodleNet integration';
|
||||
$string['enablemoodlenet_desc'] = 'If enabled, a user with the capability to create and manage activities can browse MoodleNet via the activity chooser and import MoodleNet resources into their course. In addition, a user with the capability to restore backups can select a backup file on MoodleNet and restore it into Moodle.';
|
||||
$string['errorduringdownload'] = 'An error occurred while downloading the file: {$a}';
|
||||
$string['forminfo'] = 'Your MoodleNet profile will be automatically saved in your profile on this site.';
|
||||
$string['forminfo'] = 'Your MoodleNet profile ID will be automatically saved in your profile on this site.';
|
||||
$string['footermessage'] = "Or browse for content on";
|
||||
$string['instancedescription'] = "MoodleNet is an open social media platform for educators, with a focus on the collaborative curation of collections of open resources. ";
|
||||
$string['instanceplaceholder'] = '@yourprofile@moodle.net';
|
||||
$string['inputhelp'] = 'Or if you have a MoodleNet account already, enter your MoodleNet profile:';
|
||||
$string['instanceplaceholder'] = 'a1b2c3d4e5f6-example@moodle.net';
|
||||
$string['inputhelp'] = 'Or if you have a MoodleNet account already, copy the ID from your MoodleNet profile and paste it here:';
|
||||
$string['invalidmoodlenetprofile'] = '$userprofile is not correctly formatted';
|
||||
$string['importconfirm'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into the course "{$a->coursename}". Are you sure you want to continue?';
|
||||
$string['importconfirmnocourse'] = 'You are about to import the content "{$a->resourcename} ({$a->resourcetype})" into your site. Are you sure you want to continue?';
|
||||
@@ -56,12 +56,14 @@ $string['mnetprofiledesc'] = '<p>Enter your MoodleNet profile details here to be
|
||||
$string['moodlenetsettings'] = 'MoodleNet settings';
|
||||
$string['moodlenetnotenabled'] = 'The MoodleNet integration must be enabled in Site administration / MoodleNet before resource imports can be processed.';
|
||||
$string['notification'] = 'You are about to import the content "{$a->name} ({$a->type})" into your site. Select the course in which it should be added, or <a href="{$a->cancellink}">cancel</a>.';
|
||||
$string['removedmnetprofilenotification'] = 'Due to recent changes on the MoodleNet platform, any users who previously saved their MoodleNet profile ID on the site will need to enter a MoodleNet profile ID in the new format in order to authenticate on the MoodleNet platform.';
|
||||
$string['removedmnetprofilenotification_subject'] = 'MoodleNet profile ID format change';
|
||||
$string['searchcourses'] = "Search courses";
|
||||
$string['selectpagetitle'] = 'Select page';
|
||||
$string['pluginname'] = 'MoodleNet';
|
||||
$string['privacy:metadata'] = "The MoodleNet tool only facilitates communication with MoodleNet. It stores no data.";
|
||||
$string['profilevalidationerror'] = 'There was a problem trying to validate your profile';
|
||||
$string['profilevalidationfail'] = 'Please enter a valid MoodleNet profile';
|
||||
$string['profilevalidationerror'] = 'There was a problem trying to validate your MoodleNet profile ID';
|
||||
$string['profilevalidationfail'] = 'Please enter a valid MoodleNet profile ID';
|
||||
$string['profilevalidationpass'] = 'Looks good!';
|
||||
$string['saveandgo'] = "Save and go";
|
||||
$string['uploadlimitexceeded'] = 'The file size {$a->filesize} exceeds the user upload limit of {$a->uploadlimit} bytes.';
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'tool_moodlenet';
|
||||
$plugin->version = 2021051700;
|
||||
$plugin->version = 2021051701;
|
||||
$plugin->requires = 2021051100;
|
||||
$plugin->maturity = MATURITY_ALPHA;
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['acceptrisk'] = 'I understand that disabling email verification can be a security issue';
|
||||
$string['acceptrisk_help'] = 'Disabling email verification can be a security issue. As it potentially allows users to authenticate as another in the right circumstances.';
|
||||
$string['acceptrisk'] = 'I understand that disabling email verification can be a security issue.';
|
||||
$string['acceptrisk_help'] = 'Disabling email verification can potentially allow a user to authenticate as another user.';
|
||||
$string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.';
|
||||
$string['authconnected'] = 'The system account is now connected for offline access';
|
||||
$string['authnotconnected'] = 'The system account was not connected for offline access';
|
||||
|
||||
@@ -16,10 +16,10 @@ Feature: OAuth2 email verification
|
||||
| Name | Testing service |
|
||||
| Client ID | thisistheclientid |
|
||||
| Client secret | supersecret |
|
||||
Then I should not see "I understand that disabling email verification can be a security issue"
|
||||
Then I should not see "I understand that disabling email verification can be a security issue."
|
||||
And I click on "Require email verification" "checkbox"
|
||||
And I should see "I understand that disabling email verification can be a security issue"
|
||||
And I click on "I understand that disabling email verification can be a security issue" "checkbox"
|
||||
And I should see "I understand that disabling email verification can be a security issue."
|
||||
And I click on "I understand that disabling email verification can be a security issue." "checkbox"
|
||||
And I press "Save changes"
|
||||
And I should see "Changes saved"
|
||||
And I click on "Edit" "link" in the "Testing service" "table_row"
|
||||
|
||||
@@ -54,12 +54,16 @@ if (!$task) {
|
||||
throw new moodle_exception('cannotfindinfo', 'error', new moodle_url('/admin/tool/task/scheduledtasks.php'), $taskname);
|
||||
}
|
||||
|
||||
if (!\core\task\manager::is_runnable()) {
|
||||
$redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
|
||||
throw new moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
|
||||
}
|
||||
|
||||
$plugininfo = core_plugin_manager::instance()->get_plugin_info($task->get_component());
|
||||
$plugindisabled = $plugininfo && $plugininfo->is_enabled() === false &&
|
||||
!$task->get_run_if_component_disabled();
|
||||
|
||||
$canruntasks = \core\task\manager::is_runnable() && get_config('tool_task', 'enablerunnow');
|
||||
if (!$canruntasks || $plugindisabled) {
|
||||
if (!get_config('tool_task', 'enablerunnow') || $plugindisabled) {
|
||||
throw new moodle_exception('nopermissions', 'error', new moodle_url('/admin/tool/task/scheduledtasks.php'),
|
||||
get_string('runnow', 'tool_task'), $task->get_name());
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ class tool_uploadcourse_course {
|
||||
/** Outcome of the process: deleting the course */
|
||||
const DO_DELETE = 3;
|
||||
|
||||
/** @var array assignable roles. */
|
||||
protected $assignableroles = [];
|
||||
|
||||
/** @var array Roles context levels. */
|
||||
protected $contextlevels = [];
|
||||
|
||||
/** @var array final import data. */
|
||||
protected $data = array();
|
||||
|
||||
@@ -794,16 +800,15 @@ class tool_uploadcourse_course {
|
||||
|
||||
// Get enrolment data. Where the course already exists, we can also perform validation.
|
||||
$this->enrolmentdata = tool_uploadcourse_helper::get_enrolment_data($this->rawdata);
|
||||
if ($exists) {
|
||||
$errors = $this->validate_enrolment_data($coursedata['id'], $this->enrolmentdata);
|
||||
$courseid = $coursedata['id'] ?? 0;
|
||||
$errors = $this->validate_enrolment_data($courseid, $this->enrolmentdata);
|
||||
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $key => $message) {
|
||||
$this->error($key, $message);
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $key => $message) {
|
||||
$this->error($key, $message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->rawdata['tags']) && strval($this->rawdata['tags']) !== '') {
|
||||
@@ -913,6 +918,8 @@ class tool_uploadcourse_course {
|
||||
* @return lang_string[] Errors keyed on error code
|
||||
*/
|
||||
protected function validate_enrolment_data(int $courseid, array $enrolmentdata): array {
|
||||
global $DB;
|
||||
|
||||
// Nothing to validate.
|
||||
if (empty($enrolmentdata)) {
|
||||
return [];
|
||||
@@ -924,46 +931,67 @@ class tool_uploadcourse_course {
|
||||
$instances = enrol_get_instances($courseid, false);
|
||||
|
||||
foreach ($enrolmentdata as $method => $options) {
|
||||
$plugin = $enrolmentplugins[$method];
|
||||
|
||||
// Find matching instances by enrolment method.
|
||||
$methodinstances = array_filter($instances, static function(stdClass $instance) use ($method) {
|
||||
return (strcmp($instance->enrol, $method) == 0);
|
||||
});
|
||||
if (isset($options['role'])) {
|
||||
$role = $options['role'];
|
||||
if ($courseid) {
|
||||
if (!$this->validate_role_context($courseid, $role)) {
|
||||
$errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role);
|
||||
|
||||
if (!empty($options['delete'])) {
|
||||
// Ensure user is able to delete the instances.
|
||||
foreach ($methodinstances as $methodinstance) {
|
||||
if (!$plugin->can_delete_instance($methodinstance)) {
|
||||
$errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment', 'tool_uploadcourse',
|
||||
$plugin->get_instance_name($methodinstance));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// We can at least check that context level is correct while actual context not exist.
|
||||
$roleid = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST);
|
||||
if (!$this->validate_role_context_level($roleid)) {
|
||||
$errors['contextrolenotallowed'] = new lang_string('contextrolenotallowed', 'core_role', $role);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!empty($options['disable'])) {
|
||||
// Ensure user is able to toggle instance statuses.
|
||||
foreach ($methodinstances as $methodinstance) {
|
||||
if (!$plugin->can_hide_show_instance($methodinstance)) {
|
||||
$errors['errorcannotdisableenrolment'] =
|
||||
new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
|
||||
}
|
||||
|
||||
if ($courseid) {
|
||||
$plugin = $enrolmentplugins[$method];
|
||||
|
||||
// Find matching instances by enrolment method.
|
||||
$methodinstances = array_filter($instances, static function (stdClass $instance) use ($method) {
|
||||
return (strcmp($instance->enrol, $method) == 0);
|
||||
});
|
||||
|
||||
if (!empty($options['delete'])) {
|
||||
// Ensure user is able to delete the instances.
|
||||
foreach ($methodinstances as $methodinstance) {
|
||||
if (!$plugin->can_delete_instance($methodinstance)) {
|
||||
$errors['errorcannotdeleteenrolment'] = new lang_string('errorcannotdeleteenrolment',
|
||||
'tool_uploadcourse', $plugin->get_instance_name($methodinstance));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (!empty($options['disable'])) {
|
||||
// Ensure user is able to toggle instance statuses.
|
||||
foreach ($methodinstances as $methodinstance) {
|
||||
if (!$plugin->can_hide_show_instance($methodinstance)) {
|
||||
$errors['errorcannotdisableenrolment'] =
|
||||
new lang_string('errorcannotdisableenrolment', 'tool_uploadcourse',
|
||||
$plugin->get_instance_name($methodinstance));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure user is able to create/update instance.
|
||||
$methodinstance = empty($methodinstances) ? null : reset($methodinstances);
|
||||
if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) ||
|
||||
(!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) {
|
||||
|
||||
$errors['errorcannotcreateorupdateenrolment'] =
|
||||
new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
|
||||
$plugin->get_instance_name($methodinstance));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Ensure user is able to create/update instance.
|
||||
$methodinstance = empty($methodinstances) ? null : reset($methodinstances);
|
||||
if ((empty($methodinstance) && !$plugin->can_add_instance($courseid)) ||
|
||||
(!empty($methodinstance) && !$plugin->can_edit_instance($methodinstance))) {
|
||||
|
||||
$errors['errorcannotcreateorupdateenrolment'] =
|
||||
new lang_string('errorcannotcreateorupdateenrolment', 'tool_uploadcourse',
|
||||
$plugin->get_instance_name($methodinstance));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,8 +1107,15 @@ class tool_uploadcourse_course {
|
||||
$instance->enrolenddate = $instance->enrolstartdate;
|
||||
}
|
||||
|
||||
// Sort out the given role. This does not filter the roles allowed in the course.
|
||||
// Sort out the given role.
|
||||
if (isset($method['role'])) {
|
||||
$role = $method['role'];
|
||||
if (!$this->validate_role_context($course->id, $role)) {
|
||||
$this->error('contextrolenotallowed',
|
||||
new lang_string('contextrolenotallowed', 'core_role', $role));
|
||||
break;
|
||||
}
|
||||
|
||||
$roleids = tool_uploadcourse_helper::get_role_ids();
|
||||
if (isset($roleids[$method['role']])) {
|
||||
$instance->roleid = $roleids[$method['role']];
|
||||
@@ -1093,6 +1128,41 @@ class tool_uploadcourse_course {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is allowed in course context
|
||||
*
|
||||
* @param int $courseid course context.
|
||||
* @param string $role Role.
|
||||
* @return bool
|
||||
*/
|
||||
protected function validate_role_context(int $courseid, string $role) : bool {
|
||||
if (empty($this->assignableroles[$courseid])) {
|
||||
$coursecontext = \context_course::instance($courseid);
|
||||
$this->assignableroles[$courseid] = get_assignable_roles($coursecontext, ROLENAME_SHORT);
|
||||
}
|
||||
if (!in_array($role, $this->assignableroles[$courseid])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if role is allowed at this context level.
|
||||
*
|
||||
* @param int $roleid Role ID.
|
||||
* @return bool
|
||||
*/
|
||||
protected function validate_role_context_level(int $roleid) : bool {
|
||||
if (empty($this->contextlevels[$roleid])) {
|
||||
$this->contextlevels[$roleid] = get_role_contextlevels($roleid);
|
||||
}
|
||||
|
||||
if (!in_array(CONTEXT_COURSE, $this->contextlevels[$roleid])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current course.
|
||||
*
|
||||
|
||||
@@ -325,6 +325,11 @@ class tool_uploadcourse_helper {
|
||||
continue;
|
||||
}
|
||||
$rolenames['role_' . $rolesids[$matches[1]]] = $value;
|
||||
} else if (preg_match('/^(.+)?_role$/', $field, $matches)) {
|
||||
if (!isset($rolesids[$value])) {
|
||||
$invalidroles[] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -219,6 +219,10 @@ class tool_uploadcourse_processor {
|
||||
|
||||
$data = array_merge($data, $course->get_data(), array('id' => $course->get_id()));
|
||||
$tracker->output($this->linenb, true, $status, $data);
|
||||
if ($course->has_errors()) {
|
||||
$errors++;
|
||||
$tracker->output($this->linenb, false, $course->get_errors(), $data);
|
||||
}
|
||||
} else {
|
||||
$errors++;
|
||||
$tracker->output($this->linenb, false, $course->get_errors(), $data);
|
||||
|
||||
@@ -105,3 +105,28 @@ Feature: An admin can create courses using a CSV file
|
||||
And I should see "Field 3: b"
|
||||
And I should see "Field 4: Hello"
|
||||
And I should see "Field 5: Some text"
|
||||
|
||||
@javascript
|
||||
Scenario: Validation of role for uploaded courses
|
||||
Given I navigate to "Users > Permissions > Define roles" in site administration
|
||||
And I click on "Add a new role" "button"
|
||||
And I click on "Continue" "button"
|
||||
And I set the following fields to these values:
|
||||
| Short name | notallowed |
|
||||
| Custom full name | notallowed |
|
||||
| contextlevel80 | 1 |
|
||||
And I click on "Create this role" "button"
|
||||
And I navigate to "Courses > Upload courses" in site administration
|
||||
And I upload "admin/tool/uploadcourse/tests/fixtures/enrolment_role.csv" file to "File" filemanager
|
||||
And I click on "Preview" "button"
|
||||
And I should see "Invalid role names: notexist"
|
||||
And I should see "Role notallowed not allowed in this context."
|
||||
When I click on "Upload courses" "button"
|
||||
And I should see "Course created"
|
||||
And I should see "Courses total: 3"
|
||||
And I should see "Courses created: 1"
|
||||
And I should see "Courses errors: 2"
|
||||
And I should see "Invalid role names: notexist"
|
||||
And I should see "Role notallowed not allowed in this context."
|
||||
And I am on site homepage
|
||||
And I should see "coursez"
|
||||
|
||||
@@ -1561,6 +1561,94 @@ class course_test extends \advanced_testcase {
|
||||
$this->assertArrayHasKey('cannotrenameshortnamealreadyinuse', $co->get_errors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test when role doesn't exist.
|
||||
*
|
||||
* @covers \tool_uploadcourse_course::prepare
|
||||
*/
|
||||
public function test_role_not_exist() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => 'Testing',
|
||||
'shortname' => 'T101',
|
||||
'enrolment_1' => 'manual',
|
||||
'enrolment_1_role' => 'notexist'
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('invalidroles', $upload->get_errors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test when role not allowed in course context.
|
||||
*
|
||||
* @covers \tool_uploadcourse_course::proceed
|
||||
*/
|
||||
public function test_role_not_allowed() {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$roleid = create_role('New student role', 'student2', 'New student description', 'student');
|
||||
set_role_contextlevels($roleid, [CONTEXT_BLOCK]);
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_CREATE_NEW;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_NOTHING;
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'category' => 1,
|
||||
'fullname' => 'Testing',
|
||||
'shortname' => 'T101',
|
||||
'enrolment_1' => 'manual',
|
||||
'enrolment_1_role' => 'student2'
|
||||
]);
|
||||
|
||||
$this->assertFalse($upload->prepare());
|
||||
$this->assertArrayHasKey('contextrolenotallowed', $upload->get_errors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test when role is allowed.
|
||||
*
|
||||
* @covers \tool_uploadcourse_course::proceed
|
||||
*/
|
||||
public function test_role_allowed() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$mode = tool_uploadcourse_processor::MODE_UPDATE_ONLY;
|
||||
$updatemode = tool_uploadcourse_processor::UPDATE_MISSING_WITH_DATA_OR_DEFAUTLS;
|
||||
|
||||
$course = $this->getDataGenerator()->create_course([
|
||||
'shortname' => 'c1',
|
||||
]);
|
||||
|
||||
$instances = enrol_get_instances($course->id, true);
|
||||
$studentrole = $DB->get_record('role', ['shortname' => 'student']);
|
||||
$teacherrole = $DB->get_record('role', ['shortname' => 'teacher']);
|
||||
$instance = reset($instances);
|
||||
$this->assertEquals($studentrole->id, $instance->roleid);
|
||||
|
||||
$upload = new tool_uploadcourse_course($mode, $updatemode, [
|
||||
'shortname' => 'c1',
|
||||
'enrolment_1' => 'manual',
|
||||
'enrolment_1_role' => 'teacher'
|
||||
]);
|
||||
|
||||
$this->assertTrue($upload->prepare());
|
||||
$upload->proceed();
|
||||
$instances = enrol_get_instances($course->id, true);
|
||||
$instance = reset($instances);
|
||||
$this->assertEquals($teacherrole->id, $instance->roleid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom field plugin generator
|
||||
*
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
shortname,fullname,category,enrolment_1,enrolment_1_role
|
||||
CX,coursex,1,manual,notexist
|
||||
CY,coursey,1,manual,notallowed
|
||||
CZ,coursez,1,manual,student
|
||||
|
@@ -692,7 +692,11 @@ class process {
|
||||
$dologout = false;
|
||||
|
||||
if ($this->get_update_type() != UU_UPDATE_NOCHANGES and !$remoteuser) {
|
||||
if (!empty($user->auth) and $user->auth !== $existinguser->auth) {
|
||||
|
||||
// Handle 'auth' column separately, the field can never be missing from a user.
|
||||
if (!empty($user->auth) && ($user->auth !== $existinguser->auth) &&
|
||||
($this->get_update_type() != UU_UPDATE_MISSING)) {
|
||||
|
||||
$this->upt->track('auth', s($existinguser->auth).'-->'.s($user->auth), 'info', false);
|
||||
$existinguser->auth = $user->auth;
|
||||
if (!isset($this->supportedauths[$user->auth])) {
|
||||
|
||||
@@ -18,6 +18,7 @@ body div[data-flexitour="step-background-fader"],
|
||||
body div[data-flexitour="step-background"] {
|
||||
position: absolute;
|
||||
background: inherit;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
div[data-flexitour="step-background-fader"],
|
||||
|
||||
@@ -34,6 +34,17 @@ class auth_db_testcase extends advanced_testcase {
|
||||
/** @var int The amount of users to create for the large user set deletion test */
|
||||
protected $largedeletionsetsize = 128;
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $DB;
|
||||
// Apply sqlsrv native driver error and logging default
|
||||
// settings while finishing the AdoDB tests.
|
||||
if ($DB->get_dbfamily() === 'mssql') {
|
||||
sqlsrv_configure("WarningsReturnAsErrors", false);
|
||||
sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
|
||||
sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected function init_auth_database() {
|
||||
global $DB, $CFG;
|
||||
require_once("$CFG->dirroot/auth/db/auth.php");
|
||||
|
||||
@@ -408,6 +408,11 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
'shortname', 'type', 'value', 'valueformat'
|
||||
));
|
||||
|
||||
$courseformatoptions = new backup_nested_element('courseformatoptions');
|
||||
$courseformatoption = new backup_nested_element('courseformatoption', [], [
|
||||
'courseid', 'format', 'sectionid', 'name', 'value'
|
||||
]);
|
||||
|
||||
// attach format plugin structure to $course element, only one allowed
|
||||
$this->add_plugin_structure('format', $course, false);
|
||||
|
||||
@@ -445,17 +450,14 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
$course->add_child($customfields);
|
||||
$customfields->add_child($customfield);
|
||||
|
||||
$course->add_child($courseformatoptions);
|
||||
$courseformatoptions->add_child($courseformatoption);
|
||||
|
||||
// Set the sources
|
||||
|
||||
$courserec = $DB->get_record('course', array('id' => $this->task->get_courseid()));
|
||||
$courserec->contextid = $this->task->get_contextid();
|
||||
|
||||
$formatoptions = course_get_format($courserec)->get_format_options();
|
||||
$course->add_final_elements(array_keys($formatoptions));
|
||||
foreach ($formatoptions as $key => $value) {
|
||||
$courserec->$key = $value;
|
||||
}
|
||||
|
||||
// Add 'numsections' in order to be able to restore in previous versions of Moodle.
|
||||
// Even though Moodle does not officially support restore into older verions of Moodle from the
|
||||
// version where backup was made, without 'numsections' restoring will go very wrong.
|
||||
@@ -477,6 +479,10 @@ class backup_course_structure_step extends backup_structure_step {
|
||||
backup_helper::is_sqlparam('course'),
|
||||
backup::VAR_PARENTID));
|
||||
|
||||
$courseformatoption->set_source_sql('SELECT id, format, sectionid, name, value
|
||||
FROM {course_format_options}
|
||||
WHERE courseid = ?', [ backup::VAR_PARENTID ]);
|
||||
|
||||
$handler = core_course\customfield\course_handler::create();
|
||||
$fieldsforbackup = $handler->get_instance_data_for_backup($this->task->get_courseid());
|
||||
$customfield->set_source_array($fieldsforbackup);
|
||||
|
||||
@@ -1806,7 +1806,8 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
$category = new restore_path_element('category', '/course/category');
|
||||
$tag = new restore_path_element('tag', '/course/tags/tag');
|
||||
$customfield = new restore_path_element('customfield', '/course/customfields/customfield');
|
||||
$allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
|
||||
$courseformatoptions = new restore_path_element('course_format_option', '/course/courseformatoptions/courseformatoption');
|
||||
$allowedmodule = new restore_path_element('allowed_module', '/course/allowed_modules/module');
|
||||
|
||||
// Apply for 'format' plugins optional paths at course level
|
||||
$this->add_plugin_structure('format', $course);
|
||||
@@ -1829,7 +1830,7 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
// Apply for admin tool plugins optional paths at course level.
|
||||
$this->add_plugin_structure('tool', $course);
|
||||
|
||||
return array($course, $category, $tag, $customfield, $allowed_module);
|
||||
return array($course, $category, $tag, $customfield, $allowedmodule, $courseformatoptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1951,8 +1952,6 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
// Course record ready, update it
|
||||
$DB->update_record('course', $data);
|
||||
|
||||
course_get_format($data)->update_course_format_options($data);
|
||||
|
||||
// Role name aliases
|
||||
restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
|
||||
}
|
||||
@@ -1980,6 +1979,26 @@ class restore_course_structure_step extends restore_structure_step {
|
||||
$handler->restore_instance_data_from_backup($this->task, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a course format option.
|
||||
*
|
||||
* @param array $data The record being restored.
|
||||
* @throws base_step_exception
|
||||
* @throws dml_exception
|
||||
*/
|
||||
public function process_course_format_option(array $data) : void {
|
||||
global $DB;
|
||||
|
||||
$courseid = $this->get_courseid();
|
||||
$record = $DB->get_record('course_format_options', [ 'courseid' => $courseid, 'name' => $data['name'] ], 'id');
|
||||
if ($record !== false) {
|
||||
$DB->update_record('course_format_options', (object) [ 'id' => $record->id, 'value' => $data['value'] ]);
|
||||
} else {
|
||||
$data['courseid'] = $courseid;
|
||||
$DB->insert_record('course_format_options', (object) $data);
|
||||
}
|
||||
}
|
||||
|
||||
public function process_allowed_module($data) {
|
||||
$data = (object)$data;
|
||||
|
||||
|
||||
@@ -248,8 +248,11 @@ class award_criteria_activity extends award_criteria {
|
||||
if (!empty($moduledata)) {
|
||||
$extraon = implode(' OR ', $moduledata);
|
||||
$join = " JOIN {course_modules_completion} cmc ON cmc.userid = u.id AND
|
||||
( cmc.completionstate = :completionpass OR cmc.completionstate = :completioncomplete ) AND ({$extraon})";
|
||||
( cmc.completionstate = :completionfail OR
|
||||
cmc.completionstate = :completionpass OR
|
||||
cmc.completionstate = :completioncomplete ) AND ({$extraon})";
|
||||
$params["completionpass"] = COMPLETION_COMPLETE_PASS;
|
||||
$params["completionfail"] = COMPLETION_COMPLETE_FAIL;
|
||||
$params["completioncomplete"] = COMPLETION_COMPLETE;
|
||||
}
|
||||
return array($join, $where, $params);
|
||||
@@ -259,10 +262,12 @@ class award_criteria_activity extends award_criteria {
|
||||
cmc{$param['module']}.userid = u.id AND
|
||||
cmc{$param['module']}.coursemoduleid = :completedmodule{$param['module']} AND
|
||||
( cmc{$param['module']}.completionstate = :completionpass{$param['module']} OR
|
||||
cmc{$param['module']}.completionstate = :completionfail{$param['module']} OR
|
||||
cmc{$param['module']}.completionstate = :completioncomplete{$param['module']} )";
|
||||
$where .= " AND cmc{$param['module']}.coursemoduleid IS NOT NULL ";
|
||||
$params["completedmodule{$param['module']}"] = $param['module'];
|
||||
$params["completionpass{$param['module']}"] = COMPLETION_COMPLETE_PASS;
|
||||
$params["completionfail{$param['module']}"] = COMPLETION_COMPLETE_FAIL;
|
||||
$params["completioncomplete{$param['module']}"] = COMPLETION_COMPLETE;
|
||||
}
|
||||
return array($join, $where, $params);
|
||||
|
||||
@@ -202,8 +202,8 @@ class award_criteria_profile extends award_criteria {
|
||||
$join .= " LEFT JOIN {user_info_data} uid{$idx} ON uid{$idx}.userid = u.id AND uid{$idx}.fieldid = :fieldid{$idx} ";
|
||||
$sqlparams["fieldid{$idx}"] = $param['field'];
|
||||
$whereparts[] = "uid{$idx}.id IS NOT NULL";
|
||||
} else {
|
||||
// This is a field from {user} table.
|
||||
} else if (in_array($param['field'], $this->allowed_default_fields)) {
|
||||
// This is a valid field from {user} table.
|
||||
if ($param['field'] == 'picture') {
|
||||
// The picture field is numeric and requires special handling.
|
||||
$whereparts[] = "u.{$param['field']} != 0";
|
||||
|
||||
@@ -8,6 +8,7 @@ Feature: Award badges based on activity completion
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 1 | student2@example.com |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category | enablecompletion |
|
||||
@@ -16,6 +17,7 @@ Feature: Award badges based on activity completion
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following config values are set as admin:
|
||||
| grade_item_advanced | hiddenuntil |
|
||||
And the following "question categories" exist:
|
||||
@@ -33,6 +35,9 @@ Feature: Award badges based on activity completion
|
||||
And user "student1" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And user "student2" has attempted "Test quiz name" with responses:
|
||||
| slot | response |
|
||||
| 1 | False |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Badges > Add a new badge" in current page administration
|
||||
@@ -42,6 +47,12 @@ Feature: Award badges based on activity completion
|
||||
| Description | Course badge description |
|
||||
And I upload "badges/tests/behat/badge.png" file to "Image" filemanager
|
||||
And I press "Create badge"
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I follow "Criteria"
|
||||
And I set the field "type" to "Activity completion"
|
||||
And I set the field "Quiz - Test quiz name" to "1"
|
||||
And I press "Save"
|
||||
@@ -49,10 +60,7 @@ Feature: Award badges based on activity completion
|
||||
And I press "Continue"
|
||||
And I should see "Recipients (0)"
|
||||
And I log out
|
||||
|
||||
Scenario: Student earns a badge using activity completion, but does not get passing grade
|
||||
Given I log in as "student1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And the "Receive a grade" completion condition of "Test quiz name" is displayed as "failed"
|
||||
And the "Receive a pass grade or complete all available attempts" completion condition of "Test quiz name" is displayed as "todo"
|
||||
When I am on the "Test quiz name" "quiz activity" page
|
||||
@@ -67,3 +75,47 @@ Feature: Award badges based on activity completion
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
Then I should see "Recipients (1)"
|
||||
|
||||
Scenario Outline: Previously graded pass/fail students should earn a badge after enabling a badge
|
||||
Given I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
And I follow "Criteria"
|
||||
And I set the field "type" to "Activity completion"
|
||||
And I click on "Expand all" "link"
|
||||
And I set the field "Quiz - Test quiz name" to "1"
|
||||
And I set the field "<aggregationcriteria>" to "1"
|
||||
And I press "Save"
|
||||
|
||||
# Fail grade with student2
|
||||
And I am on the "Course 1" course page logged in as student2
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "1"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
|
||||
And I log out
|
||||
|
||||
# Pass grade with student1
|
||||
And I am on the "Course 1" course page logged in as student1
|
||||
And I am on the "Test quiz name" "quiz activity" page
|
||||
And I press "Re-attempt quiz"
|
||||
And I set the field "False" to "0"
|
||||
And I press "Finish attempt ..."
|
||||
And I press "Submit all and finish"
|
||||
And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
|
||||
And I log out
|
||||
|
||||
# Enable badge access once all students have completed an activity.
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "Badges > Manage badges" in current page administration
|
||||
And I follow "Course Badge"
|
||||
When I press "Enable access"
|
||||
And I press "Continue"
|
||||
Then I should see "Recipients (2)"
|
||||
|
||||
Examples:
|
||||
| aggregationcriteria |
|
||||
| Any of the selected activities is complete |
|
||||
| All of the selected activities are complete |
|
||||
|
||||
@@ -401,6 +401,12 @@ Feature: Award badges based on cohort
|
||||
| user1 | First | User | first@example.com |
|
||||
| user2 | Second | User | second@example.com |
|
||||
| user3 | Third | User | third@example.com |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user1 | CH2 |
|
||||
| user2 | CH2 |
|
||||
| user2 | CH3 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
@@ -415,7 +421,7 @@ Feature: Award badges based on cohort
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
When I press "Continue"
|
||||
And I should see "Recipients (0)"
|
||||
And I should see "Recipients (1)"
|
||||
And I navigate to "Badges > Add a new badge" in site administration
|
||||
And I set the following fields to these values:
|
||||
| Name | Site Badge 2 |
|
||||
@@ -429,11 +435,7 @@ Feature: Award badges based on cohort
|
||||
And I press "Save"
|
||||
And I press "Enable access"
|
||||
And I press "Continue"
|
||||
Then I navigate to "Users > Accounts >Cohorts" in site administration
|
||||
And I add "First User (first@example.com)" user to "CH1" cohort members
|
||||
And I add "First User (first@example.com)" user to "CH2" cohort members
|
||||
And I add "Second User (second@example.com)" user to "CH2" cohort members
|
||||
And I add "Second User (second@example.com)" user to "CH3" cohort members
|
||||
And I should see "Recipients (1)"
|
||||
And I log out
|
||||
And I log in as "user1"
|
||||
And I follow "Profile" in the user menu
|
||||
|
||||
@@ -35,7 +35,7 @@ $string['aria:coursesummary'] = 'Course summary text:';
|
||||
$string['aria:courseprogress'] = 'Course progress:';
|
||||
$string['aria:customfield'] = 'Show {$a} courses';
|
||||
$string['aria:displaydropdown'] = 'Display drop-down menu';
|
||||
$string['aria:favourites'] = 'Show starred courses';
|
||||
$string['aria:favourites'] = 'Show starred courses only';
|
||||
$string['aria:future'] = 'Show future courses';
|
||||
$string['aria:groupingdropdown'] = 'Grouping drop-down menu';
|
||||
$string['aria:inprogress'] = 'Show courses in progress';
|
||||
|
||||
@@ -35,6 +35,35 @@ Feature: Displaying the link to the Participants page
|
||||
And "Student One" "link" should exist
|
||||
And "Student Two" "link" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Course participants link is displayed to users depending on role permissions settings
|
||||
And I log in as "admin"
|
||||
And I am on "Course1" course homepage with editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
| Forum name | Test forum name |
|
||||
| Description | Test forum description |
|
||||
And I navigate to "Users > Enrolment methods" in current page administration
|
||||
And I click on "Edit" "link" in the "Guest access" "table_row"
|
||||
And I set the following fields to these values:
|
||||
| Allow guest access | Yes |
|
||||
And I press "Save changes"
|
||||
And I log out
|
||||
When I log in as "guest"
|
||||
And I am on "Course1" course homepage
|
||||
Then I should not see "Participants" in the "Navigation" "block"
|
||||
And I am on the "Test forum name" "forum activity" page
|
||||
And I should not see "Participants" in the "Navigation" "block"
|
||||
And I log out
|
||||
And I log in as "admin"
|
||||
And I set the following system permissions of "Guest" role:
|
||||
| capability | permission |
|
||||
| moodle/course:viewparticipants | Allow |
|
||||
And I log in as "guest"
|
||||
And I am on "Course1" course homepage
|
||||
And I should see "Participants" in the "Navigation" "block"
|
||||
And I am on the "Test forum name" "forum activity" page
|
||||
And I should see "Participants" in the "Navigation" "block"
|
||||
|
||||
Scenario: Site participants link is displayed to admins
|
||||
When I log in as "admin"
|
||||
Then "Participants" "link" should exist in the "Navigation" "block"
|
||||
|
||||
@@ -101,6 +101,12 @@ function xmldb_block_tag_youtube_upgrade($oldversion) {
|
||||
// If the block uses a legacy category name, update it to use the current category ID instead.
|
||||
foreach ($blockinstances as $blockinstance) {
|
||||
$blockconfig = unserialize(base64_decode($blockinstance->configdata));
|
||||
|
||||
// Skip if the block does not have a specific category set.
|
||||
if (!isset($blockconfig->category)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$blockcategoryconfig = $blockconfig->category;
|
||||
// The block is using a legacy category name as a category config.
|
||||
if (array_key_exists($blockcategoryconfig, $categorynamemap)) {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -58,7 +58,7 @@ function(
|
||||
LOADING_ICON: 'core/loading'
|
||||
};
|
||||
|
||||
var COURSE_CLASSIFICATION = 'inprogress';
|
||||
var COURSE_CLASSIFICATION = 'all';
|
||||
var COURSE_SORT = 'fullname asc';
|
||||
var COURSE_EVENT_LIMIT = 5;
|
||||
var COURSE_LIMIT = 2;
|
||||
|
||||
@@ -66,10 +66,10 @@ Feature: The timeline block allows users to see upcoming courses
|
||||
And I should see "Test feedback 2 closes" in the "Timeline" "block"
|
||||
And I should see "Test feedback 3 closes" in the "Timeline" "block"
|
||||
And I should see "Test assign 1 is due" in the "Timeline" "block"
|
||||
And I should not see "More courses" in the "Timeline" "block"
|
||||
And I should not see "Course 4" in the "Timeline" "block"
|
||||
And I should see "More courses" in the "Timeline" "block"
|
||||
And I should see "Course 4" in the "Timeline" "block"
|
||||
And I should not see "Test choice 2 closes" in the "Timeline" "block"
|
||||
And I should not see "Test feedback 4 closes" in the "Timeline" "block"
|
||||
And I should see "Test feedback 4 closes" in the "Timeline" "block"
|
||||
|
||||
Scenario: Persistent sort filter
|
||||
Given I log in as "student1"
|
||||
@@ -107,10 +107,10 @@ Feature: The timeline block allows users to see upcoming courses
|
||||
And I should see "Test feedback 2 closes" in the "Timeline" "block"
|
||||
And I should see "Test feedback 3 closes" in the "Timeline" "block"
|
||||
And I should see "Test assign 1 is due" in the "Timeline" "block"
|
||||
And I should not see "More courses" in the "Timeline" "block"
|
||||
And I should not see "Course 4" in the "Timeline" "block"
|
||||
And I should see "More courses" in the "Timeline" "block"
|
||||
And I should see "Course 4" in the "Timeline" "block"
|
||||
And I should not see "Test choice 2 closes" in the "Timeline" "block"
|
||||
And I should not see "Test feedback 4 closes" in the "Timeline" "block"
|
||||
And I should see "Test feedback 4 closes" in the "Timeline" "block"
|
||||
|
||||
Scenario: Current filtering always applies in courses view
|
||||
Given I log in as "student1"
|
||||
|
||||
Vendored
+1
-1
@@ -466,7 +466,7 @@ class cachestore_file extends cache_store implements cache_is_key_aware, cache_i
|
||||
*/
|
||||
protected function prep_data_after_read($data) {
|
||||
$result = @unserialize($data);
|
||||
if ($result === false) {
|
||||
if ($result === false && $data != serialize(false)) {
|
||||
throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
|
||||
}
|
||||
return $result;
|
||||
|
||||
@@ -139,6 +139,13 @@ class managesubscriptions extends \moodleform {
|
||||
$errors['url'] = get_string('errorrequiredurlorfile', 'calendar');
|
||||
}
|
||||
|
||||
// Validate course/category event types (ensure appropriate field is also filled in).
|
||||
if ($eventtype === 'course' && empty($data['courseid'])) {
|
||||
$errors['courseid'] = get_string('selectacourse');
|
||||
} else if ($eventtype === 'category' && empty($data['categoryid'])) {
|
||||
$errors['categoryid'] = get_string('required');
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
|
||||
+75
-9
@@ -662,15 +662,13 @@ abstract class format_base {
|
||||
'format' => $this->format,
|
||||
'sectionid' => $sectionid
|
||||
), '', 'id,name,value');
|
||||
|
||||
$indexedrecords = [];
|
||||
foreach ($records as $record) {
|
||||
if (array_key_exists($record->name, $this->formatoptions[$sectionid])) {
|
||||
$value = $record->value;
|
||||
if ($value !== null && isset($options[$record->name]['type'])) {
|
||||
// this will convert string value to number if needed
|
||||
$value = clean_param($value, $options[$record->name]['type']);
|
||||
}
|
||||
$this->formatoptions[$sectionid][$record->name] = $value;
|
||||
}
|
||||
$indexedrecords[$record->name] = $record->value;
|
||||
}
|
||||
foreach ($options as $optionname => $option) {
|
||||
contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -765,7 +763,7 @@ abstract class format_base {
|
||||
$data = array_intersect_key($rawdata, $allformatoptions);
|
||||
foreach ($data as $key => $value) {
|
||||
$option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]];
|
||||
$data[$key] = clean_param($value, $option['type']);
|
||||
expand_value($data, $data, $option, $key);
|
||||
if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) {
|
||||
// Value invalid for select element, skip.
|
||||
unset($data[$key]);
|
||||
@@ -814,6 +812,7 @@ abstract class format_base {
|
||||
if (array_key_exists('default', $option)) {
|
||||
$defaultoptions[$key] = $option['default'];
|
||||
}
|
||||
expand_value($defaultoptions, $defaultoptions, $option, $key);
|
||||
$cached[$key] = ($sectionid === 0 || !empty($option['cache']));
|
||||
}
|
||||
$records = $DB->get_records('course_format_options',
|
||||
@@ -1400,3 +1399,70 @@ class format_site extends format_base {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Converts' a value from what is stored in the database into what is used by edit forms.
|
||||
*
|
||||
* @param array $dest The destination array
|
||||
* @param array $source The source array
|
||||
* @param array $option The definition structure of the option.
|
||||
* @param string $optionname The name of the option, as provided in the definition.
|
||||
* @author Jason den Dulk
|
||||
*/
|
||||
function contract_value(array &$dest, array $source, array $option, string $optionname) : void {
|
||||
if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor.
|
||||
$name = substr($optionname, 0, -7);
|
||||
if (isset($source[$name])) {
|
||||
$dest[$optionname] = [
|
||||
'text' => clean_param_if_not_null($source[$name], $option['type'] ?? PARAM_RAW),
|
||||
'format' => clean_param_if_not_null($source[$name . 'format'], PARAM_INT),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if (isset($source[$optionname])) {
|
||||
$dest[$optionname] = clean_param_if_not_null($source[$optionname], $option['type'] ?? PARAM_RAW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the given param, unless it is null.
|
||||
*
|
||||
* @param mixed $param The variable we are cleaning.
|
||||
* @param string $type Expected format of param after cleaning.
|
||||
* @return mixed Null if $param is null, otherwise the cleaned value.
|
||||
* @throws coding_exception
|
||||
* @author Jason den Dulk
|
||||
*/
|
||||
function clean_param_if_not_null($param, string $type = PARAM_RAW) {
|
||||
if ($param === null) {
|
||||
return null;
|
||||
} else {
|
||||
return clean_param($param, $type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Converts' a value from what is used in edit forms into a value(s) to be stored in the database.
|
||||
*
|
||||
* @param array $dest The destination array
|
||||
* @param array $source The source array
|
||||
* @param array $option The definition structure of the option.
|
||||
* @param string $optionname The name of the option, as provided in the definition.
|
||||
* @author Jason den Dulk
|
||||
*/
|
||||
function expand_value(array &$dest, array $source, array $option, string $optionname) : void {
|
||||
if (substr($optionname, -7) == '_editor') { // Suffix '_editor' indicates that the element is an editor.
|
||||
$name = substr($optionname, 0, -7);
|
||||
if (is_string($source[$optionname])) {
|
||||
$dest[$name] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW);
|
||||
$dest[$name.'format'] = 1;
|
||||
} else {
|
||||
$dest[$name] = clean_param($source[$optionname]['text'], $option['type'] ?? PARAM_RAW);
|
||||
$dest[$name.'format'] = clean_param($source[$optionname]['format'], PARAM_INT);
|
||||
}
|
||||
unset($dest[$optionname]);
|
||||
} else {
|
||||
$dest[$optionname] = clean_param($source[$optionname], $option['type'] ?? PARAM_RAW);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-167
@@ -388,171 +388,7 @@ function course_integrity_check($courseid, $rawmods = null, $sections = null, $f
|
||||
* Each item in the array contains he following properties:
|
||||
*/
|
||||
function get_array_of_activities($courseid) {
|
||||
// cm - course module id
|
||||
// mod - name of the module (eg forum)
|
||||
// section - the number of the section (eg week or topic)
|
||||
// name - the name of the instance
|
||||
// visible - is the instance visible or not
|
||||
// groupingid - grouping id
|
||||
// extra - contains extra string to include in any link
|
||||
global $CFG, $DB;
|
||||
|
||||
$course = $DB->get_record('course', array('id'=>$courseid));
|
||||
|
||||
if (empty($course)) {
|
||||
throw new moodle_exception('courseidnotfound');
|
||||
}
|
||||
|
||||
$mod = array();
|
||||
|
||||
$rawmods = get_course_mods($courseid);
|
||||
if (empty($rawmods)) {
|
||||
return $mod; // always return array
|
||||
}
|
||||
$courseformat = course_get_format($course);
|
||||
|
||||
if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
|
||||
'section ASC', 'id,section,sequence,visible')) {
|
||||
// First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
|
||||
if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) {
|
||||
debugging(join('<br>', $errormessages));
|
||||
$rawmods = get_course_mods($courseid);
|
||||
$sections = $DB->get_records('course_sections', array('course' => $courseid),
|
||||
'section ASC', 'id,section,sequence,visible');
|
||||
}
|
||||
// Build array of activities.
|
||||
foreach ($sections as $section) {
|
||||
if (!empty($section->sequence)) {
|
||||
$sequence = explode(",", $section->sequence);
|
||||
foreach ($sequence as $seq) {
|
||||
if (empty($rawmods[$seq])) {
|
||||
continue;
|
||||
}
|
||||
// Adjust visibleoncoursepage, value in DB may not respect format availability.
|
||||
$rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
|
||||
|| $rawmods[$seq]->visibleoncoursepage
|
||||
|| empty($CFG->allowstealth)
|
||||
|| !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
|
||||
|
||||
// Create an object that will be cached.
|
||||
$mod[$seq] = new stdClass();
|
||||
$mod[$seq]->id = $rawmods[$seq]->instance;
|
||||
$mod[$seq]->cm = $rawmods[$seq]->id;
|
||||
$mod[$seq]->mod = $rawmods[$seq]->modname;
|
||||
|
||||
// Oh dear. Inconsistent names left here for backward compatibility.
|
||||
$mod[$seq]->section = $section->section;
|
||||
$mod[$seq]->sectionid = $rawmods[$seq]->section;
|
||||
|
||||
$mod[$seq]->module = $rawmods[$seq]->module;
|
||||
$mod[$seq]->added = $rawmods[$seq]->added;
|
||||
$mod[$seq]->score = $rawmods[$seq]->score;
|
||||
$mod[$seq]->idnumber = $rawmods[$seq]->idnumber;
|
||||
$mod[$seq]->visible = $rawmods[$seq]->visible;
|
||||
$mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
|
||||
$mod[$seq]->visibleold = $rawmods[$seq]->visibleold;
|
||||
$mod[$seq]->groupmode = $rawmods[$seq]->groupmode;
|
||||
$mod[$seq]->groupingid = $rawmods[$seq]->groupingid;
|
||||
$mod[$seq]->indent = $rawmods[$seq]->indent;
|
||||
$mod[$seq]->completion = $rawmods[$seq]->completion;
|
||||
$mod[$seq]->extra = "";
|
||||
$mod[$seq]->completiongradeitemnumber =
|
||||
$rawmods[$seq]->completiongradeitemnumber;
|
||||
$mod[$seq]->completionview = $rawmods[$seq]->completionview;
|
||||
$mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
|
||||
$mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
|
||||
$mod[$seq]->availability = $rawmods[$seq]->availability;
|
||||
$mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
|
||||
|
||||
$modname = $mod[$seq]->mod;
|
||||
$functionname = $modname."_get_coursemodule_info";
|
||||
|
||||
if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
include_once("$CFG->dirroot/mod/$modname/lib.php");
|
||||
|
||||
if ($hasfunction = function_exists($functionname)) {
|
||||
if ($info = $functionname($rawmods[$seq])) {
|
||||
if (!empty($info->icon)) {
|
||||
$mod[$seq]->icon = $info->icon;
|
||||
}
|
||||
if (!empty($info->iconcomponent)) {
|
||||
$mod[$seq]->iconcomponent = $info->iconcomponent;
|
||||
}
|
||||
if (!empty($info->name)) {
|
||||
$mod[$seq]->name = $info->name;
|
||||
}
|
||||
if ($info instanceof cached_cm_info) {
|
||||
// When using cached_cm_info you can include three new fields
|
||||
// that aren't available for legacy code
|
||||
if (!empty($info->content)) {
|
||||
$mod[$seq]->content = $info->content;
|
||||
}
|
||||
if (!empty($info->extraclasses)) {
|
||||
$mod[$seq]->extraclasses = $info->extraclasses;
|
||||
}
|
||||
if (!empty($info->iconurl)) {
|
||||
// Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
|
||||
$url = new moodle_url($info->iconurl);
|
||||
$mod[$seq]->iconurl = $url->out(false);
|
||||
}
|
||||
if (!empty($info->onclick)) {
|
||||
$mod[$seq]->onclick = $info->onclick;
|
||||
}
|
||||
if (!empty($info->customdata)) {
|
||||
$mod[$seq]->customdata = $info->customdata;
|
||||
}
|
||||
} else {
|
||||
// When using a stdclass, the (horrible) deprecated ->extra field
|
||||
// is available for BC
|
||||
if (!empty($info->extra)) {
|
||||
$mod[$seq]->extra = $info->extra;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// When there is no modname_get_coursemodule_info function,
|
||||
// but showdescriptions is enabled, then we use the 'intro'
|
||||
// and 'introformat' fields in the module table
|
||||
if (!$hasfunction && $rawmods[$seq]->showdescription) {
|
||||
if ($modvalues = $DB->get_record($rawmods[$seq]->modname,
|
||||
array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) {
|
||||
// Set content from intro and introformat. Filters are disabled
|
||||
// because we filter it with format_text at display time
|
||||
$mod[$seq]->content = format_module_intro($rawmods[$seq]->modname,
|
||||
$modvalues, $rawmods[$seq]->id, false);
|
||||
|
||||
// To save making another query just below, put name in here
|
||||
$mod[$seq]->name = $modvalues->name;
|
||||
}
|
||||
}
|
||||
if (!isset($mod[$seq]->name)) {
|
||||
$mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance));
|
||||
}
|
||||
|
||||
// Minimise the database size by unsetting default options when they are
|
||||
// 'empty'. This list corresponds to code in the cm_info constructor.
|
||||
foreach (array('idnumber', 'groupmode', 'groupingid',
|
||||
'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
|
||||
'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
|
||||
'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
|
||||
if (property_exists($mod[$seq], $property) &&
|
||||
empty($mod[$seq]->{$property})) {
|
||||
unset($mod[$seq]->{$property});
|
||||
}
|
||||
}
|
||||
// Special case: this value is usually set to null, but may be 0
|
||||
if (property_exists($mod[$seq], 'completiongradeitemnumber') &&
|
||||
is_null($mod[$seq]->completiongradeitemnumber)) {
|
||||
unset($mod[$seq]->completiongradeitemnumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $mod;
|
||||
return course_modinfo::get_array_of_activities(get_course($courseid));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1378,12 +1214,12 @@ function move_section_to($course, $section, $destination, $ignorenumsections = f
|
||||
// uniqueness constraint
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
foreach ($movedsections as $id => $position) {
|
||||
if ($sections[$id] !== $position) {
|
||||
if ((int) $sections[$id] !== $position) {
|
||||
$DB->set_field('course_sections', 'section', -$position, array('id' => $id));
|
||||
}
|
||||
}
|
||||
foreach ($movedsections as $id => $position) {
|
||||
if ($sections[$id] !== $position) {
|
||||
if ((int) $sections[$id] !== $position) {
|
||||
$DB->set_field('course_sections', 'section', $position, array('id' => $id));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1699,7 +1699,7 @@ class core_course_renderer extends plugin_renderer_base {
|
||||
|
||||
// Only show the collapse/expand if there are children to expand.
|
||||
$content .= html_writer::start_tag('div', array('class' => 'collapsible-actions'));
|
||||
$content .= html_writer::link('#', $linkname, array('class' => implode(' ', $classes)));
|
||||
$content .= html_writer::link('#', $linkname, array('class' => implode(' ', $classes), 'aria-expanded' => false));
|
||||
$content .= html_writer::end_tag('div');
|
||||
$this->page->requires->strings_for_js(array('collapseall', 'expandall'), 'moodle');
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
|
||||
require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
|
||||
require_once($CFG->dirroot . '/course/tests/fixtures/format_theunittest.php');
|
||||
|
||||
/**
|
||||
* Course restore testcase.
|
||||
@@ -534,4 +535,48 @@ class restore_test extends \advanced_testcase {
|
||||
$this->assertEquals($chat2->chattime, $restoredchat2->chattime);
|
||||
$this->assertEquals($c2->startdate + 1 * WEEKSECS, $restoredchat2->chattime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests course restore with editor in course format.
|
||||
*
|
||||
* @author Matthew Hilton
|
||||
*/
|
||||
public function test_restore_editor_courseformat() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Setup user with restore permissions.
|
||||
$dg = $this->getDataGenerator();
|
||||
$u1 = $dg->create_user();
|
||||
|
||||
$managers = get_archetype_roles('manager');
|
||||
$manager = array_shift($managers);
|
||||
$dg->role_assign($manager->id, $u1->id);
|
||||
|
||||
// Create a course with an editor item in the course format.
|
||||
$courseformatoptiondata = (object) [
|
||||
"hideoddsections" => 1,
|
||||
'summary_editor' => [
|
||||
'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
|
||||
'format' => 1
|
||||
]
|
||||
];
|
||||
$course1 = $dg->create_course(['format' => 'theunittest']);
|
||||
$course2 = $dg->create_course(['format' => 'theunittest']);
|
||||
$this->assertEquals('theunittest', $course1->format);
|
||||
course_create_sections_if_missing($course1, array(0, 1));
|
||||
|
||||
// Set the course format.
|
||||
$courseformat = course_get_format($course1);
|
||||
$courseformat->update_course_format_options($courseformatoptiondata);
|
||||
|
||||
// Backup and restore the course.
|
||||
$backupid = $this->backup_course($course1->id);
|
||||
$this->restore_to_existing_course($backupid, $course2->id, $u1->id);
|
||||
|
||||
// Get the restored course format.
|
||||
$restoredformat = course_get_format($course2);
|
||||
$restoredformatoptions = $restoredformat->get_format_options();
|
||||
|
||||
$this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $restoredformatoptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
@core @core_course
|
||||
Feature: Report navigation
|
||||
As a teacher
|
||||
I will be redirected to the first report page on the navigation if I can't access to other reports.
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username |
|
||||
| teacher |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher | C1 | editingteacher |
|
||||
|
||||
Scenario: The teacher will be redirected to the first report page if they can't access to most recently report
|
||||
Given I am on the "C1" "Course" page logged in as "teacher"
|
||||
When I navigate to "Reports > Logs" in current page administration
|
||||
Then I should see "Choose which logs you want to see"
|
||||
When I click on "Reports" "link"
|
||||
Then I should see "Choose which logs you want to see"
|
||||
When the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| report/log:view | Prohibit | editingteacher | System | |
|
||||
And I click on "Reports" "link"
|
||||
Then I should see "Competency breakdown"
|
||||
And I should not see "Sorry, but you do not currently have permissions to do that"
|
||||
|
||||
Scenario: If capability Log view is unset, the teacher will be redirected to the first valid report page
|
||||
Given the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| report/log:view | Prohibit | editingteacher | System | |
|
||||
When I am on the "C1" "Course" page logged in as "teacher"
|
||||
And I navigate to "Reports" in current page administration
|
||||
Then I should see "Competency breakdown"
|
||||
And I should not see "Sorry, but you do not currently have permissions to do that"
|
||||
|
||||
Scenario: A warning message will be shown if the user cannot access any report page
|
||||
Given the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| report/log:view | Prohibit | editingteacher | System | |
|
||||
| report/loglive:view | Prohibit | editingteacher | System | |
|
||||
| report/outline:view | Prohibit | editingteacher | System | |
|
||||
| report/participation:view | Prohibit | editingteacher | System | |
|
||||
| moodle/competency:coursecompetencyview | Prohibit | editingteacher | System | |
|
||||
When I am on the "C1" "Course" page logged in as "teacher"
|
||||
And I navigate to "Reports" in current page administration
|
||||
Then I should see "No reports accessible"
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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_course;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/course/lib.php');
|
||||
require_once($CFG->dirroot . '/course/tests/fixtures/format_theunittest.php');
|
||||
require_once($CFG->dirroot . '/course/format/lib.php');
|
||||
|
||||
/**
|
||||
* Course related unit tests
|
||||
*
|
||||
* @package core_course
|
||||
* @copyright 2021 Catalyst IT Pty Ltd
|
||||
* @author Jason den Dulk
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class course_format_function_test extends \basic_testcase {
|
||||
public function test_clean_param_if_not_null() {
|
||||
$this->assertNull(clean_param_if_not_null(null));
|
||||
$n = '3x';
|
||||
$this->assertEquals(clean_param($n, PARAM_INT), clean_param_if_not_null($n, PARAM_INT));
|
||||
$this->assertEquals(clean_param($n, PARAM_RAW), clean_param_if_not_null($n, PARAM_RAW));
|
||||
$this->assertEquals(clean_param($n, PARAM_ALPHANUM), clean_param_if_not_null($n, PARAM_ALPHANUM));
|
||||
$this->assertEquals(clean_param($n, PARAM_ALPHA), clean_param_if_not_null($n, PARAM_ALPHA));
|
||||
$s = '<abc>xyz</abc>';
|
||||
$this->assertEquals(clean_param($s, PARAM_ALPHANUM), clean_param_if_not_null($s, PARAM_ALPHANUM));
|
||||
$this->assertEquals(clean_param($s, PARAM_RAW), clean_param_if_not_null($s, PARAM_RAW));
|
||||
}
|
||||
|
||||
public function test_contract_value() {
|
||||
$input = [
|
||||
'abc' => '<p>All together Now</p>',
|
||||
'abcformat' => '1',
|
||||
'jolly' => 'Roger'
|
||||
];
|
||||
$expected = [
|
||||
'abc_editor' => [ 'text' => $input['abc'], 'format' => $input['abcformat'] ],
|
||||
'jolly' => $input['jolly'],
|
||||
];
|
||||
$defs = [
|
||||
'abc_editor' => [],
|
||||
'jolly' => [ 'type' => PARAM_ALPHA ],
|
||||
];
|
||||
$dest = [];
|
||||
|
||||
foreach ($defs as $name => $def) {
|
||||
contract_value($dest, $input, $def, $name);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $dest);
|
||||
}
|
||||
|
||||
public function test_expand_value() {
|
||||
$input = [
|
||||
'abc_editor' => [ 'text' => '<p>All together Now</p>', 'format' => '1' ],
|
||||
'jolly' => 'Roger',
|
||||
];
|
||||
$expected = [
|
||||
'abc' => $input['abc_editor']['text'],
|
||||
'abcformat' => $input['abc_editor']['format'],
|
||||
'jolly' => $input['jolly'],
|
||||
];
|
||||
$defs = [
|
||||
'abc_editor' => [],
|
||||
'jolly' => [ 'type' => PARAM_ALPHA ],
|
||||
];
|
||||
$dest = [];
|
||||
|
||||
foreach ($defs as $name => $def) {
|
||||
expand_value($dest, $input, $def, $name);
|
||||
}
|
||||
|
||||
$this->assertEquals($expected, $dest);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,35 @@ require_once($CFG->dirroot . '/course/lib.php');
|
||||
require_once($CFG->dirroot . '/course/tests/fixtures/format_theunittest.php');
|
||||
|
||||
class core_course_courseformat_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tests the save and load functionality.
|
||||
*
|
||||
* @author Jason den Dulk
|
||||
*/
|
||||
public function test_courseformat_saveandload() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$courseformatoptiondata = (object) [
|
||||
"hideoddsections" => 1,
|
||||
'summary_editor' => [
|
||||
'text' => '<p>Somewhere over the rainbow</p><p>The <b>quick</b> brown fox jumpos over the lazy dog.</p>',
|
||||
'format' => 1
|
||||
]
|
||||
];
|
||||
$generator = $this->getDataGenerator();
|
||||
$course1 = $generator->create_course(array('format' => 'theunittest'));
|
||||
$this->assertEquals('theunittest', $course1->format);
|
||||
course_create_sections_if_missing($course1, array(0, 1));
|
||||
|
||||
$courseformat = course_get_format($course1);
|
||||
$courseformat->update_course_format_options($courseformatoptiondata);
|
||||
|
||||
$savedcourseformatoptiondata = $courseformat->get_format_options();
|
||||
|
||||
$this->assertEqualsCanonicalizing($courseformatoptiondata, (object) $savedcourseformatoptiondata);
|
||||
}
|
||||
|
||||
public function test_available_hook() {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
@@ -996,6 +996,34 @@ class core_course_courselib_testcase extends advanced_testcase {
|
||||
$this->assertEquals(3, $course->marker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test move_section_to method.
|
||||
* Make sure that we only update the moving sections, not all the sections in the current course.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_move_section_to() {
|
||||
global $DB, $CFG;
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Generate the course and pre-requisite module.
|
||||
$course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
|
||||
|
||||
ob_start();
|
||||
$DB->set_debug(true);
|
||||
// Move section.
|
||||
move_section_to($course, 2, 3);
|
||||
$DB->set_debug(false);
|
||||
$debuginfo = ob_get_contents();
|
||||
ob_end_clean();
|
||||
$sectionmovequerycount = substr_count($debuginfo, 'UPDATE ' . $CFG->phpunit_prefix . 'course_sections SET');
|
||||
// We are updating the course_section table in steps to avoid breaking database uniqueness constraint.
|
||||
// So the queries will be doubled. See: course/lib.php:1423
|
||||
// Make sure that we only need 4 queries to update the position of section 2 and section 3.
|
||||
$this->assertEquals(4, $sectionmovequerycount);
|
||||
}
|
||||
|
||||
public function test_course_can_delete_section() {
|
||||
global $DB;
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
+9
-1
@@ -41,6 +41,10 @@ class format_theunittest extends format_base {
|
||||
'default' => 0,
|
||||
'type' => PARAM_INT,
|
||||
),
|
||||
'summary_editor' => array(
|
||||
'default' => '',
|
||||
'type' => PARAM_RAW,
|
||||
),
|
||||
);
|
||||
}
|
||||
if ($foreditform && !isset($courseformatoptions['hideoddsections']['label'])) {
|
||||
@@ -55,6 +59,10 @@ class format_theunittest extends format_base {
|
||||
'element_type' => 'select',
|
||||
'element_attributes' => array($sectionmenu),
|
||||
),
|
||||
'summary_editor' => array(
|
||||
'label' => 'Summary Text',
|
||||
'element_type' => 'editor',
|
||||
),
|
||||
);
|
||||
$courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
|
||||
}
|
||||
@@ -78,4 +86,4 @@ class format_theunittest extends format_base {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -7,14 +7,15 @@ var FORMATCHOOSER = function() {
|
||||
Y.extend(FORMATCHOOSER, Y.Base, {
|
||||
initializer: function(params) {
|
||||
if (params && params.formid) {
|
||||
var updatebut = Y.one('#' + params.formid + ' #id_updatecourseformat');
|
||||
var formatselect = Y.one('#' + params.formid + ' #id_format');
|
||||
var form = Y.one('#' + params.formid);
|
||||
var updatebut = form.one('#id_updatecourseformat');
|
||||
var formatselect = form.one('#id_format');
|
||||
var ancestor = updatebut.ancestor('fieldset');
|
||||
var action = Y.one('form.mform').get('action');
|
||||
var action = form.get('action');
|
||||
if (updatebut && formatselect) {
|
||||
updatebut.setStyle('display', 'none');
|
||||
formatselect.on('change', function() {
|
||||
Y.one('form.mform').set('action', action + '#' + ancestor.get('id'));
|
||||
form.set('action', action + '#' + ancestor.get('id'));
|
||||
updatebut.simulate('click');
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
YUI.add("moodle-course-formatchooser",function(t,o){var e=function(){e.superclass.constructor.apply(this,arguments)};t.extend(e,t.Base,{initializer:function(o){var e,n,r;o&&o.formid&&(e=t.one("#"+o.formid+" #id_updatecourseformat"),o=t.one("#"+o.formid+" #id_format"),n=e.ancestor("fieldset"),r=t.one("form.mform").get("action"),e&&o&&(e.setStyle("display","none"),o.on("change",function(){t.one("form.mform").set("action",r+"#"+n.get("id")),e.simulate("click")})))}}),M.course=M.course||{},M.course.init_formatchooser=function(o){return new e(o)}},"@VERSION@",{requires:["base","node","node-event-simulate"]});
|
||||
YUI.add("moodle-course-formatchooser",function(r,e){var o=function(){o.superclass.constructor.apply(this,arguments)};r.extend(o,r.Base,{initializer:function(e){var o,n,t,i;e&&e.formid&&(o=r.one("#"+e.formid),n=o.one("#id_updatecourseformat"),e=o.one("#id_format"),t=n.ancestor("fieldset"),i=o.get("action"),n&&e&&(n.setStyle("display","none"),e.on("change",function(){o.set("action",i+"#"+t.get("id")),n.simulate("click")})))}}),M.course=M.course||{},M.course.init_formatchooser=function(e){return new o(e)}},"@VERSION@",{requires:["base","node","node-event-simulate"]});
|
||||
+5
-4
@@ -7,14 +7,15 @@ var FORMATCHOOSER = function() {
|
||||
Y.extend(FORMATCHOOSER, Y.Base, {
|
||||
initializer: function(params) {
|
||||
if (params && params.formid) {
|
||||
var updatebut = Y.one('#' + params.formid + ' #id_updatecourseformat');
|
||||
var formatselect = Y.one('#' + params.formid + ' #id_format');
|
||||
var form = Y.one('#' + params.formid);
|
||||
var updatebut = form.one('#id_updatecourseformat');
|
||||
var formatselect = form.one('#id_format');
|
||||
var ancestor = updatebut.ancestor('fieldset');
|
||||
var action = Y.one('form.mform').get('action');
|
||||
var action = form.get('action');
|
||||
if (updatebut && formatselect) {
|
||||
updatebut.setStyle('display', 'none');
|
||||
formatselect.on('change', function() {
|
||||
Y.one('form.mform').set('action', action + '#' + ancestor.get('id'));
|
||||
form.set('action', action + '#' + ancestor.get('id'));
|
||||
updatebut.simulate('click');
|
||||
});
|
||||
}
|
||||
|
||||
+5
-4
@@ -5,14 +5,15 @@ var FORMATCHOOSER = function() {
|
||||
Y.extend(FORMATCHOOSER, Y.Base, {
|
||||
initializer: function(params) {
|
||||
if (params && params.formid) {
|
||||
var updatebut = Y.one('#' + params.formid + ' #id_updatecourseformat');
|
||||
var formatselect = Y.one('#' + params.formid + ' #id_format');
|
||||
var form = Y.one('#' + params.formid);
|
||||
var updatebut = form.one('#id_updatecourseformat');
|
||||
var formatselect = form.one('#id_format');
|
||||
var ancestor = updatebut.ancestor('fieldset');
|
||||
var action = Y.one('form.mform').get('action');
|
||||
var action = form.get('action');
|
||||
if (updatebut && formatselect) {
|
||||
updatebut.setStyle('display', 'none');
|
||||
formatselect.on('change', function() {
|
||||
Y.one('form.mform').set('action', action + '#' + ancestor.get('id'));
|
||||
form.set('action', action + '#' + ancestor.get('id'));
|
||||
updatebut.simulate('click');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class lib_test extends \advanced_testcase {
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $DB;
|
||||
// Apply sqlsrv native driver error and logging default
|
||||
// settings while finishing the AdoDB tests.
|
||||
if ($DB->get_dbfamily() === 'mssql') {
|
||||
sqlsrv_configure("WarningsReturnAsErrors", false);
|
||||
sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
|
||||
sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for getting user enrolment actions.
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,17 @@ class sync_test extends \advanced_testcase {
|
||||
/** @var string Original error log */
|
||||
protected $oldlog;
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $DB;
|
||||
// Apply sqlsrv native driver error and logging default
|
||||
// settings while finishing the AdoDB tests.
|
||||
if ($DB->get_dbfamily() === 'mssql') {
|
||||
sqlsrv_configure("WarningsReturnAsErrors", false);
|
||||
sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF);
|
||||
sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
protected function init_enrol_database() {
|
||||
global $DB, $CFG;
|
||||
|
||||
|
||||
+27
-11
@@ -1782,9 +1782,10 @@ class grade_report_grader extends grade_report {
|
||||
*/
|
||||
protected static function filter_collapsed_categories($courseid, $collapsed) {
|
||||
global $DB;
|
||||
if (empty($collapsed)) {
|
||||
$collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
|
||||
}
|
||||
// Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
|
||||
$collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
|
||||
$collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
|
||||
|
||||
if (empty($collapsed['aggregatesonly']) && empty($collapsed['gradesonly'])) {
|
||||
return $collapsed;
|
||||
}
|
||||
@@ -1805,12 +1806,23 @@ class grade_report_grader extends grade_report {
|
||||
*/
|
||||
protected static function get_collapsed_preferences($courseid) {
|
||||
if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories'.$courseid)) {
|
||||
return json_decode($collapsed, true);
|
||||
$collapsed = json_decode($collapsed, true);
|
||||
// Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
|
||||
$collapsed['aggregatesonly'] = $collapsed['aggregatesonly'] ?? [];
|
||||
$collapsed['gradesonly'] = $collapsed['gradesonly'] ?? [];
|
||||
return $collapsed;
|
||||
}
|
||||
|
||||
// Try looking for old location of user setting that used to store all courses in one serialized user preference.
|
||||
$collapsed = ['aggregatesonly' => [], 'gradesonly' => []]; // Use this if old settings are not found.
|
||||
$collapsedall = [];
|
||||
$oldprefexists = false;
|
||||
if (($oldcollapsedpref = get_user_preferences('grade_report_grader_collapsed_categories')) !== null) {
|
||||
$oldprefexists = true;
|
||||
if ($collapsedall = unserialize_array($oldcollapsedpref)) {
|
||||
// Ensure we always have an element for aggregatesonly and another for gradesonly, no matter it's empty.
|
||||
$collapsedall['aggregatesonly'] = $collapsedall['aggregatesonly'] ?? [];
|
||||
$collapsedall['gradesonly'] = $collapsedall['gradesonly'] ?? [];
|
||||
// We found the old-style preference, filter out only categories that belong to this course and update the prefs.
|
||||
$collapsed = static::filter_collapsed_categories($courseid, $collapsedall);
|
||||
if (!empty($collapsed['aggregatesonly']) || !empty($collapsed['gradesonly'])) {
|
||||
@@ -1819,17 +1831,21 @@ class grade_report_grader extends grade_report {
|
||||
$collapsedall['gradesonly'] = array_diff($collapsedall['gradesonly'], $collapsed['gradesonly']);
|
||||
if (!empty($collapsedall['aggregatesonly']) || !empty($collapsedall['gradesonly'])) {
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsedall));
|
||||
} else {
|
||||
unset_user_preference('grade_report_grader_collapsed_categories');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We found the old-style preference, but it is unreadable, discard it.
|
||||
unset_user_preference('grade_report_grader_collapsed_categories');
|
||||
}
|
||||
} else {
|
||||
$collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
|
||||
}
|
||||
|
||||
// Arrived here, if the old pref exists and it doesn't contain
|
||||
// more information, it means that the migration of all the
|
||||
// data to new, by course, preferences is completed, so
|
||||
// the old one can be safely deleted.
|
||||
if ($oldprefexists &&
|
||||
empty($collapsedall['aggregatesonly']) &&
|
||||
empty($collapsedall['gradesonly'])) {
|
||||
unset_user_preference('grade_report_grader_collapsed_categories');
|
||||
}
|
||||
|
||||
return $collapsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@
|
||||
*/
|
||||
.path-grade-report-grader .gradeparent .avg .cell {
|
||||
text-align: right;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
/**
|
||||
* Content styling.
|
||||
@@ -162,8 +165,9 @@
|
||||
* Editing fields.
|
||||
*/
|
||||
.path-grade-report-grader .gradeparent select {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 8rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.path-grade-report-grader .gradeparent .text {
|
||||
|
||||
@@ -230,6 +230,226 @@ class core_grade_report_graderlib_testcase extends advanced_testcase {
|
||||
$this->assertEquals(count($toobigvalue['gradesonly']) - 1, count($report1->collapsed['gradesonly']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test some special cases of the conversion from old preferences to new ones
|
||||
*
|
||||
* @covers \grade_report_grader::get_collapsed_preferences
|
||||
* @covers \grade_report_grader::filter_collapsed_categories
|
||||
*/
|
||||
public function test_old_collapsed_preferences() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$course1 = $this->getDataGenerator()->create_course();
|
||||
$course2 = $this->getDataGenerator()->create_course();
|
||||
$course3 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$course1cats = $course2cats = $course3cats = [];
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$course1cats[] = $this->create_grade_category($course1)->id;
|
||||
$course2cats[] = $this->create_grade_category($course2)->id;
|
||||
$course3cats[] = $this->create_grade_category($course3)->id;
|
||||
}
|
||||
|
||||
$report1 = $this->create_report($course1);
|
||||
// Collapse all the cats in course1.
|
||||
foreach ($course1cats as $catid) {
|
||||
$report1->process_action('cg'. $catid, 'switch_minus');
|
||||
}
|
||||
|
||||
// Expand all the cats in course2.
|
||||
$report2 = $this->create_report($course2);
|
||||
foreach ($course2cats as $catid) {
|
||||
$report2->process_action('cg'.$catid, 'switch_minus');
|
||||
$report2->process_action('cg'.$catid, 'switch_plus');
|
||||
}
|
||||
|
||||
// Collapse odd cats and expand even cats in course3.
|
||||
$report3 = $this->create_report($course3);
|
||||
foreach ($course3cats as $catid) {
|
||||
$report3->process_action('cg'.$catid, 'switch_minus');
|
||||
if (($i++) % 2) {
|
||||
$report3->process_action('cg'.$catid, 'switch_plus');
|
||||
}
|
||||
}
|
||||
|
||||
$report1 = $this->create_report($course1);
|
||||
$this->assertEquals(10, count($report1->collapsed['aggregatesonly']));
|
||||
$this->assertEquals(0, count($report1->collapsed['gradesonly']));
|
||||
$report2 = $this->create_report($course2);
|
||||
$this->assertEquals(0, count($report2->collapsed['aggregatesonly']));
|
||||
$this->assertEquals(10, count($report2->collapsed['gradesonly']));
|
||||
$report3 = $this->create_report($course3);
|
||||
$this->assertEquals(5, count($report3->collapsed['aggregatesonly']));
|
||||
$this->assertEquals(5, count($report3->collapsed['gradesonly']));
|
||||
|
||||
// Use the preferences generated for user1 and set it in the old format for other users.
|
||||
|
||||
// User2: both gradesonly and aggregatesonly.
|
||||
$user2 = $this->getDataGenerator()->create_user();
|
||||
$alldata = [
|
||||
'gradesonly' => array_merge(
|
||||
$report1->collapsed['gradesonly'],
|
||||
$report2->collapsed['gradesonly'],
|
||||
$report3->collapsed['gradesonly']),
|
||||
'aggregatesonly' => array_merge(
|
||||
$report1->collapsed['aggregatesonly'],
|
||||
$report2->collapsed['aggregatesonly'],
|
||||
$report3->collapsed['aggregatesonly']),
|
||||
];
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($alldata), $user2);
|
||||
|
||||
$this->setUser($user2);
|
||||
$convertedreport1 = $this->create_report($course1);
|
||||
$this->assertEquals($report1->collapsed['gradesonly'], $convertedreport1->collapsed['gradesonly']);
|
||||
$this->assertEquals($report1->collapsed['aggregatesonly'], $convertedreport1->collapsed['aggregatesonly']);
|
||||
$newprefs1 = get_user_preferences('grade_report_grader_collapsed_categories' . $course1->id); // Also verify new prefs.
|
||||
$this->assertEquals($report1->collapsed['gradesonly'], json_decode($newprefs1, true)['gradesonly']);
|
||||
$this->assertEquals($report1->collapsed['aggregatesonly'], json_decode($newprefs1, true)['aggregatesonly']);
|
||||
|
||||
$convertedreport2 = $this->create_report($course2);
|
||||
$this->assertEquals($report2->collapsed['gradesonly'], $convertedreport2->collapsed['gradesonly']);
|
||||
$this->assertEquals($report2->collapsed['aggregatesonly'], $convertedreport2->collapsed['aggregatesonly']);
|
||||
$newprefs2 = get_user_preferences('grade_report_grader_collapsed_categories' . $course2->id); // Also verify new prefs.
|
||||
$this->assertEquals($report2->collapsed['gradesonly'], json_decode($newprefs2, true)['gradesonly']);
|
||||
$this->assertEquals($report2->collapsed['aggregatesonly'], json_decode($newprefs2, true)['aggregatesonly']);
|
||||
|
||||
$convertedreport3 = $this->create_report($course3);
|
||||
$this->assertEquals($report3->collapsed['gradesonly'], $convertedreport3->collapsed['gradesonly']);
|
||||
$this->assertEquals($report3->collapsed['aggregatesonly'], $convertedreport3->collapsed['aggregatesonly']);
|
||||
$newprefs3 = get_user_preferences('grade_report_grader_collapsed_categories' . $course3->id); // Also verify new prefs.
|
||||
$this->assertEquals($report3->collapsed['gradesonly'], json_decode($newprefs3, true)['gradesonly']);
|
||||
$this->assertEquals($report3->collapsed['aggregatesonly'], json_decode($newprefs3, true)['aggregatesonly']);
|
||||
|
||||
// Make sure the old style user preference is removed now.
|
||||
$this->assertEmpty(get_user_preferences('grade_report_grader_collapsed_categories'));
|
||||
|
||||
// User3: only gradesonly (missing aggregatesonly).
|
||||
$user3 = $this->getDataGenerator()->create_user();
|
||||
$alldata = [
|
||||
'gradesonly' => array_merge(
|
||||
$report1->collapsed['gradesonly'],
|
||||
$report2->collapsed['gradesonly'],
|
||||
$report3->collapsed['gradesonly']),
|
||||
];
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($alldata), $user3);
|
||||
|
||||
$this->setUser($user3);
|
||||
$convertedreport1 = $this->create_report($course1);
|
||||
$this->assertEquals($report1->collapsed['gradesonly'], $convertedreport1->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['aggregatesonly']);
|
||||
$newprefs1 = get_user_preferences('grade_report_grader_collapsed_categories' . $course1->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs1);
|
||||
|
||||
$convertedreport2 = $this->create_report($course2);
|
||||
$this->assertEquals($report2->collapsed['gradesonly'], $convertedreport2->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['aggregatesonly']);
|
||||
$newprefs2 = get_user_preferences('grade_report_grader_collapsed_categories' . $course2->id); // Also verify new prefs.
|
||||
$this->assertEquals($report2->collapsed['gradesonly'], json_decode($newprefs2, true)['gradesonly']);
|
||||
$this->assertEquals([], json_decode($newprefs2, true)['aggregatesonly']);
|
||||
|
||||
$convertedreport3 = $this->create_report($course3);
|
||||
$this->assertEquals($report3->collapsed['gradesonly'], $convertedreport3->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['aggregatesonly']);
|
||||
$newprefs3 = get_user_preferences('grade_report_grader_collapsed_categories' . $course3->id); // Also verify new prefs.
|
||||
$this->assertEquals($report3->collapsed['gradesonly'], json_decode($newprefs3, true)['gradesonly']);
|
||||
$this->assertEquals([], json_decode($newprefs3, true)['aggregatesonly']);
|
||||
|
||||
// Make sure the old style user preference is removed now.
|
||||
$this->assertEmpty(get_user_preferences('grade_report_grader_collapsed_categories'));
|
||||
|
||||
// User4: only aggregatesonly (missing gradesonly).
|
||||
$user4 = $this->getDataGenerator()->create_user();
|
||||
$alldata = [
|
||||
'aggregatesonly' => array_merge(
|
||||
$report1->collapsed['aggregatesonly'],
|
||||
$report2->collapsed['aggregatesonly'],
|
||||
$report3->collapsed['aggregatesonly']),
|
||||
];
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($alldata), $user4);
|
||||
|
||||
$this->setUser($user4);
|
||||
$convertedreport1 = $this->create_report($course1);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['gradesonly']);
|
||||
$this->assertEquals($report1->collapsed['aggregatesonly'], $convertedreport1->collapsed['aggregatesonly']);
|
||||
$newprefs1 = get_user_preferences('grade_report_grader_collapsed_categories' . $course1->id); // Also verify new prefs.
|
||||
$this->assertEquals([], json_decode($newprefs1, true)['gradesonly']);
|
||||
$this->assertEquals($report1->collapsed['aggregatesonly'], json_decode($newprefs1, true)['aggregatesonly']);
|
||||
|
||||
$convertedreport2 = $this->create_report($course2);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['gradesonly']);
|
||||
$this->assertEquals($report2->collapsed['aggregatesonly'], $convertedreport2->collapsed['aggregatesonly']);
|
||||
$newprefs2 = get_user_preferences('grade_report_grader_collapsed_categories' . $course2->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs2);
|
||||
|
||||
$convertedreport3 = $this->create_report($course3);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['gradesonly']);
|
||||
$this->assertEquals($report3->collapsed['aggregatesonly'], $convertedreport3->collapsed['aggregatesonly']);
|
||||
$newprefs3 = get_user_preferences('grade_report_grader_collapsed_categories' . $course3->id); // Also verify new prefs.
|
||||
$this->assertEquals([], json_decode($newprefs3, true)['gradesonly']);
|
||||
$this->assertEquals($report3->collapsed['aggregatesonly'], json_decode($newprefs3, true)['aggregatesonly']);
|
||||
|
||||
// Make sure the old style user preference is removed now.
|
||||
$this->assertEmpty(get_user_preferences('grade_report_grader_collapsed_categories'));
|
||||
|
||||
// User5: both missing gradesonly and aggregatesonly.
|
||||
$user5 = $this->getDataGenerator()->create_user();
|
||||
$alldata = [];
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($alldata), $user5);
|
||||
|
||||
$this->setUser($user5);
|
||||
$convertedreport1 = $this->create_report($course1);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['aggregatesonly']);
|
||||
$newprefs1 = get_user_preferences('grade_report_grader_collapsed_categories' . $course1->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs1);
|
||||
|
||||
$convertedreport2 = $this->create_report($course2);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['aggregatesonly']);
|
||||
$newprefs2 = get_user_preferences('grade_report_grader_collapsed_categories' . $course2->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs2);
|
||||
|
||||
$convertedreport3 = $this->create_report($course3);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['aggregatesonly']);
|
||||
$newprefs3 = get_user_preferences('grade_report_grader_collapsed_categories' . $course3->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs3);
|
||||
|
||||
// Make sure the old style user preference is removed now.
|
||||
$this->assertEmpty(get_user_preferences('grade_report_grader_collapsed_categories'));
|
||||
|
||||
// User6: both empty gradesonly and aggregatesonly.
|
||||
$user6 = $this->getDataGenerator()->create_user();
|
||||
$alldata = [
|
||||
'gradesonly' => [],
|
||||
'aggregatesonly' => []
|
||||
];
|
||||
set_user_preference('grade_report_grader_collapsed_categories', serialize($alldata), $user6);
|
||||
|
||||
$this->setUser($user6);
|
||||
$convertedreport1 = $this->create_report($course1);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport1->collapsed['aggregatesonly']);
|
||||
$newprefs1 = get_user_preferences('grade_report_grader_collapsed_categories' . $course1->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs1);
|
||||
|
||||
$convertedreport2 = $this->create_report($course2);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport2->collapsed['aggregatesonly']);
|
||||
$newprefs2 = get_user_preferences('grade_report_grader_collapsed_categories' . $course2->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs2);
|
||||
|
||||
$convertedreport3 = $this->create_report($course3);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['gradesonly']);
|
||||
$this->assertEquals([], $convertedreport3->collapsed['aggregatesonly']);
|
||||
$newprefs3 = get_user_preferences('grade_report_grader_collapsed_categories' . $course3->id); // Also verify new prefs.
|
||||
$this->assertNull($newprefs3);
|
||||
|
||||
// Make sure the old style user preference is removed now.
|
||||
$this->assertEmpty(get_user_preferences('grade_report_grader_collapsed_categories'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get_right_rows function with one 'normal' and one 'ungraded' quiz.
|
||||
*
|
||||
|
||||
@@ -31,10 +31,9 @@ use core_h5p\helper;
|
||||
use file_archive;
|
||||
use moodle_exception;
|
||||
use ReflectionMethod;
|
||||
use stored_file;
|
||||
use zip_archive;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Test class covering the H5PFileStorage interface implementation.
|
||||
*
|
||||
@@ -782,7 +781,6 @@ class h5p_file_storage_test extends \advanced_testcase {
|
||||
* Tests that the content folder of an H5P content is imported in the Moodle filesystem.
|
||||
*/
|
||||
public function test_moveContentDiretory(): void {
|
||||
global $DB;
|
||||
|
||||
// Create temp folder.
|
||||
$tempfolder = make_request_directory(false);
|
||||
@@ -818,19 +816,21 @@ class h5p_file_storage_test extends \advanced_testcase {
|
||||
$this->h5p_file_storage->moveContentDirectory($h5pcontentfolder, $targeth5pcontentid);
|
||||
|
||||
// Get database records.
|
||||
$sql = "SELECT concat(filepath, filename)
|
||||
FROM {files}
|
||||
WHERE filearea = :filearea AND itemid = :itemid AND component = :component AND filename != '.'";
|
||||
$params = [
|
||||
'component' => file_storage::COMPONENT,
|
||||
'filearea' => file_storage::CONTENT_FILEAREA,
|
||||
'itemid' => $targeth5pcontentid
|
||||
];
|
||||
$filesdb = $DB->get_fieldset_sql($sql, $params);
|
||||
sort($filesdb);
|
||||
$files = $this->h5p_fs_fs->get_area_files(
|
||||
$this->h5p_fs_context->id,
|
||||
file_storage::COMPONENT,
|
||||
file_storage::CONTENT_FILEAREA,
|
||||
$targeth5pcontentid,
|
||||
'filepath, filename',
|
||||
false
|
||||
);
|
||||
|
||||
$filepaths = array_map(static function(stored_file $file): string {
|
||||
return $file->get_filepath() . $file->get_filename();
|
||||
}, $files);
|
||||
|
||||
// Check that created files match with database records.
|
||||
$this->assertEquals($filesexpected, $filesdb);
|
||||
$this->assertEquals($filesexpected, array_values($filepaths));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ $string['chooselanguagesub'] = 'Pasirinkite diegimo kalbą. Ši kalba bus naudoj
|
||||
$string['clialreadyconfigured'] = 'Failas config.php jau yra, prašau naudoti admin/cli/install_database.php jei norite įrašyti šią svetainę.';
|
||||
$string['clialreadyinstalled'] = 'Failas config.php jau yra. Jei norite atnaujinti svetainę, naudokite admin/cli/upgrade.php.';
|
||||
$string['cliinstallheader'] = '„Moodle“ {$a} komandų eilutės diegimo programa';
|
||||
$string['clitablesexist'] = 'Duomenų bazės lentelės jau yra, „cli“ diegimo tęsti negalima.';
|
||||
$string['clitablesexist'] = 'Jau yra duomenų bazės lentelės; CLI diegimas negali tęstis.';
|
||||
$string['databasehost'] = 'Duomenų bazės pagrindinis kompiuteris';
|
||||
$string['databasename'] = 'Duomenų bazės pavadinimas';
|
||||
$string['databasetypehead'] = 'Pasirinkite duomenų bazės tvarkyklę';
|
||||
|
||||
@@ -30,4 +30,4 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = 'lt';
|
||||
$string['thislanguage'] = 'Lietuvių';
|
||||
$string['thislanguage'] = 'Lietuvių (universitetas)';
|
||||
|
||||
@@ -38,3 +38,5 @@ $string['clitypevaluedefault'] = 'တန်ဖိုး ထည့်သွင်
|
||||
$string['cliunknowoption'] = 'အသိအမှတ် မပြုထားသော ရွေးချယ်မှုများ :
|
||||
{$a}
|
||||
ကျေးဇူးပြု၍ -- အကူအညီရယူရန် ကိုနှိပ်ပါ။';
|
||||
$string['environmentrequireinstall'] = 'ထည့်သွင်းပြီး အသုံးပြုခွင့်ဖွင့်ပါ';
|
||||
$string['environmentrequireversion'] = 'ဗားရှင်း{$a->needed} ကို လိုအပ်ပြီး သင်သည် {$a->current} ကိုအသုံးပြုနေပါသည်';
|
||||
|
||||
@@ -39,4 +39,8 @@ $string['cannotdownloadzipfile'] = 'ZIP ဖိုင်ကိုဒေါင်
|
||||
$string['cannotfindcomponent'] = 'အစိတ်အပိုင်းကို ရှာဖွေမတွေ့ရှိပါ။';
|
||||
$string['cannotsavemd5file'] = 'md5 ဖိုင်ကို မသိမ်းဆည်းနိုင်ပါ';
|
||||
$string['cannotsavezipfile'] = 'ZIP ဖိုင်ကို မသိမ်းဆည်းနိုင်ပါ';
|
||||
$string['cannotunzipfile'] = 'ZIP ဖိုင် ဖြည်၍မရပါ';
|
||||
$string['componentisuptodate'] = 'Component နောက်ဆုံးပေါ်ဖြစ်သည်။';
|
||||
$string['dmlexceptiononinstall'] = '<p>A database error has occurred [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'ဒေါင်းလုဒ်ပြုလုပ်ထားသော ဖိုင် စစ်ဆေးခြင်း မအောင်မြင်ပါ';
|
||||
$string['invalidmd5'] = 'ပြောင်းလဲမှုစစ်ဆေးခြင်း မှားယွင်းနေပါသည် - ထပ်မံကြိုးစားကြည့်ပါ';
|
||||
|
||||
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['language'] = 'ဘာသာစကား';
|
||||
$string['moodlelogo'] = 'Moodle Logo';
|
||||
$string['next'] = 'နောက်သို့';
|
||||
$string['previous'] = 'ရှေ့သို့';
|
||||
|
||||
@@ -45,7 +45,8 @@ $string['dmlexceptiononinstall'] = '<p>Došlo k chybe databázy [{$a->errorcode}
|
||||
$string['downloadedfilecheckfailed'] = 'Kontrola stiahnutého súboru bola neúspešná';
|
||||
$string['invalidmd5'] = 'Kontrolovaná premenná bola nesprávna - skúste znova';
|
||||
$string['missingrequiredfield'] = 'Chýba niektoré z povinných polí';
|
||||
$string['remotedownloaderror'] = 'Stiahnutie komponentu na server zlyhalo, skontrolujte nastavenia proxy, doporučené je PHP rozšírenie cURL. <br /><br />Musíte stiahnuť súbor <a href="{$a->url}">{$a->url}</a> manuálne, skopírovať ho do "{$a->dest}" na serveri a rozzipovať ho tam.';
|
||||
$string['remotedownloaderror'] = '<p>Sťahovanie komponentu na váš server zlyhalo. Preverte nastavenia proxy. Vrelo odporúčame PHP rozšírenie cURL.</p>
|
||||
<p>Teraz musíte stiahnuť súbor <a href="{$a->url}">{$a->url}</a> ručne, prekopírovať ho do "{$a->dest}" na vašom serveri a tam ho rozbaliť.</p>';
|
||||
$string['wrongdestpath'] = 'Chybné umiestnenie cieľa';
|
||||
$string['wrongsourcebase'] = 'Chybné základné URL serveru';
|
||||
$string['wrongzipfilename'] = 'Chybné meno súboru ZIP';
|
||||
|
||||
+1
-1
@@ -316,7 +316,7 @@ $string['configpasswordresettime'] = 'This specifies the amount of time people h
|
||||
$string['configpathtodu'] = 'Path to du. Probably something like /usr/bin/du. If you enter this, pages that display directory contents will run much faster for directories with a lot of files.';
|
||||
$string['configpathtophp'] = 'Path to PHP CLI. Probably something like /usr/bin/php. If you enter this, cron scripts can be executed from admin web interface.';
|
||||
$string['configperfdebug'] = 'If you turn this on, performance info will be printed in the footer of the standard theme';
|
||||
$string['configprofileroles'] = 'List of roles that are visible on user profiles and participation page.';
|
||||
$string['configprofileroles'] = 'Roles that are listed in user profiles and on the participants page.';
|
||||
$string['configprofilesforenrolledusersonly'] = 'To prevent misuse by spammers, profile descriptions of users who are not yet enrolled in any course are hidden. New users must enrol in at least one course before they can add a profile description.';
|
||||
$string['configprotectusernames'] = 'If enabled, the forgotten password form will not display any hints allowing account usernames or email addresses to be guessed.';
|
||||
$string['configproxybypass'] = 'Comma separated list of (partial) hostnames or IPs that should bypass proxy (e.g., 192.168., .mydomain.com)';
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ $string['activemethodinfonone'] = 'There is no advanced grading method selected
|
||||
$string['changeactivemethod'] = 'Change active grading method to';
|
||||
$string['clicktoclose'] = 'click to close';
|
||||
$string['exc_gradingformelement'] = 'Unable to instantiate grading form element';
|
||||
$string['formnotavailable'] = 'An advanced grading method was selected to use but the grading form is not available yet. You may need to define it first via a link in the actions menu or administration block.';
|
||||
$string['formnotavailable'] = 'An advanced grading method was selected but the grading form still needs to be defined.';
|
||||
$string['gradingformunavailable'] = 'Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status.';
|
||||
$string['gradingmanagement'] = 'Advanced grading';
|
||||
$string['gradingmanagementtitle'] = 'Advanced grading: {$a->component} ({$a->area})';
|
||||
|
||||
+1
-1
@@ -759,7 +759,7 @@ $string['emptydragdropregion'] = 'empty region';
|
||||
$string['enable'] = 'Enable';
|
||||
$string['encryptedcode'] = 'Encrypted code';
|
||||
$string['enddate'] = 'Course end date';
|
||||
$string['enddate_help'] = 'The course end date is used for determining whether a course should be included in a user\'s list of courses. When the end date is past, the course is no longer listed in the navigation and is listed as past in the course overview. The course end date may also be used by custom reports. Users can still enter the course after the end date; in other words the date does not restrict access.';
|
||||
$string['enddate_help'] = 'The course end date determines whether a course is listed as past in the course overview. The course end date may also be used by custom reports. Note that users can still enter the course after the end date; in other words the date does not restrict access.';
|
||||
$string['english'] = 'English';
|
||||
$string['enrolmentmethods'] = 'Enrolment methods';
|
||||
$string['entercourse'] = 'Click to enter this course';
|
||||
|
||||
@@ -202,6 +202,7 @@ $string['importparseerror'] = 'Error(s) found parsing the import file. No questi
|
||||
$string['importquestions'] = 'Import questions from file';
|
||||
$string['importquestions_help'] = 'This function enables questions in a variety of formats to be imported via text file. Note that the file must use UTF-8 encoding.';
|
||||
$string['importquestions_link'] = 'question/import';
|
||||
$string['importwrongfileencoding'] = 'The file you selected does not use UTF-8 character encoding. {$a} files must use UTF-8.';
|
||||
$string['importwrongfiletype'] = 'The type of the file you selected ({$a->actualtype}) does not match the type expected by this import format ({$a->expectedtype}).';
|
||||
$string['invalidarg'] = 'No valid arguments supplied or incorrect server configuration';
|
||||
$string['invalidcategoryidforparent'] = 'Invalid category id for parent!';
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ $string['ratinginvalid'] = 'Rating is invalid';
|
||||
$string['ratingtime'] = 'Restrict ratings to items with dates in this range:';
|
||||
$string['ratings'] = 'Ratings';
|
||||
$string['rolewarning'] = 'Roles with permission to rate';
|
||||
$string['rolewarning_help'] = 'To submit ratings users require the moodle/rating:rate capability and any module specific capabilities. Users assigned the following roles should be able to rate items. The list of roles may be amended via the permissions link in the actions menu or administration block, depending on the theme.';
|
||||
$string['rolewarning_help'] = 'Roles with permission to rate are roles with the capability moodle/rating:rate plus any activity-specific rating capabilities. You can give more roles permission to rate via the page Permissions.';
|
||||
$string['scaleselectionrequired'] = 'When selecting a ratings aggregate type you must also select to use either a scale or set a maximum points.';
|
||||
$string['privacy:metadata:rating'] = 'The user-entered rating is stored alongside a mapping of the item which was rated.';
|
||||
$string['privacy:metadata:rating:userid'] = 'The user who made the rating.';
|
||||
|
||||
+2
-1
@@ -160,6 +160,7 @@ $string['contentbank:viewunlistedcontent'] = 'View unlisted content from the con
|
||||
$string['contentbank:upload'] = 'Upload new content to the content bank';
|
||||
$string['contentbank:useeditor'] = 'Create or edit content using a content type editor';
|
||||
$string['context'] = 'Context';
|
||||
$string['contextrolenotallowed'] = 'Role <b>{$a}</b> not allowed in this context.';
|
||||
$string['course:activityvisibility'] = 'Hide/show activities';
|
||||
$string['course:bulkmessaging'] = 'Send a message to many people';
|
||||
$string['course:create'] = 'Create courses';
|
||||
@@ -251,7 +252,7 @@ $string['extusers'] = 'Existing users';
|
||||
$string['extusersmatching'] = 'Existing users matching \'{$a}\'';
|
||||
$string['filter:manage'] = 'Manage local filter settings';
|
||||
$string['frontpageuser'] = 'Authenticated user on frontpage';
|
||||
$string['frontpageuserdescription'] = 'All logged in users in the frontpage course.';
|
||||
$string['frontpageuserdescription'] = 'All logged-in users in the frontpage course.';
|
||||
$string['globalrole'] = 'System role';
|
||||
$string['globalroleswarning'] = 'Warning: Any roles you assign from this page will apply to users throughout the entire system, including the front page and all courses.';
|
||||
$string['gotoassignroles'] = 'Go to Assign roles for this {$a->contextlevel}';
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@ $string['filtersetmatchdescription'] = 'How multiple filters should be combined'
|
||||
$string['filtertype'] = 'Filter type';
|
||||
$string['match'] = 'Match';
|
||||
$string['matchofthefollowing'] = 'of the following:';
|
||||
$string['moodlenetprofile'] = 'MoodleNet profile';
|
||||
$string['moodlenetprofile_help'] = 'This field is to link your MoodleNet profile to Moodle. It expects a WebFinger-compliant URI.';
|
||||
$string['moodlenetprofile'] = 'MoodleNet profile ID';
|
||||
$string['moodlenetprofile_help'] = 'Your MoodleNet profile ID links your MoodleNet profile with this site.';
|
||||
$string['placeholdertypeorselect'] = 'Type or select...';
|
||||
$string['placeholdertype'] = 'Type...';
|
||||
$string['privacy:courserequestpath'] = 'Requested courses';
|
||||
|
||||
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
+2
-2
@@ -110,9 +110,9 @@ define([
|
||||
if (e.ajaxreturn.itemtype === 'tagflag') {
|
||||
var row = $(e.target).closest('tr');
|
||||
if (e.ajaxreturn.value === '0') {
|
||||
row.removeClass('flagged-tag');
|
||||
row.removeClass('table-warning');
|
||||
} else {
|
||||
row.addClass('flagged-tag');
|
||||
row.addClass('table-warning');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+43
-19
@@ -877,6 +877,7 @@ function login_attempt_valid($user) {
|
||||
/**
|
||||
* To be called after failed user login.
|
||||
* @param stdClass $user
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
function login_attempt_failed($user) {
|
||||
global $CFG;
|
||||
@@ -888,30 +889,53 @@ function login_attempt_failed($user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$count = get_user_preferences('login_failed_count', 0, $user);
|
||||
$last = get_user_preferences('login_failed_last', 0, $user);
|
||||
$sincescuccess = get_user_preferences('login_failed_count_since_success', $count, $user);
|
||||
$sincescuccess = $sincescuccess + 1;
|
||||
set_user_preference('login_failed_count_since_success', $sincescuccess, $user);
|
||||
// Force user preferences cache reload to ensure the most up-to-date login_failed_count is fetched.
|
||||
// This is perhaps overzealous but is the documented way of reloading the cache, as per the test method
|
||||
// 'test_check_user_preferences_loaded'.
|
||||
unset($user->preference);
|
||||
|
||||
if (empty($CFG->lockoutthreshold)) {
|
||||
// No threshold means no lockout.
|
||||
// Always unlock here, there might be some race conditions or leftovers when switching threshold.
|
||||
login_unlock_account($user);
|
||||
return;
|
||||
}
|
||||
$resource = 'user:' . $user->id;
|
||||
$lockfactory = \core\lock\lock_config::get_lock_factory('core_failed_login_count_lock');
|
||||
|
||||
if (!empty($CFG->lockoutwindow) and time() - $last > $CFG->lockoutwindow) {
|
||||
$count = 0;
|
||||
}
|
||||
// Get a new lock for the resource, waiting for it for a maximum of 10 seconds.
|
||||
if ($lock = $lockfactory->get_lock($resource, 10)) {
|
||||
try {
|
||||
$count = get_user_preferences('login_failed_count', 0, $user);
|
||||
$last = get_user_preferences('login_failed_last', 0, $user);
|
||||
$sincescuccess = get_user_preferences('login_failed_count_since_success', $count, $user);
|
||||
$sincescuccess = $sincescuccess + 1;
|
||||
set_user_preference('login_failed_count_since_success', $sincescuccess, $user);
|
||||
|
||||
$count = $count+1;
|
||||
if (empty($CFG->lockoutthreshold)) {
|
||||
// No threshold means no lockout.
|
||||
// Always unlock here, there might be some race conditions or leftovers when switching threshold.
|
||||
login_unlock_account($user);
|
||||
$lock->release();
|
||||
return;
|
||||
}
|
||||
|
||||
set_user_preference('login_failed_count', $count, $user);
|
||||
set_user_preference('login_failed_last', time(), $user);
|
||||
if (!empty($CFG->lockoutwindow) and time() - $last > $CFG->lockoutwindow) {
|
||||
$count = 0;
|
||||
}
|
||||
|
||||
if ($count >= $CFG->lockoutthreshold) {
|
||||
login_lock_account($user);
|
||||
$count = $count + 1;
|
||||
|
||||
set_user_preference('login_failed_count', $count, $user);
|
||||
set_user_preference('login_failed_last', time(), $user);
|
||||
|
||||
if ($count >= $CFG->lockoutthreshold) {
|
||||
login_lock_account($user);
|
||||
}
|
||||
|
||||
// Release locks when we're done.
|
||||
$lock->release();
|
||||
} catch (Exception $e) {
|
||||
// Always release the lock on a failure.
|
||||
$lock->release();
|
||||
}
|
||||
} else {
|
||||
// We did not get access to the resource in time, give up.
|
||||
throw new moodle_exception('locktimeout');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,31 @@ class WebDriverFactory extends UpstreamFactory implements DriverFactory {
|
||||
$extracapabilities = $config['capabilities']['extra_capabilities'];
|
||||
unset($config['capabilities']['extra_capabilities']);
|
||||
|
||||
// Ensure that the capabilites.browser is set correctly.
|
||||
$config['capabilities']['browser'] = $config['browser'];
|
||||
// Normalise the Edge browser name.
|
||||
if ($config['browser'] === 'edge') {
|
||||
$config['browser'] = 'MicrosoftEdge';
|
||||
}
|
||||
|
||||
$capabilities = array_replace($this->guessCapabilities(), $extracapabilities, $config['capabilities']);
|
||||
// Ensure that the capabilites.browserName is set correctly.
|
||||
$config['capabilities']['browserName'] = $config['browser'];
|
||||
|
||||
$capabilities = array_replace($extracapabilities, $config['capabilities']);
|
||||
|
||||
// Incorrect top level capabilities lead to invalid Selenium browser selection.
|
||||
// See https://github.com/SeleniumHQ/selenium/issues/10410 for more information.
|
||||
// If any of these settings are mentioned then additional empty Capability options are created and a random
|
||||
// browser is chosen.
|
||||
$filteredcapabilities = [
|
||||
'tags',
|
||||
'ignoreZoomSetting',
|
||||
'marionette',
|
||||
'browser',
|
||||
'name',
|
||||
];
|
||||
|
||||
foreach ($filteredcapabilities as $capabilityname) {
|
||||
unset($capabilities[$capabilityname]);
|
||||
}
|
||||
|
||||
// Build driver definition.
|
||||
return new Definition(WebDriver::class, [
|
||||
|
||||
@@ -56,17 +56,19 @@ class behat_form_date extends behat_form_group {
|
||||
// Disable the given date selector field.
|
||||
$this->set_child_field_value('enabled', false);
|
||||
} else if (is_numeric($value)) { // The value is numeric (unix timestamp).
|
||||
// First, reset the day always to an existing one (1st). Without that
|
||||
// undesired modifications (JS) happens when changing of month and day if
|
||||
// the interim combination doesn't exists (for example, 31 March => 01 April).
|
||||
// Note that instead of always setting the day to 1, this could be a little more
|
||||
// clever, for example only changing when the day > 28, or only when the
|
||||
// months (current or changed) have less days that the other. But that would
|
||||
// require more complex calculations than the simpler line below.
|
||||
$this->set_child_field_value('day', 1);
|
||||
// Assign the mapped values to each form element in the date selector field.
|
||||
foreach ($this->get_mapped_fields($value) as $childname => $childvalue) {
|
||||
$this->set_child_field_value($childname, $childvalue);
|
||||
if ($childname === 'enabled') {
|
||||
// As soon as the form is enabled, reset the day to an existing one (1st). Without that
|
||||
// undesired modifications (JS) happens when changing of month and day if
|
||||
// the interim combination doesn't exists (for example, 31 March => 01 April).
|
||||
// Note that instead of always setting the day to 1, this could be a little more
|
||||
// clever, for example only changing when the day > 28, or only when the
|
||||
// months (current or changed) have less days that the other. But that would
|
||||
// require more complex calculations than the simpler line below.
|
||||
$this->set_child_field_value('day', 1);
|
||||
}
|
||||
}
|
||||
} else { // Invalid value.
|
||||
// Get the name of the field.
|
||||
|
||||
@@ -1815,7 +1815,6 @@ class block_manager {
|
||||
$editpage->navbar->add($block->get_title());
|
||||
$editpage->navbar->add(get_string('configuration'));
|
||||
echo $output->header();
|
||||
echo $output->heading($strheading, 2);
|
||||
$mform->display();
|
||||
echo $output->footer();
|
||||
exit;
|
||||
|
||||
@@ -121,10 +121,40 @@ class completion_daily_task extends scheduled_task {
|
||||
if ($prev->completionid) {
|
||||
$completion->id = $prev->completionid;
|
||||
}
|
||||
$completion->mark_enrolled();
|
||||
|
||||
if (debugging()) {
|
||||
mtrace('Marked started user ' . $prev->userid . ' in course ' . $prev->course);
|
||||
try {
|
||||
$completion->mark_enrolled();
|
||||
|
||||
if (debugging()) {
|
||||
mtrace('Marked started user '.$prev->userid.' in course '.$prev->course);
|
||||
}
|
||||
} catch (\dml_write_exception $e) {
|
||||
// Most likely this happened because the completion object was created while we were working.
|
||||
// So get the record and make sure it has a time enrolled set.
|
||||
if (debugging()) {
|
||||
mtrace('Exception while marking started user '.$prev->userid.' in course '.$prev->course.', retrying');
|
||||
}
|
||||
|
||||
$params = ['userid' => $completion->userid, 'course' => $completion->course];
|
||||
$existing = new \completion_completion($params);
|
||||
if (!empty($existing->id) && empty($existing->timeenrolled)) {
|
||||
$existing->timeenrolled = $completion->timeenrolled;
|
||||
try {
|
||||
$existing->mark_enrolled();
|
||||
} catch (\Exception $e) {
|
||||
// Catch everything, so we can continue on to other records.
|
||||
if (debugging()) {
|
||||
mtrace('Exception again while marking started user '.$prev->userid.' in course '.$prev->course.
|
||||
': '.$e->getMessage()."\n".$e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Catch anything else, so we can continue on to other records.
|
||||
if (debugging()) {
|
||||
mtrace('Exception while marking started user '.$prev->userid.' in course '.$prev->course.
|
||||
': '.$e->getMessage()."\n".$e->getTraceAsString());
|
||||
}
|
||||
}
|
||||
} else if ($prev && $current) {
|
||||
// Else, if this record is for the same user/course use oldest timeenrolled.
|
||||
|
||||
@@ -986,7 +986,7 @@ class manager {
|
||||
$params = ['now1' => time(), 'now2' => time()];
|
||||
|
||||
$sql = "SELECT subquery.*
|
||||
FROM (SELECT concat('s', ts.id) as uniqueid,
|
||||
FROM (SELECT " . $DB->sql_concat("'s'", 'ts.id') . " as uniqueid,
|
||||
ts.id,
|
||||
'scheduled' as type,
|
||||
ts.classname,
|
||||
@@ -997,7 +997,7 @@ class manager {
|
||||
FROM {task_scheduled} ts
|
||||
WHERE ts.timestarted IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT concat('a', ta.id) as uniqueid,
|
||||
SELECT " . $DB->sql_concat("'a'", 'ta.id') . " as uniqueid,
|
||||
ta.id,
|
||||
'adhoc' as type,
|
||||
ta.classname,
|
||||
@@ -1116,7 +1116,7 @@ class manager {
|
||||
|
||||
if (!self::is_runnable()) {
|
||||
$redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
|
||||
throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
|
||||
throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
|
||||
} else {
|
||||
// Shell-escaped path to the PHP binary.
|
||||
$phpbinary = escapeshellarg(self::find_php_cli_path());
|
||||
|
||||
@@ -2682,5 +2682,23 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2021051700.05);
|
||||
}
|
||||
|
||||
if ($oldversion < 2021051706.12) {
|
||||
// Social custom fields could had been created linked to category id = 1. Let's check category 1 exists.
|
||||
if (!$DB->get_record('user_info_category', ['id' => 1])) {
|
||||
// Let's check if we have any custom field linked to category id = 1.
|
||||
$fields = $DB->get_records('user_info_field', ['categoryid' => 1]);
|
||||
if (!empty($fields)) {
|
||||
$categoryid = $DB->get_field_sql('SELECT min(id) from {user_info_category}');
|
||||
foreach ($fields as $field) {
|
||||
$field->categoryid = $categoryid;
|
||||
$DB->update_record('user_info_field', $field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2021051706.12);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -438,9 +438,10 @@ class oci_native_moodle_database extends moodle_database {
|
||||
$indexes = array();
|
||||
$tablename = strtoupper($this->prefix.$table);
|
||||
|
||||
$sql = "SELECT i.INDEX_NAME, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, ac.CONSTRAINT_TYPE
|
||||
$sql = "SELECT i.INDEX_NAME, i.INDEX_TYPE, i.UNIQUENESS, c.COLUMN_POSITION, c.COLUMN_NAME, e.COLUMN_EXPRESSION, ac.CONSTRAINT_TYPE
|
||||
FROM ALL_INDEXES i
|
||||
JOIN ALL_IND_COLUMNS c ON c.INDEX_NAME=i.INDEX_NAME
|
||||
LEFT JOIN ALL_IND_EXPRESSIONS e ON (e.INDEX_NAME = c.INDEX_NAME AND e.COLUMN_POSITION = c.COLUMN_POSITION)
|
||||
LEFT JOIN ALL_CONSTRAINTS ac ON (ac.TABLE_NAME=i.TABLE_NAME AND ac.CONSTRAINT_NAME=i.INDEX_NAME AND ac.CONSTRAINT_TYPE='P')
|
||||
WHERE i.TABLE_NAME = '$tablename'
|
||||
ORDER BY i.INDEX_NAME, c.COLUMN_POSITION";
|
||||
@@ -463,6 +464,20 @@ class oci_native_moodle_database extends moodle_database {
|
||||
'unique' => ($record['UNIQUENESS'] === 'UNIQUE'),
|
||||
'columns' => array());
|
||||
}
|
||||
|
||||
// If this is an unique, function-based, index, then we have to look to the expression
|
||||
// and calculate the column name by parsing it.
|
||||
if ($record['UNIQUENESS'] === 'UNIQUE' && $record['INDEX_TYPE'] === 'FUNCTION-BASED NORMAL') {
|
||||
// Only if there is an expression to look.
|
||||
if (!empty($record['COLUMN_EXPRESSION'])) {
|
||||
// Let's parse the usual code used for these unique indexes.
|
||||
$regex = '/^CASE *WHEN .* THEN "(?<column_name>[^"]+)" ELSE NULL END *$/';
|
||||
if (preg_match($regex, $record['COLUMN_EXPRESSION'], $matches)) {
|
||||
$record['COLUMN_NAME'] = $matches['column_name'] ?? $record['COLUMN_NAME'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$indexes[$indexname]['columns'][] = strtolower($record['COLUMN_NAME']);
|
||||
}
|
||||
|
||||
|
||||
@@ -711,6 +711,41 @@ EOD;
|
||||
$this->assertSame('id', $composed['columns'][1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Let's verify get_indexes() when we mix null and not null columns in unique indexes.
|
||||
*
|
||||
* Some databases, for unique indexes of this type, need to create function indexes to
|
||||
* provide cross-db behaviour. Here we check that those indexes don't break get_indexes().
|
||||
*
|
||||
* Note that, strictly speaking, unique indexes on null columns are far from ideal. Both
|
||||
* conceptually and also in practice, because they cause DBs to use full scans in a
|
||||
* number of situations. But if we support them, we need to ensure get_indexes() work on them.
|
||||
*/
|
||||
public function test_get_indexes_unique_mixed_nullability() {
|
||||
$DB = $this->tdb;
|
||||
$dbman = $this->tdb->get_manager();
|
||||
$table = $this->get_test_table();
|
||||
$tablename = $table->getName();
|
||||
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('nullable01', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
|
||||
$table->add_field('nullable02', XMLDB_TYPE_INTEGER, 10, null, null, null, null);
|
||||
$table->add_field('nonullable01', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('nonullable02', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
$indexcolumns = ['nullable01', 'nonullable01', 'nullable02', 'nonullable02'];
|
||||
$table->add_index('course-id', XMLDB_INDEX_UNIQUE, $indexcolumns);
|
||||
$dbman->create_table($table);
|
||||
|
||||
$indexes = $DB->get_indexes($tablename);
|
||||
$this->assertIsArray($indexes);
|
||||
$this->assertCount(1, $indexes);
|
||||
|
||||
$index = array_shift($indexes);
|
||||
$this->assertTrue($index['unique']);
|
||||
$this->assertSame($indexcolumns, $index['columns']);
|
||||
}
|
||||
|
||||
public function test_get_columns() {
|
||||
$DB = $this->tdb;
|
||||
$dbman = $this->tdb->get_manager();
|
||||
|
||||
Vendored
+43
@@ -434,6 +434,12 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
this.editor.delegate('dblclick', this._displayDialogue, 'video', this);
|
||||
this.editor.delegate('click', this._handleClick, 'video', this);
|
||||
|
||||
// For some reason, clicking the video on Firefox does not trigger the click event, while in Chrome it does.
|
||||
// We also need to handle the play/pause instead.
|
||||
this._attachPlayPauseEvents();
|
||||
var changeHandler = this._attachPlayPauseEvents.bind(this);
|
||||
this.get('host').on('change', changeHandler, null);
|
||||
|
||||
this.addButton({
|
||||
icon: 'e/insert_edit_video',
|
||||
callback: this._displayDialogue,
|
||||
@@ -443,6 +449,26 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches Play/Pause events to the video nodes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_attachPlayPauseEvents: function() {
|
||||
if (this._handlePlayEndBound === undefined) {
|
||||
this._handlePlayEndBound = this._handlePlayEnd.bind(this);
|
||||
}
|
||||
var videos = this.editor.getDOMNode().querySelectorAll('video');
|
||||
videos.forEach(function(video) {
|
||||
// Prevent duplicated event listeners.
|
||||
video.removeEventListener('play', this._handlePlayEndBound);
|
||||
video.removeEventListener('pause', this._handlePlayEndBound);
|
||||
// Add event listeners.
|
||||
video.addEventListener('play', this._handlePlayEndBound);
|
||||
video.addEventListener('pause', this._handlePlayEndBound);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the root context for all templates, with extra supplied context.
|
||||
*
|
||||
@@ -478,6 +504,23 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a play/end on a media element.
|
||||
*
|
||||
* @method _handlePlayEnd
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
_handlePlayEnd: function(e) {
|
||||
var medium = Y.one(e.target);
|
||||
|
||||
var selection = this.get('host').getSelectionFromNode(medium);
|
||||
if (this.get('host').getSelection() !== selection) {
|
||||
this.get('host').setSelection(selection);
|
||||
this.get('host')._hasSelectionChanged(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the media editing tool.
|
||||
*
|
||||
|
||||
Vendored
+4
-4
File diff suppressed because one or more lines are too long
Vendored
+43
@@ -434,6 +434,12 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
this.editor.delegate('dblclick', this._displayDialogue, 'video', this);
|
||||
this.editor.delegate('click', this._handleClick, 'video', this);
|
||||
|
||||
// For some reason, clicking the video on Firefox does not trigger the click event, while in Chrome it does.
|
||||
// We also need to handle the play/pause instead.
|
||||
this._attachPlayPauseEvents();
|
||||
var changeHandler = this._attachPlayPauseEvents.bind(this);
|
||||
this.get('host').on('change', changeHandler, null);
|
||||
|
||||
this.addButton({
|
||||
icon: 'e/insert_edit_video',
|
||||
callback: this._displayDialogue,
|
||||
@@ -443,6 +449,26 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches Play/Pause events to the video nodes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_attachPlayPauseEvents: function() {
|
||||
if (this._handlePlayEndBound === undefined) {
|
||||
this._handlePlayEndBound = this._handlePlayEnd.bind(this);
|
||||
}
|
||||
var videos = this.editor.getDOMNode().querySelectorAll('video');
|
||||
videos.forEach(function(video) {
|
||||
// Prevent duplicated event listeners.
|
||||
video.removeEventListener('play', this._handlePlayEndBound);
|
||||
video.removeEventListener('pause', this._handlePlayEndBound);
|
||||
// Add event listeners.
|
||||
video.addEventListener('play', this._handlePlayEndBound);
|
||||
video.addEventListener('pause', this._handlePlayEndBound);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the root context for all templates, with extra supplied context.
|
||||
*
|
||||
@@ -478,6 +504,23 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a play/end on a media element.
|
||||
*
|
||||
* @method _handlePlayEnd
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
_handlePlayEnd: function(e) {
|
||||
var medium = Y.one(e.target);
|
||||
|
||||
var selection = this.get('host').getSelectionFromNode(medium);
|
||||
if (this.get('host').getSelection() !== selection) {
|
||||
this.get('host').setSelection(selection);
|
||||
this.get('host')._hasSelectionChanged(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the media editing tool.
|
||||
*
|
||||
|
||||
@@ -432,6 +432,12 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
this.editor.delegate('dblclick', this._displayDialogue, 'video', this);
|
||||
this.editor.delegate('click', this._handleClick, 'video', this);
|
||||
|
||||
// For some reason, clicking the video on Firefox does not trigger the click event, while in Chrome it does.
|
||||
// We also need to handle the play/pause instead.
|
||||
this._attachPlayPauseEvents();
|
||||
var changeHandler = this._attachPlayPauseEvents.bind(this);
|
||||
this.get('host').on('change', changeHandler, null);
|
||||
|
||||
this.addButton({
|
||||
icon: 'e/insert_edit_video',
|
||||
callback: this._displayDialogue,
|
||||
@@ -441,6 +447,26 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attaches Play/Pause events to the video nodes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_attachPlayPauseEvents: function() {
|
||||
if (this._handlePlayEndBound === undefined) {
|
||||
this._handlePlayEndBound = this._handlePlayEnd.bind(this);
|
||||
}
|
||||
var videos = this.editor.getDOMNode().querySelectorAll('video');
|
||||
videos.forEach(function(video) {
|
||||
// Prevent duplicated event listeners.
|
||||
video.removeEventListener('play', this._handlePlayEndBound);
|
||||
video.removeEventListener('pause', this._handlePlayEndBound);
|
||||
// Add event listeners.
|
||||
video.addEventListener('play', this._handlePlayEndBound);
|
||||
video.addEventListener('pause', this._handlePlayEndBound);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the root context for all templates, with extra supplied context.
|
||||
*
|
||||
@@ -476,6 +502,23 @@ Y.namespace('M.atto_media').Button = Y.Base.create('button', Y.M.editor_atto.Edi
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a play/end on a media element.
|
||||
*
|
||||
* @method _handlePlayEnd
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
_handlePlayEnd: function(e) {
|
||||
var medium = Y.one(e.target);
|
||||
|
||||
var selection = this.get('host').getSelectionFromNode(medium);
|
||||
if (this.get('host').getSelection() !== selection) {
|
||||
this.get('host').setSelection(selection);
|
||||
this.get('host')._hasSelectionChanged(e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the media editing tool.
|
||||
*
|
||||
|
||||
@@ -2672,6 +2672,8 @@ function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring
|
||||
function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownload=false, array $options=array()) {
|
||||
global $CFG, $COURSE;
|
||||
|
||||
static $recursion = 0;
|
||||
|
||||
if (empty($options['filename'])) {
|
||||
$filename = null;
|
||||
} else {
|
||||
@@ -2715,6 +2717,13 @@ function send_stored_file($stored_file, $lifetime=null, $filter=0, $forcedownloa
|
||||
|
||||
// handle external resource
|
||||
if ($stored_file && $stored_file->is_external_file() && !isset($options['sendcachedexternalfile'])) {
|
||||
|
||||
// Have we been here before?
|
||||
$recursion++;
|
||||
if ($recursion > 10) {
|
||||
throw new coding_exception('Recursive file serving detected');
|
||||
}
|
||||
|
||||
$stored_file->send_file($lifetime, $filter, $forcedownload, $options);
|
||||
die;
|
||||
}
|
||||
|
||||
@@ -664,6 +664,13 @@ function filter_set_global_state($filtername, $state, $move = 0) {
|
||||
|
||||
// Move only active.
|
||||
if ($move != 0 and isset($on[$filter->filter])) {
|
||||
// Capture the old order for logging.
|
||||
$oldorder = implode(', ', array_map(
|
||||
function($f) {
|
||||
return $f->filter;
|
||||
}, $on));
|
||||
|
||||
// Work out the new order.
|
||||
$i = 1;
|
||||
foreach ($on as $f) {
|
||||
$f->newsortorder = $i;
|
||||
@@ -686,6 +693,13 @@ function filter_set_global_state($filtername, $state, $move = 0) {
|
||||
}
|
||||
|
||||
core_collator::asort_objects_by_property($on, 'newsortorder', core_collator::SORT_NUMERIC);
|
||||
|
||||
// Log in config_log.
|
||||
$neworder = implode(', ', array_map(
|
||||
function($f) {
|
||||
return $f->filter;
|
||||
}, $on));
|
||||
add_to_config_log('order', $oldorder, $neworder, 'core_filter');
|
||||
}
|
||||
|
||||
// Inactive are sorted by filter name.
|
||||
|
||||
+17
-5
@@ -60,19 +60,31 @@ class flickr_client extends oauth_helper {
|
||||
* @param moodle_url|string $callbackurl
|
||||
*/
|
||||
public function __construct($consumerkey, $consumersecret, $callbackurl = '') {
|
||||
global $CFG;
|
||||
$version = moodle_major_version();
|
||||
$useragent = "MoodleSite/$version (+{$CFG->wwwroot})";
|
||||
|
||||
parent::__construct([
|
||||
'api_root' => self::OAUTH_ROOT,
|
||||
'oauth_consumer_key' => $consumerkey,
|
||||
'oauth_consumer_secret' => $consumersecret,
|
||||
'oauth_callback' => $callbackurl,
|
||||
'http_options' => ['CURLOPT_USERAGENT' => $useragent]
|
||||
'http_options' => [
|
||||
'CURLOPT_USERAGENT' => static::user_agent(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return User-Agent string suitable for calls to Flickr endpoint, avoiding problems caused by the string returned by
|
||||
* the {@see core_useragent::get_moodlebot_useragent} helper, which is often rejected due to presence of "Bot" within
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function user_agent(): string {
|
||||
global $CFG;
|
||||
|
||||
$version = moodle_major_version();
|
||||
|
||||
return "MoodleSite/{$version} (+{$CFG->wwwroot})";
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporarily store the request token secret in the session.
|
||||
*
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
* @subpackage 3rd-party
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once("{$CFG->libdir}/flickrclient.php");
|
||||
|
||||
/**
|
||||
* Flickr Class
|
||||
* @package moodlecore
|
||||
@@ -78,7 +83,12 @@ class phpFlickr {
|
||||
//Find the PHP version and store it for future reference
|
||||
$this->php_version = explode("-", phpversion());
|
||||
$this->php_version = explode(".", $this->php_version[0]);
|
||||
|
||||
// Initialize curl helper, set custom user agent as Flickr blocks our "MoodleBot" agent string.
|
||||
$this->curl = new curl(array('cache'=>true, 'module_cache'=>'repository'));
|
||||
$this->curl->setopt([
|
||||
'CURLOPT_USERAGENT' => flickr_client::user_agent(),
|
||||
]);
|
||||
}
|
||||
|
||||
function request ($command, $args = array())
|
||||
|
||||
+16
-14
@@ -80,16 +80,18 @@ M.form.dateselector = {
|
||||
this.panel.render(document.body);
|
||||
|
||||
// Determine the correct zindex by looking at all existing dialogs and menubars in the page.
|
||||
var highestzindex = 0;
|
||||
Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) {
|
||||
var zindex = this.findZIndex(node);
|
||||
if (zindex > highestzindex) {
|
||||
highestzindex = zindex;
|
||||
}
|
||||
this.panel.on('focus', function() {
|
||||
var highestzindex = 0;
|
||||
Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) {
|
||||
var zindex = this.findZIndex(node);
|
||||
if (zindex > highestzindex) {
|
||||
highestzindex = zindex;
|
||||
}
|
||||
}, this);
|
||||
// Only set the zindex if we found a wrapper.
|
||||
var zindexvalue = (highestzindex + 1).toString();
|
||||
Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue);
|
||||
}, this);
|
||||
// Only set the zindex if we found a wrapper.
|
||||
var zindexvalue = (highestzindex + 1).toString();
|
||||
Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue);
|
||||
|
||||
this.panel.on('heightChange', this.fix_position, this);
|
||||
|
||||
@@ -358,14 +360,14 @@ CALENDAR.prototype = {
|
||||
}
|
||||
},
|
||||
toggle_calendar_image: function() {
|
||||
// If the enable checkbox is det checked, disable the image.
|
||||
// If the enable checkbox is not checked, disable the calendar image and prevent focus.
|
||||
if (!this.enablecheckbox.get('checked')) {
|
||||
this.calendarimage.set('disabled', 'disabled');
|
||||
this.calendarimage.setStyle('cursor', 'default');
|
||||
this.calendarimage.addClass('disabled');
|
||||
this.calendarimage.setAttribute('tabindex', -1);
|
||||
this.release_calendar();
|
||||
} else {
|
||||
this.calendarimage.set('disabled', false);
|
||||
this.calendarimage.setStyle('cursor', null);
|
||||
this.calendarimage.removeClass('disabled');
|
||||
this.calendarimage.setAttribute('tabindex', 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
@@ -80,16 +80,18 @@ M.form.dateselector = {
|
||||
this.panel.render(document.body);
|
||||
|
||||
// Determine the correct zindex by looking at all existing dialogs and menubars in the page.
|
||||
var highestzindex = 0;
|
||||
Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) {
|
||||
var zindex = this.findZIndex(node);
|
||||
if (zindex > highestzindex) {
|
||||
highestzindex = zindex;
|
||||
}
|
||||
this.panel.on('focus', function() {
|
||||
var highestzindex = 0;
|
||||
Y.all(DIALOGUE_SELECTOR + ', ' + MENUBAR_SELECTOR + ', ' + DOT + HAS_ZINDEX).each(function(node) {
|
||||
var zindex = this.findZIndex(node);
|
||||
if (zindex > highestzindex) {
|
||||
highestzindex = zindex;
|
||||
}
|
||||
}, this);
|
||||
// Only set the zindex if we found a wrapper.
|
||||
var zindexvalue = (highestzindex + 1).toString();
|
||||
Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue);
|
||||
}, this);
|
||||
// Only set the zindex if we found a wrapper.
|
||||
var zindexvalue = (highestzindex + 1).toString();
|
||||
Y.one('#dateselector-calendar-panel').setStyle('zIndex', zindexvalue);
|
||||
|
||||
this.panel.on('heightChange', this.fix_position, this);
|
||||
|
||||
@@ -358,14 +360,14 @@ CALENDAR.prototype = {
|
||||
}
|
||||
},
|
||||
toggle_calendar_image: function() {
|
||||
// If the enable checkbox is det checked, disable the image.
|
||||
// If the enable checkbox is not checked, disable the calendar image and prevent focus.
|
||||
if (!this.enablecheckbox.get('checked')) {
|
||||
this.calendarimage.set('disabled', 'disabled');
|
||||
this.calendarimage.setStyle('cursor', 'default');
|
||||
this.calendarimage.addClass('disabled');
|
||||
this.calendarimage.setAttribute('tabindex', -1);
|
||||
this.release_calendar();
|
||||
} else {
|
||||
this.calendarimage.set('disabled', false);
|
||||
this.calendarimage.setStyle('cursor', null);
|
||||
this.calendarimage.removeClass('disabled');
|
||||
this.calendarimage.setAttribute('tabindex', 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -228,9 +228,11 @@ Y.extend(SHORTFORMS, Y.Base, {
|
||||
// Updating the button.
|
||||
if (expandbtn) {
|
||||
btn.removeClass(CSS.COLLAPSEALL);
|
||||
btn.setAttribute('aria-expanded', false);
|
||||
btn.setHTML(M.util.get_string('expandall', 'moodle'));
|
||||
} else {
|
||||
btn.addClass(CSS.COLLAPSEALL);
|
||||
btn.setAttribute('aria-expanded', true);
|
||||
btn.setHTML(M.util.get_string('collapseall', 'moodle'));
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
YUI.add("moodle-form-shortforms",function(r,e){function t(){t.superclass.constructor.apply(this,arguments)}var o=".collapsible-actions .collapseexpand",a=".collapsed",i="fieldset.collapsible",l="fieldset.collapsible .fheader",n=".fheader",d="legend.ftoggler",c="collapse-all",f="collapsed",h="fheader",s={formid:{value:null}};r.extend(t,r.Base,{form:null,initializer:function(){var e,t,s,a=r.one("#"+this.get("formid"));a&&((this.form=a).all(i).each(this.process_fieldset,this),a.delegate("click",this.switch_state,l,this),a.delegate("key",this.switch_state,"down:enter,32",l,this),r.Global.on(M.core.globalEvents.FORM_ERROR,this.expand_fieldset,this),(e=a.one(o))&&((t=r.Node.create('<a href="#"></a>')).setHTML(e.getHTML()),t.setAttribute("class",e.getAttribute("class")),t.setAttribute("role","button"),s=[],a.all(l).each(function(e){s[s.length]=e.generateID()}),t.setAttribute("aria-controls",s.join(" ")),t.on("click",this.set_state_all,this,!0),t.on("key",this.set_state_all,"down:enter,32",this,!0),e.replace(t),this.update_btns(a)))},process_fieldset:function(e){var t=e.one(d),s=r.Node.create('<a href="#"></a>');return s.addClass(h),s.appendChild(t.get("firstChild")),s.setAttribute("role","button"),s.setAttribute("aria-controls",e.generateID()),t.ancestor(a)?s.setAttribute("aria-expanded","false"):s.setAttribute("aria-expanded","true"),t.prepend(s),this},set_state:function(e,t){var s=e.one(n);return t?(e.addClass(f),s&&s.setAttribute("aria-expanded","false")):(e.removeClass(f),s&&s.setAttribute("aria-expanded","true")),(s=this.form.one("input[name=mform_isexpanded_"+e.get("id")+"]"))&&s.set("value",t?0:1),this},set_state_all:function(e){e.preventDefault();var t=e.target.hasClass(c);this.form.all(i).each(function(e){this.set_state(e,t)},this),this.update_btns()},switch_state:function(e){e.preventDefault();e=e.target.ancestor(i);this.set_state(e,!e.hasClass(f)),this.update_btns()},update_btns:function(){var t=0,e=!1,s=this.form.one(o);return s&&(this.form.all(i).each(function(e){e.hasClass(f)&&t++}),(e=0!==t?!0:e)?(s.removeClass(c),s.setHTML(M.util.get_string("expandall","moodle"))):(s.addClass(c),s.setHTML(M.util.get_string("collapseall","moodle")))),this},expand_fieldset:function(e){var t;e.stopPropagation(),e.formid===this.form.getAttribute("id")&&(t=r.one("#"+e.elementid).ancestor("fieldset"))&&this.set_state(t,!1)}},{NAME:"moodle-form-shortforms",ATTRS:s}),M.form=M.form||{},M.form.shortforms=M.form.shortforms||function(e){return new t(e)}},"@VERSION@",{requires:["node","base","selector-css3","moodle-core-event"]});
|
||||
YUI.add("moodle-form-shortforms",function(r,e){function t(){t.superclass.constructor.apply(this,arguments)}var i=".collapsible-actions .collapseexpand",a=".collapsed",o="fieldset.collapsible",l="fieldset.collapsible .fheader",n=".fheader",d="legend.ftoggler",c="collapse-all",f="collapsed",h="fheader",s={formid:{value:null}};r.extend(t,r.Base,{form:null,initializer:function(){var e,t,s,a=r.one("#"+this.get("formid"));a&&((this.form=a).all(o).each(this.process_fieldset,this),a.delegate("click",this.switch_state,l,this),a.delegate("key",this.switch_state,"down:enter,32",l,this),r.Global.on(M.core.globalEvents.FORM_ERROR,this.expand_fieldset,this),(e=a.one(i))&&((t=r.Node.create('<a href="#"></a>')).setHTML(e.getHTML()),t.setAttribute("class",e.getAttribute("class")),t.setAttribute("role","button"),s=[],a.all(l).each(function(e){s[s.length]=e.generateID()}),t.setAttribute("aria-controls",s.join(" ")),t.on("click",this.set_state_all,this,!0),t.on("key",this.set_state_all,"down:enter,32",this,!0),e.replace(t),this.update_btns(a)))},process_fieldset:function(e){var t=e.one(d),s=r.Node.create('<a href="#"></a>');return s.addClass(h),s.appendChild(t.get("firstChild")),s.setAttribute("role","button"),s.setAttribute("aria-controls",e.generateID()),t.ancestor(a)?s.setAttribute("aria-expanded","false"):s.setAttribute("aria-expanded","true"),t.prepend(s),this},set_state:function(e,t){var s=e.one(n);return t?(e.addClass(f),s&&s.setAttribute("aria-expanded","false")):(e.removeClass(f),s&&s.setAttribute("aria-expanded","true")),(s=this.form.one("input[name=mform_isexpanded_"+e.get("id")+"]"))&&s.set("value",t?0:1),this},set_state_all:function(e){e.preventDefault();var t=e.target.hasClass(c);this.form.all(o).each(function(e){this.set_state(e,t)},this),this.update_btns()},switch_state:function(e){e.preventDefault();e=e.target.ancestor(o);this.set_state(e,!e.hasClass(f)),this.update_btns()},update_btns:function(){var t=0,e=!1,s=this.form.one(i);return s&&(this.form.all(o).each(function(e){e.hasClass(f)&&t++}),(e=0!==t?!0:e)?(s.removeClass(c),s.setAttribute("aria-expanded",!1),s.setHTML(M.util.get_string("expandall","moodle"))):(s.addClass(c),s.setAttribute("aria-expanded",!0),s.setHTML(M.util.get_string("collapseall","moodle")))),this},expand_fieldset:function(e){var t;e.stopPropagation(),e.formid===this.form.getAttribute("id")&&(t=r.one("#"+e.elementid).ancestor("fieldset"))&&this.set_state(t,!1)}},{NAME:"moodle-form-shortforms",ATTRS:s}),M.form=M.form||{},M.form.shortforms=M.form.shortforms||function(e){return new t(e)}},"@VERSION@",{requires:["node","base","selector-css3","moodle-core-event"]});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user