Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df1157bb84 | |||
| 6e3f6a9cb9 | |||
| 98f72dee05 | |||
| 4284ff971e | |||
| ec20fc4868 | |||
| e2f12b1e7a | |||
| 347e10c764 | |||
| 191365e09f | |||
| 04b3e9d359 | |||
| 4419db89d2 | |||
| a5aeb2ef10 | |||
| c4e2106834 | |||
| 906f0191aa | |||
| 6a0b2ee174 | |||
| 1e54b2a86b | |||
| 8f31bda522 | |||
| bdeba5416a | |||
| 238bc8bf87 | |||
| 20388bbdac | |||
| 2354627702 | |||
| 592dcf7f7f | |||
| 0f54245988 | |||
| 22f722d2d0 | |||
| ec12af83dd | |||
| 4e47698ca9 | |||
| e6651b7bde | |||
| d0dc60895c | |||
| 3512c1fe32 | |||
| 26bea242d9 | |||
| c6672aa58c | |||
| c0e2de83b2 | |||
| 6dfc4a77eb | |||
| 6dc2d2b9ec | |||
| 3046f836aa | |||
| 54e1488821 | |||
| b7cd590de7 | |||
| ffa968a34a | |||
| cd4c94ca68 | |||
| f8b2bf5a3f | |||
| 63ab738ae2 | |||
| f0c327b94c | |||
| 18065046f4 | |||
| 0d58c33e7d | |||
| bc575ee1ac | |||
| f087c4eb99 | |||
| 60bc55dc8c | |||
| f04d7d4a3f | |||
| 6c06fab0a1 | |||
| 291e340b9a | |||
| 90d176b2d5 | |||
| 86a7753def | |||
| 4563fd2fe2 | |||
| da14987cbf | |||
| 035f84ee8c | |||
| 3e0a272b74 | |||
| f3a499a129 | |||
| 629338999b | |||
| 75f2b6ad5d | |||
| 637a6bdb45 | |||
| 7c7ae75b1f | |||
| fb3d29e53d | |||
| 6f77d3c5b0 | |||
| 4b7ccd75d8 | |||
| 053e93b18a | |||
| e84c2d821a | |||
| a6d9be042d | |||
| a255637c74 | |||
| b19752f929 | |||
| c9ce233f08 | |||
| 7f5911b137 | |||
| 2ca67ce948 | |||
| ccbfbbc3bf | |||
| 9c4134efca | |||
| 752ed6e63e | |||
| 7ed44dc763 | |||
| ba295cac53 | |||
| ef0c245178 | |||
| f3fcc635e9 | |||
| e375cbc024 | |||
| be83232f57 | |||
| 3a3d251c91 | |||
| 7135adf77c | |||
| 3a53982564 | |||
| 52d8c7259b | |||
| 95a93ab74d | |||
| 40736ec563 | |||
| 3b37608523 | |||
| adcb52a66c | |||
| c899c6fb5a | |||
| 6d6fea6c3e | |||
| 9a257e13c2 | |||
| 239be346a1 | |||
| e23a5c6b85 | |||
| fd6667f694 | |||
| 6d990ff157 | |||
| 5ad39879ea | |||
| 0e8027d3dd | |||
| 13ab8ce3f6 | |||
| 321f20bde4 | |||
| ccb94debcf | |||
| 596f290ca7 | |||
| 3cf753aea9 | |||
| 2689a8fdd3 | |||
| 0ef7b2853d | |||
| 3389ddbe77 | |||
| 9c0d3f8037 | |||
| 7b5bdf9b6e | |||
| 78468dffaa | |||
| d314c4719d | |||
| 4b30d08025 | |||
| bc29ef5d9e | |||
| 3030cde4ba | |||
| bfef5e4581 | |||
| 17c4412033 | |||
| 5b9e5416d5 | |||
| d3777d8a06 | |||
| 3613faf8d6 | |||
| 9b060f43ce | |||
| d63b359944 | |||
| 144c57c4ff | |||
| 9956df769b | |||
| b2fab52908 | |||
| 787199fa99 | |||
| 0afa5bfdb5 | |||
| 9247cf1bd6 | |||
| 42eeec6261 | |||
| 8b234f6690 | |||
| f4795ade4a | |||
| fc1a79d882 | |||
| 6b715e7e7f | |||
| bd96199040 | |||
| 127b856a73 | |||
| 28f675da81 | |||
| f2552bb9c4 |
@@ -3835,6 +3835,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4410,6 +4411,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4605,6 +4607,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4800,6 +4803,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -4995,6 +4999,7 @@
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
@@ -5098,4 +5103,208 @@
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="5.2" requires="4.4">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.11.0" />
|
||||
<VENDOR name="mysql" version="8.4" />
|
||||
<VENDOR name="postgres" version="16" />
|
||||
<VENDOR name="mssql" version="15.0" />
|
||||
<VENDOR name="auroramysql" version="8.0" />
|
||||
</DATABASE>
|
||||
<PHP version="8.3.0" level="required">
|
||||
</PHP>
|
||||
<PCREUNICODE level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="pcreunicodewarning" />
|
||||
</FEEDBACK>
|
||||
</PCREUNICODE>
|
||||
<PHP_EXTENSIONS>
|
||||
<PHP_EXTENSION name="iconv" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="iconvrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="mbstring" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="mbstringrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="curl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="curlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="openssl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="opensslrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="tokenizer" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="ctype" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ctyperequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zip" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="ziprequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="zlib" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="gd" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="gdrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="simplexml" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="simplexmlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="spl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="splrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="pcre" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="dom" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xml" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlreader" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="intl" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="intlrequired" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="json" level="required">
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="hash" level="required"/>
|
||||
<PHP_EXTENSION name="fileinfo" level="required"/>
|
||||
<PHP_EXTENSION name="sodium" level="required"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
<PHP_EXTENSION name="filter" level="required"/>
|
||||
</PHP_EXTENSIONS>
|
||||
<PHP_SETTINGS>
|
||||
<PHP_SETTING name="memory_limit" value="96M" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="settingmemorylimit" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="file_uploads" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingfileuploads" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="opcache.enable" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="opcacherecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
<PHP_SETTING name="zend.exception_ignore_args" value="1" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="settingzendexceptionignoreargs" />
|
||||
</FEEDBACK>
|
||||
</PHP_SETTING>
|
||||
</PHP_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_developer_dependencies_not_installed" level="optional" />
|
||||
<CUSTOM_CHECK function="\core\environment::check_composer_dependencies_optimised" level="optional" />
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_storage_engine" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbstorageengine" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="question/engine/upgrade/upgradelib.php" function="quiz_attempts_upgraded" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="quizattemptsupgradedmessage" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_slasharguments" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="slashargumentswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_database_tables_row_format" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unsupporteddbtablerowformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_unoconv_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="unoconvwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_libcurl_version" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="libcurlwarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_format" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfileformat" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_file_per_table" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddbfilepertable" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_large_prefix" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unsupporteddblargeprefix" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_is_https" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="ishttpswarning" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mysql_incomplete_unicode_support" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="incompleteunicodesupport" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_sixtyfour_bits" level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="sixtyfourbitsrequired" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_max_input_vars" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_admin_dir_usage" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_mod_assignment" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_db_prefix_length" level="required">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_async_backup" level="recommended">
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_aurora_version" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
+8
-2
@@ -916,8 +916,14 @@ if (empty($CFG->disabledevlibdirscheck) && (is_dir($CFG->dirroot.'/vendor') || i
|
||||
// Check if the site is being foced onto ssl.
|
||||
$overridetossl = !empty($CFG->overridetossl);
|
||||
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
if (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) {
|
||||
// We need to add this in order to be able to verify the showcampaigncontent setting behaviour during behat tests.
|
||||
$showcampaigncontent = get_config('core', 'showcampaigncontent');
|
||||
$showcampaigncontent = $showcampaigncontent === 'true';
|
||||
} else {
|
||||
// Check if moodle campaign content setting is enabled or not.
|
||||
$showcampaigncontent = !isset($CFG->showcampaigncontent) || $CFG->showcampaigncontent;
|
||||
}
|
||||
|
||||
// Encourage admins to enable the user feedback feature if it is not enabled already.
|
||||
$showfeedbackencouragement = empty($CFG->enableuserfeedback);
|
||||
|
||||
@@ -51,6 +51,14 @@ if ($hassiteconfig) {
|
||||
|
||||
$temp->add(new admin_setting_countrycodes('allcountrycodes', new lang_string('allcountrycodes', 'core_admin'),
|
||||
new lang_string('configallcountrycodes', 'core_admin')));
|
||||
|
||||
$temp->add(new admin_setting_configtext(
|
||||
'geopluginapikey',
|
||||
new lang_string('geopluginapikey', 'core_admin'),
|
||||
new lang_string('geopluginapikey_desc', 'core_admin'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
));
|
||||
}
|
||||
|
||||
$ADMIN->add('location', $temp);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
@core @core_admin
|
||||
Feature: Toggle campaign banner visibility
|
||||
In order to control the visibility of the campaign banner content
|
||||
As an admin
|
||||
I need to be able to disable the campaign banner display
|
||||
|
||||
Scenario Outline: Admin can disable the campaign banner display
|
||||
Given the following config values are set as admin:
|
||||
| showcampaigncontent | <showcampaigncontent> |
|
||||
And I log in as "admin"
|
||||
When I navigate to "Notifications" in site administration
|
||||
Then "//iframe[@id='campaign-content']" "xpath_element" <display> exist
|
||||
|
||||
Examples:
|
||||
| showcampaigncontent | display |
|
||||
| true | should |
|
||||
| false | should not |
|
||||
@@ -0,0 +1,77 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Manage CRUD operations for competencies
|
||||
In order to perform CRUD operations on competencies
|
||||
As a manager
|
||||
I need to be able to create, read, update and delete competencies
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber | description |
|
||||
| C1 | CF1 | C1ID | C1 description |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Create a new competency
|
||||
# Targets the `CF1` element to make `Add competency` button visible and accessible to avoid ambiguity with the other 'CF1' element on the screen.
|
||||
Given I click on "//ul[@class='competency-tree']//span[text()='CF1']" "xpath_element"
|
||||
When I press "Add competency"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
# Access the newly created competencies to ensure that correct information was registered.
|
||||
Then "Competency created" "text" should exist
|
||||
And "C2" "text" should appear after "C1" "text"
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C2 description" "text" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Read a competency
|
||||
When I select "C1" of the competency tree
|
||||
# Confirm that selected competency info displayed matches registered info.
|
||||
Then "C1ID" "text" should exist
|
||||
And "C1 description" "text" should exist
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
# Confirm that the details displayed when accessing edit screen values match registered data.
|
||||
And the field "Name" matches value "C1"
|
||||
And the field "Description" matches value "C1 description"
|
||||
And the field "ID number" matches value "C1ID"
|
||||
|
||||
@javascript
|
||||
Scenario: Update a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
When I click on "[data-action=edit]" "css_element"
|
||||
And I set the field "Name" to "C2"
|
||||
And I set the field "Description" to "C2 description"
|
||||
And I set the field "ID number" to "C2ID"
|
||||
And I press "Save changes"
|
||||
Then "Competency updated" "text" should exist
|
||||
And "C1" "text" should not exist
|
||||
And "C2" "text" should exist
|
||||
And I select "C2" of the competency tree
|
||||
And "C2ID" "text" should exist
|
||||
And "C1ID" "text" should not exist
|
||||
And "C2 description" "text" should exist
|
||||
And "C1 description" "text" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Delete a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Delete" "link"
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
# Confirm that "C1" competency was successfully deleted.
|
||||
Then "C1" "text" should not exist
|
||||
@@ -0,0 +1,63 @@
|
||||
@tool @tool_lp @tool_lp_framework
|
||||
Feature: Move and cross-reference competencies
|
||||
In order to move and cross-reference competencies
|
||||
As a manager
|
||||
I need to be open the competency's menu items.
|
||||
|
||||
Background:
|
||||
Given the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework |
|
||||
| C1 | CF1 |
|
||||
| C2 | CF1 |
|
||||
| C3 | CF1 |
|
||||
| C4 | CF1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Competency frameworks" in site administration
|
||||
And I click on "CF1 (CF1)" "link"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Move up/Move down menu items
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Move down" "link"
|
||||
Then "C1" "text" should appear after "C2" "text"
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
And I click on "Move up" "link"
|
||||
And "C1" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Move a competency using Relocate menu item
|
||||
Given I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Relocate" "link"
|
||||
And I click on "C1" "text" in the "Move competency" "dialogue"
|
||||
And I click on "Move" "button" in the "Move competency" "dialogue"
|
||||
Then "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
And I select "C3" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
# Similar to the previous step, to avoid ambiguity with the competency framework "Edit", target css element with data-action=edit.
|
||||
And I click on "[data-action=edit]" "css_element"
|
||||
And "C1" "text" should exist
|
||||
And "No parent (top-level competency)" "text" should not exist
|
||||
And I press "Cancel"
|
||||
And "C1" "text" should appear before "C3" "text"
|
||||
And "C3" "text" should appear before "C2" "text"
|
||||
|
||||
@javascript
|
||||
Scenario: Cross-reference a competency
|
||||
Given I select "C1" of the competency tree
|
||||
# Targets the unique 'Edit' link needed to open the menu, avoiding ambiguity with the other 'Edit' link.
|
||||
And I click on "//a[@href='#' and text()='Edit']" "xpath_element"
|
||||
When I click on "Add cross-referenced competency" "link"
|
||||
And I click on "C2" "text" in the "Competency picker" "dialogue"
|
||||
And I click on "Add" "button" in the "Competency picker" "dialogue"
|
||||
Then "Cross-referenced competencies:" "text" should exist
|
||||
And I should see "C2 cmp2"
|
||||
@@ -16,16 +16,10 @@ Feature: Manage plan workflow
|
||||
| usermanageownplan | User manage own plan role | user |
|
||||
| manageplan | Manager all plans role | manager |
|
||||
And the following "role capabilities" exist:
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown |
|
||||
| usermanageowndraftplan | allow | |
|
||||
| usermanageownplan | allow | allow |
|
||||
| manageplan | allow | allow |
|
||||
And the following "role capability" exists:
|
||||
| role | manageplan |
|
||||
| moodle/competency:planmanage | allow |
|
||||
| moodle/competency:planview | allow |
|
||||
| moodle/competency:planreview | allow |
|
||||
| moodle/competency:planrequestreview | allow |
|
||||
| role | moodle/competency:planmanageowndraft | moodle/competency:planmanageown | moodle/competency:planmanage | moodle/competency:planview | moodle/competency:planreview | moodle/competency:planrequestreview |
|
||||
| usermanageowndraftplan | allow | | | | | |
|
||||
| usermanageownplan | allow | allow | | | | |
|
||||
| manageplan | allow | allow | allow | allow | allow | allow |
|
||||
And the following "role assigns" exist:
|
||||
| user | role | contextlevel | reference |
|
||||
| user1 | usermanageowndraftplan | System | |
|
||||
@@ -53,8 +47,7 @@ Feature: Manage plan workflow
|
||||
| lp | System | 1 | my-index | content |
|
||||
|
||||
Scenario: User can manages his own plan draft
|
||||
Given I log in as "user1"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user1" "user > profile" page logged in as user1
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan1"
|
||||
@@ -63,11 +56,9 @@ Feature: Manage plan workflow
|
||||
And I should see "Waiting for review"
|
||||
And I click on "Cancel review" of edit menu in the "Test-Plan1" row
|
||||
And I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: User can manages his own plan
|
||||
Given I log in as "user2"
|
||||
And I follow "Profile" in the user menu
|
||||
Given I am on the "user2" "user > profile" page logged in as user2
|
||||
When I follow "Learning plans"
|
||||
Then I should see "List of learning plans"
|
||||
And I should see "Test-Plan2"
|
||||
@@ -82,11 +73,11 @@ Feature: Manage plan workflow
|
||||
And I should see "Active"
|
||||
And I click on "Complete this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I wait until the page is ready
|
||||
And I should see "Complete"
|
||||
And I click on "Reopen this learning plan" of edit menu in the "Test-Plan2" row
|
||||
And I click on "Reopen this learning plan" "button" in the "Confirm" "dialogue"
|
||||
And I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can see learning plan with status waiting for review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -96,7 +87,6 @@ Feature: Manage plan workflow
|
||||
When I log in as "manager1"
|
||||
Then I should see "Test-Plan3"
|
||||
And I should not see "Test-Plan4"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can start review of learning plan with status waiting for review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -108,7 +98,6 @@ Feature: Manage plan workflow
|
||||
And I should see "Test-Plan3"
|
||||
When I follow "Start review"
|
||||
Then I should see "In review"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can reject a learning plan with status in review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -121,7 +110,6 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Finish review"
|
||||
Then I should see "Draft"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager can accept a learning plan with status in review
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -134,7 +122,6 @@ Feature: Manage plan workflow
|
||||
And I should see "In review"
|
||||
When I follow "Make active"
|
||||
Then I should see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager send back to draft an active learning plan
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -150,7 +137,6 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Draft"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager change an active learning plan to completed
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -169,7 +155,6 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Complete"
|
||||
And I should not see "Active"
|
||||
And I log out
|
||||
|
||||
Scenario: Manager reopen a complete learning plan
|
||||
Given the following "core_competency > plans" exist:
|
||||
@@ -187,4 +172,67 @@ Feature: Manage plan workflow
|
||||
And I follow "Learning plans"
|
||||
Then I should see "Active"
|
||||
And I should not see "Complete"
|
||||
And I log out
|
||||
|
||||
Scenario: Student learning plan derived from templates can be completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 1 user to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I click on "LPT1" "link" in the "LPT1" "table_row"
|
||||
When I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add a short wait to ensure the page has loaded before checking that "Complete" "text" exists.
|
||||
And I wait until the page is ready
|
||||
# Confirm that student's learning plan template is marked as Completed.
|
||||
Then "Complete" "text" should exist
|
||||
And "Reopen this learning plan" "link" should exist
|
||||
|
||||
Scenario: Learning plan template updates are not reflected on plans already completed
|
||||
Given the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp1 |
|
||||
And I log in as "admin"
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Select 2 users to assign to create learning plans for using template.
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
And I set the field "Select users" to "user1"
|
||||
And I press "Create learning plans"
|
||||
And I set the field "Select users" to "user2"
|
||||
And I press "Create learning plans"
|
||||
# Complete the learning plan for User 1.
|
||||
And I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
And I click on "Complete this learning plan" "link"
|
||||
And I click on "Complete this learning plan" "button" in the "Confirm" "dialogue"
|
||||
# Add another competency to the learning plan template.
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | Test-Comp2 |
|
||||
# Navigate back to the list of Learning plan templates in order to access User 1's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that only the first competency is reflected on User 1's learning plan since it's already completed.
|
||||
When I click on "LPT1" "link" in the "User 1" "table_row"
|
||||
Then "Test-Comp2" "link" should not exist
|
||||
And "Test-Comp2" "text" should not exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
# Navigate back to list of Learning plan templates in order to access User 2's learning plan.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that both competencies are reflected on User 2's learning plan since it's not yet completed.
|
||||
And I click on "LPT1" "link" in the "User 2" "table_row"
|
||||
And "Test-Comp2" "link" should exist
|
||||
And "Test-Comp2" "text" should exist
|
||||
And "Test-Comp1" "link" should exist
|
||||
And "Test-Comp1" "text" should exist
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@tool @tool_lp @core_cohort
|
||||
Feature: Cohorts can be synchronized with learning plans
|
||||
In order to create learning plans for cohort members
|
||||
As an admin
|
||||
I need to be able to synchronise cohorts with learning plans
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| user1 | User | One | user1@example.com |
|
||||
| user2 | User | Two | user2@example.com |
|
||||
And the following "cohorts" exist:
|
||||
| name | idnumber |
|
||||
| Cohort 1 | CH1 |
|
||||
And the following "cohort members" exist:
|
||||
| user | cohort |
|
||||
| user1 | CH1 |
|
||||
| user2 | CH1 |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber |
|
||||
| CF1 | CF1 |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | competencyframework | idnumber |
|
||||
| C1 | CF1 | C1 |
|
||||
And the following "core_competency > templates" exist:
|
||||
| shortname |
|
||||
| LPT1 |
|
||||
And the following "core_competency > template_competencies" exist:
|
||||
| template | competency |
|
||||
| LPT1 | C1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Cohorts can be synchronised with learning plans
|
||||
Given I log in as "admin"
|
||||
# Navigate to the list of learning plan templates in order to add cohorts.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
When I click on "Add cohorts to sync" of edit menu in the "LPT1" row
|
||||
And I set the field "Select cohorts to sync" to "Cohort 1"
|
||||
And I press "Add cohorts"
|
||||
And I wait until the page is ready
|
||||
# Confirm that 2 learning plans were created for members of the cohort.
|
||||
Then "2 learning plans were created" "text" should exist
|
||||
# Confirm current screen is still "Cohorts synced to this learning plan template screen"
|
||||
And "Cohorts synced to this learning plan template" "text" should exist
|
||||
# Confirm that the cohort is now added to the learning plan template.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Cohort ID |
|
||||
| Cohort 1 | CH1 |
|
||||
# Navigate back to the list of learning plan templates to view updated list.
|
||||
And I navigate to "Competencies > Learning plan templates" in site administration
|
||||
# Confirm that the added cohort and learning plans are now reflected on the list of Learning plan templates.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | Category | Cohorts | Learning plans |
|
||||
| LPT1 | System | 1 | 2 |
|
||||
And I click on ".template-userplans" "css_element" in the "LPT1" "table_row"
|
||||
# Confirm that learning plans were created for all cohort members.
|
||||
And the following should exist in the "generaltable" table:
|
||||
| Name | First name | Email address |
|
||||
| LPT1 | User One | user1@example.com |
|
||||
| LPT1 | User Two | user2@example.com |
|
||||
@@ -365,7 +365,7 @@ class core_auth_external extends external_api {
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public static function resend_confirmation_email($username, $password, $redirect = '') {
|
||||
global $PAGE;
|
||||
global $PAGE, $CFG;
|
||||
|
||||
$warnings = array();
|
||||
$params = self::validate_parameters(
|
||||
@@ -382,20 +382,27 @@ class core_auth_external extends external_api {
|
||||
$username = trim(core_text::strtolower($params['username']));
|
||||
$password = $params['password'];
|
||||
|
||||
$user = core_user::get_user_by_username($username);
|
||||
|
||||
if (!empty($user) && $user->confirmed) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
if (is_restored_user($username)) {
|
||||
if (!empty($CFG->protectusernames)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
throw new moodle_exception('restoredaccountresetpassword', 'webservice');
|
||||
}
|
||||
|
||||
$user = authenticate_user_login($username, $password);
|
||||
|
||||
if (empty($user)) {
|
||||
throw new moodle_exception('invalidlogin');
|
||||
}
|
||||
|
||||
if ($user->confirmed) {
|
||||
throw new moodle_exception('alreadyconfirmed');
|
||||
}
|
||||
|
||||
// Check if we should redirect the user once the user is confirmed.
|
||||
$confirmationurl = null;
|
||||
if (!empty($params['redirect'])) {
|
||||
|
||||
@@ -104,6 +104,7 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
* @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users.
|
||||
* @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists.
|
||||
* @throws coding_exception if the specified provisioning mode is invalid.
|
||||
* @throws moodle_exception if user authentication fails.
|
||||
*/
|
||||
public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode,
|
||||
array $legacyconsumersecrets = []): void {
|
||||
@@ -113,6 +114,19 @@ class auth_plugin_lti extends \auth_plugin_base {
|
||||
// Always sync the PII, regardless of whether we're already authenticated as this user or not.
|
||||
$user = $this->find_or_create_user_from_launch($launchdata, true);
|
||||
|
||||
if ($user->suspended) {
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $user->id,
|
||||
'other' => [
|
||||
'username' => $user->username,
|
||||
'reason' => $failurereason
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
throw new moodle_exception('invalidlogin', 'core');
|
||||
}
|
||||
|
||||
if (isloggedin()) {
|
||||
// If a different user is currently logged in, authenticate the linked user instead.
|
||||
global $USER;
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
require_once('../../config.php');
|
||||
|
||||
$issuerid = required_param('id', PARAM_INT);
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_URL));
|
||||
$wantsurl = new moodle_url(optional_param('wantsurl', '', PARAM_LOCALURL));
|
||||
|
||||
$PAGE->set_context(context_system::instance());
|
||||
$PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid]));
|
||||
|
||||
+9
@@ -180,6 +180,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -203,6 +204,7 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$this->assertTrue($result['success']);
|
||||
$this->assertEmpty($result['warnings']);
|
||||
|
||||
set_config('protectusernames', 0);
|
||||
$_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
@@ -233,6 +235,13 @@ final class external_test extends externallib_advanced_testcase {
|
||||
$result = \external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result);
|
||||
$this->assertTrue($result['success']);
|
||||
|
||||
// Keep protectusernames enabled so the call returns invalidlogin exception.
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/invalidlogin');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
// Now disable protectusernames and expect an exception.
|
||||
set_config('protectusernames', 0);
|
||||
$this->expectException('\moodle_exception');
|
||||
$this->expectExceptionMessage('error/alreadyconfirmed');
|
||||
core_auth_external::resend_confirmation_email($username, $password);
|
||||
|
||||
@@ -73,7 +73,15 @@ class auth_plugin_webservice extends auth_plugin_base {
|
||||
function user_login_webservice($username, $password) {
|
||||
global $CFG, $DB;
|
||||
// special web service login
|
||||
if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) {
|
||||
$user = $DB->get_record('user', [
|
||||
'username' => $username,
|
||||
'auth' => 'webservice',
|
||||
'deleted' => '0',
|
||||
'suspended' => '0',
|
||||
'mnethostid' => $CFG->mnet_localhost_id,
|
||||
]);
|
||||
|
||||
if ($user) {
|
||||
return validate_internal_user_password($user, $password);
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -82,6 +82,16 @@ if (!$badge->is_active()) {
|
||||
die();
|
||||
}
|
||||
|
||||
if (!empty($role)) {
|
||||
if (!user_has_role_assignment($USER->id, $role, $context->id) && !$isadmin) {
|
||||
// User does not have the role passed by the parameter.
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->notification(get_string('wrongrole', 'badges'));
|
||||
echo $OUTPUT->footer();
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
$returnurl = new moodle_url('recipients.php', array('id' => $badge->id));
|
||||
$returnlink = html_writer::link($returnurl, $strrecipients);
|
||||
$actionbar = new \core_badges\output\standard_action_bar($PAGE, $badge->type, false, false, $returnurl);
|
||||
|
||||
@@ -177,7 +177,7 @@ if (!empty($issuedbadge->recipient->id)) {
|
||||
} else {
|
||||
if (isset($userapi) && !empty($userapi->get_errors())) {
|
||||
// If the api used to import the badge to the backpack has errors, show them to inform the user.
|
||||
if (array_filter($userapi->get_errors(), fn($element) => str_contains($element, "DUPLICATE_BADGE"))) {
|
||||
if (array_filter($userapi->get_errors(), fn($element) => (strpos($element, "DUPLICATE_BADGE") !== false))) {
|
||||
// Duplicated badges are displayed as a warning.
|
||||
$message = get_string('existsinbackpack', 'badges');
|
||||
$messagetype = \core\output\notification::NOTIFY_WARNING;
|
||||
|
||||
@@ -911,10 +911,10 @@ class core_calendar_external extends external_api {
|
||||
$formoptions['eventtypes'] = $allowedeeventtypes;
|
||||
if ($courseid) {
|
||||
require_once($CFG->libdir . '/grouplib.php');
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
if (!empty($groupcoursedata->groups)) {
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
if (!empty($groupcoursedata)) {
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3608,9 +3608,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
|
||||
if (is_null($eventid)) {
|
||||
if (!empty($courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($courseid);
|
||||
$groupcoursedata = groups_get_all_groups($courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
@@ -3652,9 +3652,9 @@ function calendar_output_fragment_event_form($args) {
|
||||
$formoptions['event'] = $event;
|
||||
|
||||
if (!empty($event->courseid)) {
|
||||
$groupcoursedata = groups_get_course_data($event->courseid);
|
||||
$groupcoursedata = groups_get_all_groups($event->courseid);
|
||||
$formoptions['groups'] = [];
|
||||
foreach ($groupcoursedata->groups as $groupid => $groupdata) {
|
||||
foreach ($groupcoursedata as $groupid => $groupdata) {
|
||||
$formoptions['groups'][$groupid] = $groupdata->name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Set our timezone based on the timezone in the RFC's samples (US/Eastern).
|
||||
$tz = 'US/Eastern';
|
||||
// Set our timezone based on the timezone in the RFC's samples (America/New_York).
|
||||
$tz = 'America/New_York';
|
||||
$this->setTimezone($tz);
|
||||
$timezone = new \DateTimeZone($tz);
|
||||
// Create our event's DTSTART date based on RFC's samples (most commonly used in RFC is 1997-09-02 09:00:00 EDT).
|
||||
@@ -686,8 +686,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -843,8 +843,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change the start date for forever events to 9am of the 2nd day of the current month and year.
|
||||
$this->change_event_startdate(date('Ym02\T090000'));
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart), new \DateTimeZone('Australia/Perth'));
|
||||
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
$interval = new \DateInterval('P12M');
|
||||
@@ -1202,8 +1202,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_day_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdatetime = new \DateTime(date('Y-m-d H:i:s', $this->event->timestart));
|
||||
$interval = new \DateInterval('P2D');
|
||||
@@ -1272,7 +1272,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1306,7 +1306,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 01-01-1998, based on the example from the RFC.
|
||||
$this->change_event_startdate('19980101T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19980101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=DAILY;UNTIL=20000131T090000Z;BYMONTH=1';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1404,8 +1404,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_week_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the current date in US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change the start date for forever events to 9am of the current date in America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P2W');
|
||||
|
||||
@@ -1615,7 +1615,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1651,7 +1651,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970905T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1689,7 +1689,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970907T090000', 'America/New_York');
|
||||
$startdate = new \DateTime(date('Y-m-d', $this->event->timestart));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1732,7 +1732,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 05-09-1997, based on the example from the RFC.
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970922T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1774,7 +1774,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to 28 September of the current year, based on the example from the RFC.
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate(date('Y0928\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYMONTHDAY=-3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1860,7 +1860,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_first_and_last_day_of_the_month_10_count() {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970930T090000', 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offsetinterval = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -1905,7 +1905,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_18_months_days_10_to_15_10_count() {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970910T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=18;COUNT=10;BYMONTHDAY=10,11,12,13,14,15';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -1950,9 +1950,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_tuesday_every_other_month_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in US/Eastern time.
|
||||
// Change the start date for forever events to 9am of the Tuesday on or before of the current date in America/New_York time.
|
||||
$nexttuesday = new \DateTime('next Tuesday');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$this->change_event_startdate($nexttuesday->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;INTERVAL=2;BYDAY=TU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2005,7 +2005,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_yearly_in_june_july_10_count() {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970610T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;COUNT=10;BYMONTH=6,7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2048,7 +2048,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_other_year_in_june_july_10_count() {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970310T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=2;COUNT=10;BYMONTH=1,2,3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2095,7 +2095,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_3_years_1st_100th_200th_days_10_count() {
|
||||
global $DB;
|
||||
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate('19970101T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=YEARLY;INTERVAL=3;COUNT=10;BYYEARDAY=1,100,200';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2145,7 +2145,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the 20th Monday of the current year.
|
||||
$twentiethmonday = new \DateTime(date('Y-01-01'));
|
||||
$twentiethmonday->modify('+20 Monday');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethmonday->format('Ymd\T000000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2191,7 +2191,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
// Change our event's date to the start of the 20th week of the current year.
|
||||
$twentiethweek = new \DateTime(date('Y-01-01'));
|
||||
$twentiethweek->setISODate($twentiethweek->format('Y'), 20);
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($twentiethweek->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = clone($startdatetime);
|
||||
$startdate->modify($startdate->format('Y-m-d'));
|
||||
@@ -2238,9 +2238,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_in_march_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of March of the current year at 9am America/New_York time.
|
||||
$firstthursdayofmarch = new \DateTime('first Thursday of March');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofmarch->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$interval = new \DateInterval('P1Y');
|
||||
|
||||
@@ -2297,9 +2297,9 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_every_thursday_june_july_august_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$firstthursdayofjune = new \DateTime('first Thursday of June');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstthursdayofjune->format('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
|
||||
@@ -2354,8 +2354,8 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_friday_the_thirteenth_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am US/Eastern time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'US/Eastern');
|
||||
// Change our event's date to the first Thursday of June in the current year at 9am America/New_York time.
|
||||
$this->change_event_startdate(date('Ymd\T090000'), 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2391,10 +2391,10 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_first_saturday_following_first_sunday_forever() {
|
||||
global $DB;
|
||||
|
||||
// Change our event's date to the next Saturday after the first Sunday of the the current month at 9am US/Eastern time.
|
||||
// Change our event's date to the next Saturday after the first Sunday of the current month at 9am America/New_York time.
|
||||
$firstsaturdayafterfirstsunday = new \DateTime('first Sunday of this month');
|
||||
$firstsaturdayafterfirstsunday->modify('next Saturday');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($firstsaturdayafterfirstsunday->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2452,7 +2452,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
$electiondate->modify('+1 Tuesday');
|
||||
|
||||
// Use the most recent election date as the starting date of our recurring events.
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'US/Eastern');
|
||||
$startdatetime = $this->change_event_startdate($electiondate->format('Ymd\T090000'), 'America/New_York');
|
||||
$startdate = new \DateTime($startdatetime->format('Y-m-d'));
|
||||
$offset = $startdatetime->diff($startdate, true);
|
||||
|
||||
@@ -2495,7 +2495,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_monthly_bysetpos_3_count() {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970904T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970904T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2532,7 +2532,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_second_to_the_last_weekday_of_the_month() {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970929T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970929T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-2;COUNT=7';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2757,7 +2757,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_mo() {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=MO';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2790,7 +2790,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
public function test_weekly_byday_with_wkst_su() {
|
||||
global $DB;
|
||||
|
||||
$this->change_event_startdate('19970805T090000', 'US/Eastern');
|
||||
$this->change_event_startdate('19970805T090000', 'America/New_York');
|
||||
|
||||
$rrule = 'FREQ=WEEKLY;INTERVAL=2;COUNT=4;BYDAY=TU,SU;WKST=SU';
|
||||
$mang = new rrule_manager($rrule);
|
||||
@@ -2870,7 +2870,7 @@ final class rrule_manager_test extends \advanced_testcase {
|
||||
* Change the event's timestart (DTSTART) based on the test's needs.
|
||||
*
|
||||
* @param string $datestr The date string. In 'Ymd\This' format. e.g. 19990902T090000.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'US/Eastern'.
|
||||
* @param null|string $timezonestr A valid timezone string. e.g. 'America/New_York'.
|
||||
* If not provided, the default timezone will be used.
|
||||
* @return bool|DateTime
|
||||
*/
|
||||
|
||||
@@ -380,6 +380,12 @@ class core_cohort_external extends external_api {
|
||||
$results = array_merge($results, cohort_get_available_cohorts($context, COHORT_ALL, $limitfrom, $limitnum, $query));
|
||||
}
|
||||
} else if ($includes == 'all') {
|
||||
$contextsystem = context_system::instance();
|
||||
if (!$context instanceof context_system &&
|
||||
!has_any_capability(['moodle/cohort:view', 'moodle/cohort:manage'], $contextsystem)) {
|
||||
|
||||
throw new required_capability_exception($contextsystem, 'moodle/cohort:view', 'nopermissions', '');
|
||||
}
|
||||
$results = cohort_get_all_cohorts($limitfrom, $limitnum, $query);
|
||||
$results = $results['cohorts'];
|
||||
} else {
|
||||
|
||||
@@ -555,6 +555,7 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
role_assign($userrole, $catuser->id, $catcontext->id);
|
||||
|
||||
// Enrol user in the course.
|
||||
$this->getDataGenerator()->enrol_user($creator->id, $course->id);
|
||||
$this->getDataGenerator()->enrol_user($courseuser->id, $course->id, 'courserole');
|
||||
|
||||
$syscontext = array('contextid' => \context_system::instance()->id);
|
||||
@@ -612,18 +613,27 @@ final class externallib_test extends externallib_advanced_testcase {
|
||||
$this->assertEquals(3, count($result['cohorts']));
|
||||
|
||||
// A user in the course context with the system cohort:view capability. Check that all the system cohorts are returned.
|
||||
$this->setUser($courseuser);
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->assertEquals(1, count($result['cohorts']));
|
||||
$this->assertEquals('Cohortsearch 1', $result['cohorts'][$cohort1->id]->name);
|
||||
$this->assertEquals(3, count($result['cohorts']));
|
||||
|
||||
// A user in the course context without the ability to view system cohorts.
|
||||
$this->setUser($courseuser);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $coursecontext, 'all');
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\required_capability_exception::class, $e);
|
||||
$this->assertStringContainsString('(View site-wide cohorts)', $e->getMessage());
|
||||
}
|
||||
|
||||
// Detect invalid parameter $includes.
|
||||
$this->setUser($creator);
|
||||
try {
|
||||
$result = core_cohort_external::search_cohorts("Cohortsearch", $syscontext, 'invalid');
|
||||
$this->fail('Invalid parameter includes');
|
||||
} catch (\coding_exception $e) {
|
||||
// All good.
|
||||
$this->fail('Exception expected');
|
||||
} catch (\Throwable $e) {
|
||||
$this->assertInstanceOf(\coding_exception::class, $e);
|
||||
$this->assertStringContainsString('Invalid parameter value for \'includes\'', $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@core @core_contentbank @core_h5p @contentbank_h5p
|
||||
Feature: Content bank contents are retained when course is restored
|
||||
In order to restore content bank contents
|
||||
As a manager
|
||||
I need to be able to restore course containing the content bank
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| admin | C1 | editingteacher |
|
||||
And the following "contentbank content" exist:
|
||||
| contextlevel | reference | contenttype | user | contentname | filepath |
|
||||
| Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And the following config values are set as admin:
|
||||
| unaddableblocks | | theme_boost|
|
||||
# TODO MDL-57120 site "Content bank" link not accessible without navigation block.
|
||||
And I add the "Navigation" block if not present
|
||||
|
||||
@javascript
|
||||
Scenario: Deleted courses with content banks can be restored
|
||||
Given I navigate to "Courses > Manage courses and categories" in site administration
|
||||
And I click on "delete" action for "Course 1" in management course listing
|
||||
And I click on "Delete" "button" in the "Confirm" "dialogue"
|
||||
And I press "Continue"
|
||||
And I navigate to "Recycle bin" in current page administration
|
||||
And I click on "Restore" "link" in the "Course 1" "table_row"
|
||||
And I am on "Course 1" course homepage
|
||||
And I expand "Site pages" node
|
||||
When I click on "Content bank" "link"
|
||||
And I click on "filltheblanks.h5p" "link"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?"
|
||||
@@ -259,6 +259,20 @@ class tool_provider extends ToolProvider {
|
||||
// Get the updated user record.
|
||||
$user = $DB->get_record('user', ['id' => $user->id]);
|
||||
} else {
|
||||
if ($dbuser->suspended) {
|
||||
require_once($CFG->libdir . '/authlib.php');
|
||||
$failurereason = AUTH_LOGIN_SUSPENDED;
|
||||
$event = \core\event\user_login_failed::create([
|
||||
'userid' => $dbuser->id,
|
||||
'other' => [
|
||||
'username' => $dbuser->username,
|
||||
'reason' => $failurereason
|
||||
]
|
||||
]);
|
||||
$event->trigger();
|
||||
throw new moodle_exception('invalidlogin', 'core');
|
||||
}
|
||||
|
||||
if (helper::user_match($user, $dbuser)) {
|
||||
$user = $dbuser;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Marking guide criterion can be edited and replaced
|
||||
In order to edit and replace a marking guide criterion
|
||||
As a teacher
|
||||
I need to have an existing marking guide criterion
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
|
||||
| assign | C1 | Assign 1 | guide | 1 |
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Assign 1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Assign 1 marking guide |
|
||||
| Description | Marking guide description |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
|
||||
And I define the following frequently used comments:
|
||||
| Comment 1 |
|
||||
| Comment 2 |
|
||||
| Comment 3 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
And the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Assign 1 | student1 | I'm the student first submission |
|
||||
And I am on the "Assign 1" "assign activity" page
|
||||
And I go to "Student 1" "Assign 1" activity advanced grading page
|
||||
And I grade by filling the marking guide with:
|
||||
| Grade Criteria 1 | 50 | Comment 1 |
|
||||
| Grade Criteria 2 | 20 | Comment 2 |
|
||||
And I press "Save changes"
|
||||
|
||||
@javascript
|
||||
Scenario: Marking guide frequently used comments can be updated and reordered
|
||||
Given I go to "Assign 1" advanced grading definition page
|
||||
And I click on "Move up" "button" in the "Comment 3" "table_row"
|
||||
And I click on "Move down" "button" in the "Comment 1" "table_row"
|
||||
When I press "Save"
|
||||
# Confirm that the order of the frequently used comments are updated.
|
||||
Then "Comment 3" "text" should appear before "Comment 1" "text"
|
||||
And "Comment 2" "text" should appear after "Comment 1" "text"
|
||||
And I am on the "Assign 1" "assign activity" page
|
||||
And I go to "Student 1" "Assign 1" activity advanced grading page
|
||||
# Confirm that there are no changes to the existing marking records.
|
||||
And I should see "Comment 1" in the "Grade Criteria 1" "table_row"
|
||||
And I should see "Comment 2" in the "Grade Criteria 2" "table_row"
|
||||
|
||||
@javascript
|
||||
Scenario: Marking guide criterion can be modified
|
||||
Given I go to "Assign 1" advanced grading definition page
|
||||
And I click on "Delete criterion" "button" in the "Grade Criteria 1" "table_row"
|
||||
And I press "Yes"
|
||||
And I wait "1" seconds
|
||||
And I should not see "Grade Criteria 1"
|
||||
And I press "Add criterion"
|
||||
# Used xpath_element selectors in order to differentiate multiple criterion fields.
|
||||
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionname']" "xpath_element"
|
||||
# Set the new criterion name.
|
||||
And I set the field with xpath "//input[@id='guide-criteria-NEWID1-shortname']" to "Grade Criteria 3"
|
||||
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondesc']//span[@class='textvalue']" "xpath_element"
|
||||
# Set the new criterion description for students.
|
||||
And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][description]']" to "Grade 3 description for students"
|
||||
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criteriondescmarkers']//span[@class='textvalue']" "xpath_element"
|
||||
# Set the new criterion description for markers.
|
||||
And I set the field with xpath "//textarea[@id='guide[criteria][NEWID1][descriptionmarkers]']" to "Grade 3 description for markers"
|
||||
And I click on "//table[@id='guide-criteria']//tr[@class='criterion odd last']//div[@class='criterionmaxscore']//span[@class='textvalue']" "xpath_element"
|
||||
# Set the new criterion max score.
|
||||
And I set the field with xpath "//input[@id='guide[criteria][NEWID1][maxscore]']" to "70"
|
||||
When I press "Save"
|
||||
Then "You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded." "text" should exist
|
||||
And I press "Continue"
|
||||
And I am on the "Assign 1" "assign activity" page logged in as student1
|
||||
# Confirm that the remarks for each grading criterion are not displayed.
|
||||
And "Comment 1" "text" should not exist
|
||||
And "Comment 2" "text" should not exist
|
||||
# Confirm that the submission is not regraded.
|
||||
And I should see "70.00 / 100.00"
|
||||
# Regrade the submission.
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I go to "Student 1" "Assign 1" activity advanced grading page
|
||||
And I grade by filling the marking guide with:
|
||||
| Grade Criteria 2 | 15 | Comment 3 |
|
||||
| Grade Criteria 3 | 50 | Comment 2 |
|
||||
And I press "Save changes"
|
||||
And I am on the "Assign 1" "assign activity" page logged in as student1
|
||||
# Confirm that submission is re-marked and regraded.
|
||||
And I should see "65.00 / 100.00"
|
||||
And I should see the marking guide information displayed as:
|
||||
| criteria | description | remark | maxscore | criteriascore |
|
||||
| Grade Criteria 2 | Grade 2 description for students | Comment 3 | 30 | 15 / 30 |
|
||||
| Grade Criteria 3 | Grade 3 description for students | Comment 2 | 70 | 50 / 70 |
|
||||
@@ -0,0 +1,44 @@
|
||||
@gradingform @gradingform_guide
|
||||
Feature: Verify listings and grading submissions
|
||||
In order to verify grade listing
|
||||
As a teacher
|
||||
I need to be able to see grades in the correct column
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student | Student | One | one@example.com |
|
||||
| teacher | Teacher | One | t1@example.com |
|
||||
And the following "courses" exist:
|
||||
| shortname | fullname |
|
||||
| C1 | Course 1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student | C1 | student |
|
||||
| teacher | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | advancedgradingmethod_submissions | assignsubmission_onlinetext_enabled |
|
||||
| assign | Assign1 | C1 | guide | 1 |
|
||||
And I am on the "Course 1" course page logged in as teacher
|
||||
And I go to "Assign1" advanced grading definition page
|
||||
And I set the following fields to these values:
|
||||
| Name | Assign1 marking guide |
|
||||
| Description | Marking guide description |
|
||||
And I define the following marking guide:
|
||||
| Criterion name | Description for students | Description for markers | Maximum score |
|
||||
| Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 100 |
|
||||
And I press "Save marking guide and make it ready"
|
||||
And the following "mod_assign > submissions" exist:
|
||||
| assign | user | onlinetext |
|
||||
| Assign1 | student | This is a submission for assignment |
|
||||
|
||||
@javascript
|
||||
Scenario: Mark and view all grades in submissions table
|
||||
Given I am on the "Assign1" "assign activity" page
|
||||
And I click on "View all submission" "link"
|
||||
And I click on "Grade" "link" in the "Student One" "table_row"
|
||||
And I grade by filling the marking guide with:
|
||||
| Criteria 1 | 50 | Excellent work! |
|
||||
And I press "Save changes"
|
||||
When I follow "View all submissions"
|
||||
Then "Student One" row "Grade" column of "generaltable" table should contain "50"
|
||||
@@ -31,3 +31,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['clianswerno'] = '';
|
||||
$string['cliansweryes'] = '';
|
||||
$string['cliincorrectvalueretry'] = 'Skeivt virði, vinarliga royn aftur';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link https://moodledev.io/general/projects/api/amos}) using the
|
||||
* list of strings defined in install/stringnames.txt file.
|
||||
*
|
||||
* @package installer
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link https://moodledev.io/general/projects/api/amos}) using the
|
||||
* list of strings defined in install/stringnames.txt file.
|
||||
*
|
||||
* @package installer
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cliincorrectvalueerror'] = 'Mearachd, luach ceàrr "{$a->value}" airson "{$a->option}"';
|
||||
$string['cliincorrectvalueretry'] = 'Luach ceàrr, feuch a-rithist';
|
||||
$string['cliunknowoption'] = 'Roghainnean nach deach an aithneachadh:
|
||||
{$a}
|
||||
Cleachd— an roghainn airson cuideachadh';
|
||||
$string['environmentrequireversion'] = 'tha tionndadh {$a->needed} deatamach agus tha thusa a’ ruith {$a->current}';
|
||||
$string['upgradekeyset'] = 'Iuchair àrdachaidh (fàg bàn gus nach tèid a shuidheachadh)';
|
||||
@@ -32,3 +32,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['language'] = 'Cànan';
|
||||
$string['moodlelogo'] = 'Logo Mhoodle';
|
||||
$string['previous'] = 'Roimhe';
|
||||
$string['reload'] = 'Ath-luchdaich';
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>לא ניתן ליצור את מסד הנתונים.</p>
|
||||
<p>מסד הנתונים שצוין אינו קיים, ולמשתמש שצוין אין הרשאה ליצור את מסד הנתונים.</p>
|
||||
<p>מנהל האתר צריך לבדוק את הגדרות מסד הנתונים.</p>';
|
||||
$string['cannotcreatelangdir'] = 'לא ניתן ליצור סיפריית שפה.';
|
||||
$string['cannotcreatetempdir'] = 'לא ניתן ליצור סיפרייה זמנית.';
|
||||
$string['cannotdownloadcomponents'] = 'לא ניתן להוריד רכיבים.';
|
||||
@@ -38,6 +41,7 @@ $string['cannotsavemd5file'] = 'לא ניתן לשמור קובץ md5';
|
||||
$string['cannotsavezipfile'] = 'לא ניתן לשמור קובץ ZIP';
|
||||
$string['cannotunzipfile'] = 'לא ניתן לפתוח את קובץ ה-ZIP.';
|
||||
$string['componentisuptodate'] = 'הרכיב מעודכן.';
|
||||
$string['dmlexceptiononinstall'] = '<p>אירעה שגיאת מסד נתונים [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'הקובץ אשר ירד נמצא שגוי';
|
||||
$string['invalidmd5'] = 'md5 לא חוקי';
|
||||
$string['missingrequiredfield'] = 'חסר שדה נדרש כלשהו';
|
||||
|
||||
@@ -45,7 +45,7 @@ $string['dmlexceptiononinstall'] = '<p>データベースエラーが発生し
|
||||
$string['downloadedfilecheckfailed'] = 'ダウンロードファイルのチェックに失敗しました。';
|
||||
$string['invalidmd5'] = 'チェック変数が正しくありません - 再度お試しください。';
|
||||
$string['missingrequiredfield'] = 'いくつかの必須入力フィールドが入力されていません。';
|
||||
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定を確認してください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
|
||||
$string['remotedownloaderror'] = '<p>あなたのサーバへのコンポーネントのダウンロードに失敗しました。プロキシ設定をご確認ください。PHP cURL拡張モジュールの使用を強くお勧めします。</p>
|
||||
<p><a href="{$a->url}">{$a->url}</a>ファイルを手動でダウンロードした後、あなたのサーバの「{$a->dest}」にコピーおよび展開してください。</p>';
|
||||
$string['wrongdestpath'] = '宛先パスが正しくありません。';
|
||||
$string['wrongsourcebase'] = 'ソースURLベースが正しくありません。';
|
||||
|
||||
@@ -32,10 +32,13 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['clianswerno'] = 'n';
|
||||
$string['cliansweryes'] = 'y';
|
||||
$string['cliincorrectvalueerror'] = 'Xato, “{$a->option}” uchun noto‘g‘ri “{$a->value}” qiymati';
|
||||
$string['cliincorrectvalueretry'] = 'Noto\'g\'ri qiymat, iltimos qayta urinib ko\'ring';
|
||||
$string['cliincorrectvalueretry'] = 'Noto‘g‘ri qiymat, iltimos qayta urinib ko‘ring';
|
||||
$string['clitypevalue'] = 'qiymatni kiritish';
|
||||
$string['clitypevaluedefault'] = 'qiymatni kiriting, ({$a}) odatiy qiymatni ishlatish uchun Enter tumgasini bosing';
|
||||
$string['cliunknowoption'] = 'Noma\'lum parametrlar:
|
||||
{$a}
|
||||
Iltimos, -- help parametridan foydalaning';
|
||||
$string['cliyesnoprompt'] = 'y tugmasini bosing (ha degani) yoki n tugmasini bosing (yo‘q degani)';
|
||||
$string['environmentrequireinstall'] = 'o‘rnatilishi va yoqilishi shart.';
|
||||
$string['environmentrequireversion'] = '{$a->needed} versiyasi talab etiladi, sizda esa hozirda {$a->current} ishlatilmoqda';
|
||||
$string['upgradekeyset'] = 'Yangilash kaliti (o‘rnatmaslik uchun bo‘sh qoldiring)';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// This file is part of Moodle - https://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Automatically generated strings for Moodle installer
|
||||
*
|
||||
* Do not edit this file manually! It contains just a subset of strings
|
||||
* needed during the very first steps of installation. This file was
|
||||
* generated automatically by export-installer.php (which is part of AMOS
|
||||
* {@link https://moodledev.io/general/projects/api/amos}) using the
|
||||
* list of strings defined in install/stringnames.txt file.
|
||||
*
|
||||
* @package installer
|
||||
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['cannotcreatedboninstall'] = '<p>Ma\'lumotlar bazasini yaratib bo\'lmadi.</p>
|
||||
<p>Ko\'rsatilgan ma\'lumotlar bazasi mavjud emas va berilgan foydalanuvchida uni yaratish huquqi yo\'q.</p>
|
||||
<p>Sayt administratori ma\'lumotlar bazasi konfiguratsiyasini tekshirishi kerak.</p>';
|
||||
$string['cannotcreatelangdir'] = 'Til katalogini yaratib bo\'lmadi';
|
||||
$string['cannotcreatetempdir'] = 'Vaqtinchalik jildni yaratib bo‘lmadi';
|
||||
$string['cannotdownloadcomponents'] = 'Komponentlarni yuklab olib bo\'lmadi';
|
||||
$string['cannotdownloadzipfile'] = 'ZIP faylini yuklab olib bo\'lmadi';
|
||||
$string['cannotfindcomponent'] = 'Komponentni topib bo\'lmadi';
|
||||
$string['cannotsavemd5file'] = 'md5 faylini saqlab bo‘lmadi';
|
||||
$string['cannotsavezipfile'] = 'ZIP faylini saqlab bo‘lmadi';
|
||||
$string['cannotunzipfile'] = 'Faylni arxivdan chiqarib bo‘lmadi';
|
||||
$string['componentisuptodate'] = 'Komponent yangilangan holda';
|
||||
$string['dmlexceptiononinstall'] = '<p>Ma’lumotlar bazasida xatolik yuz berdi [{$a->errorcode}].<br />{$a->debuginfo}</p>';
|
||||
$string['downloadedfilecheckfailed'] = 'Yuklab olingan faylni tekshirish muvaffaqiyatsiz tugadi';
|
||||
@@ -29,9 +29,17 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['admindirname'] = 'Administrator katalogi nomi';
|
||||
$string['availablelangs'] = 'Mavjud(foydalanish mumkin bo\'lgan)til paketlari';
|
||||
$string['dataroot'] = 'Ma\'lumotlar katalogi';
|
||||
$string['admindirname'] = 'Admin katalogi';
|
||||
$string['availablelangs'] = 'Mavjud til paketlari';
|
||||
$string['dataroot'] = 'Ma’lumotlar katalogi';
|
||||
$string['dbprefix'] = 'Jadvallar prefiksi';
|
||||
$string['dirroot'] = 'Moodle kataloglari';
|
||||
$string['dirroot'] = 'Moodle katalogi';
|
||||
$string['installation'] = 'O\'rnatish';
|
||||
$string['welcomep10'] = '{$a->installername} ({$a->installerversion})';
|
||||
$string['welcomep20'] = 'Siz bu sahifani ko‘rayotganingizning sababi, siz kompyuteringizda <strong>{$a->packname} {$a->packversion}</strong> paketini muvaffaqiyatli o‘rnatdingiz va ishga tushirdingiz. Tabriklaymiz!';
|
||||
$string['welcomep30'] = '<strong>{$a->installername}</strong> ning ushbu relizi <strong>Moodle</strong> ishlaydigan muhitni yaratish uchun ilovalarni o‘z ichiga oladi, ya’ni:';
|
||||
$string['welcomep40'] = 'Paket shuningdek <strong>Moodle {$a->moodlerelease} ({$a->moodleversion})</strong> ni o‘z ichiga oladi.';
|
||||
$string['welcomep50'] = 'Ushbu paketdagi barcha ilovalardan foydalanish ularning tegishli litsenziyalari bilan tartibga solinadi. To‘liq <strong>{$a->installername}</strong> paketi <a href="https://www.opensource.org/docs/definition_plain.html">ochiq kodli</a> hisoblanadi va <a href="https://www.gnu.org/copyleft/gpl.html">GPL</a> litsenziyasi ostida tarqatiladi.';
|
||||
$string['welcomep60'] = 'Keyingi sahifalar kompyuteringizda <strong>Moodle</strong>ni sozlash va o‘rnatish bo‘yicha oson bajariladigan qadamlar orqali sizni yetaklaydi. Siz sukut bo‘yicha sozlamalarni qabul qilishingiz yoki ixtiyoriy ravishda ularni o‘z ehtiyojlaringizga mos ravishda o‘zgartirishingiz mumkin.';
|
||||
$string['welcomep70'] = '<strong>Moodle</strong>ni o‘rnatishni davom ettirish uchun quyidagi "Keyingi" tugmasini bosing.';
|
||||
$string['wwwroot'] = 'Veb-manzil';
|
||||
|
||||
+10
-5
@@ -64,7 +64,7 @@ function iplookup_find_location($ip) {
|
||||
|
||||
return $info;
|
||||
|
||||
} else {
|
||||
} else if (!empty($CFG->geopluginapikey)) {
|
||||
require_once($CFG->libdir.'/filelib.php');
|
||||
|
||||
if (strpos($ip, ':') !== false) {
|
||||
@@ -73,11 +73,13 @@ function iplookup_find_location($ip) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$ipdata = download_file_content('http://www.geoplugin.net/json.gp?ip='.$ip);
|
||||
if ($ipdata) {
|
||||
$ipdata = preg_replace('/^geoPlugin\((.*)\)\s*$/s', '$1', $ipdata);
|
||||
$ipdata = json_decode($ipdata, true);
|
||||
$requesturl = new moodle_url('https://api.geoplugin.com', ['ip' => $ip, 'auth' => $CFG->geopluginapikey]);
|
||||
$response = download_file_content($requesturl->out(false), null, null, true);
|
||||
if ($response->response_code != 200) {
|
||||
$info['error'] = get_string('cannotgeoplugin', 'error');
|
||||
return $info;
|
||||
}
|
||||
$ipdata = json_decode($response->results, true);
|
||||
if (!is_array($ipdata)) {
|
||||
$info['error'] = get_string('cannotgeoplugin', 'error');
|
||||
return $info;
|
||||
@@ -103,4 +105,7 @@ function iplookup_find_location($ip) {
|
||||
return $info;
|
||||
}
|
||||
|
||||
$info['error'] = get_string('iplookupfailed', 'error', $ip);
|
||||
return $info;
|
||||
|
||||
}
|
||||
|
||||
@@ -50,8 +50,12 @@ final class geoip_test extends \advanced_testcase {
|
||||
* @param string $ip The IP to test
|
||||
*/
|
||||
public function test_ip($ip): void {
|
||||
global $CFG;
|
||||
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
|
||||
$this->markTestSkipped('External geo tests are disabled.');
|
||||
}
|
||||
$this->resetAfterTest();
|
||||
|
||||
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
|
||||
$this->setup_geoip2file();
|
||||
|
||||
// Note: The results we get from the iplookup tests are beyond our control.
|
||||
|
||||
@@ -35,12 +35,20 @@ final class geoplugin_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
|
||||
* In order to execute this test:
|
||||
* - PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
|
||||
* - GeoPlugin API key should be defined in config.php as TEST_GEOIP_APIKEY
|
||||
*/
|
||||
public function setUp(): void {
|
||||
global $CFG;
|
||||
if (!PHPUNIT_LONGTEST) {
|
||||
$this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
|
||||
}
|
||||
|
||||
if (!defined('TEST_GEOIP_APIKEY') || empty(TEST_GEOIP_APIKEY)) {
|
||||
$this->markTestSkipped('External geo tests are disabled.');
|
||||
}
|
||||
$CFG->geopluginapikey = TEST_GEOIP_APIKEY;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -667,6 +667,8 @@ $string['fullnamedisplayprivate'] = 'Full name format - private';
|
||||
$string['gdrequired'] = 'The GD extension is now required by Moodle for image conversion.';
|
||||
$string['generalsettings'] = 'General settings';
|
||||
$string['geoipfile'] = 'GeoLite2 City MaxMind DB';
|
||||
$string['geopluginapikey'] = 'GeoPlugin API key';
|
||||
$string['geopluginapikey_desc'] = 'The API key used to access the GeoPlugin service. Get your own key at <a href="https://www.geoplugin.com/" target="_blank">GeoPlugin page</a>.';
|
||||
$string['getremoteaddrconf'] = 'Logged IP address source';
|
||||
$string['globalsearch'] = 'Global search';
|
||||
$string['globalsearchmanage'] = 'Manage global search';
|
||||
|
||||
@@ -579,6 +579,7 @@ $string['visible'] = 'Visible';
|
||||
$string['version'] = 'Version';
|
||||
$string['version_help'] = 'The version field may be used to keep track of the badge\'s development. If specified, the version is displayed on the badge page.';
|
||||
$string['warnexpired'] = ' (This badge has expired!)';
|
||||
$string['wrongrole'] = 'The role you want to use is not assigned to you.';
|
||||
$string['year'] = 'Year(s)';
|
||||
$string['includeauthdetails'] = "Include authentication details with the backpack";
|
||||
|
||||
|
||||
@@ -1053,19 +1053,37 @@ EOF;
|
||||
/**
|
||||
* Helper function to execute api in a given context.
|
||||
*
|
||||
* @param string $contextapi context in which api is defined.
|
||||
* @param array $params list of params to pass.
|
||||
* Note: The contextapi does not support a callback.
|
||||
*
|
||||
* @param string|array $contextapi context in which api is defined.
|
||||
* @param array|mixed $params list of params to pass or a single parameter
|
||||
* @throws Exception
|
||||
* @throws DriverException
|
||||
*/
|
||||
protected function execute($contextapi, $params = array()) {
|
||||
protected function execute(
|
||||
$contextapi,
|
||||
$params = []
|
||||
): void {
|
||||
if (!is_array($params)) {
|
||||
$params = array($params);
|
||||
$params = [$params];
|
||||
}
|
||||
|
||||
if (is_string($contextapi)) {
|
||||
$contextapi = explode('::', $contextapi);
|
||||
}
|
||||
|
||||
if (count($contextapi) !== 2) {
|
||||
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
|
||||
}
|
||||
|
||||
// Get required context and execute the api.
|
||||
$contextapi = explode("::", $contextapi);
|
||||
$context = behat_context_helper::get($contextapi[0]);
|
||||
call_user_func_array(array($context, $contextapi[1]), $params);
|
||||
[$classname, $method] = $contextapi;
|
||||
if (!is_string($classname) || !is_string($method)) {
|
||||
throw new DriverException('Invalid contextapi format, expected "context::api" or ["context", "api"]');
|
||||
}
|
||||
|
||||
$context = behat_context_helper::get($classname);
|
||||
call_user_func_array([$context, $method], $params);
|
||||
|
||||
// NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results.
|
||||
// Don't make them optional for performance reasons.
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Date form field class.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2013 David Monllaó
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/behat_form_group.php');
|
||||
@@ -41,17 +32,14 @@ use Behat\Mink\Exception\ExpectationException;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_form_date extends behat_form_group {
|
||||
|
||||
/**
|
||||
* Sets the value to a date field.
|
||||
*
|
||||
* @param string $value The value to be assigned to the date selector field. The string value must be either
|
||||
* parsable into a UNIX timestamp or equal to 'disabled' (if disabling the date selector).
|
||||
* @return void
|
||||
* @throws ExpectationException If the value is invalid.
|
||||
*/
|
||||
public function set_value($value) {
|
||||
|
||||
if ($value === 'disabled') {
|
||||
// Disable the given date selector field.
|
||||
$this->set_child_field_value('enabled', false);
|
||||
@@ -77,6 +65,29 @@ class behat_form_date extends behat_form_group {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the field
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_value() {
|
||||
return make_timestamp(
|
||||
$this->get_child_field_value('year'),
|
||||
$this->get_child_field_value('month'),
|
||||
$this->get_child_field_value('day'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the provided value against the current field value
|
||||
*
|
||||
* @param mixed $expectedvalue
|
||||
* @return bool
|
||||
*/
|
||||
public function matches($expectedvalue) {
|
||||
return (int) $expectedvalue === $this->get_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date field identifiers and the values that should be assigned to them.
|
||||
*
|
||||
@@ -115,4 +126,14 @@ class behat_form_date extends behat_form_group {
|
||||
$childinstance->set_value($childvalue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value of a child element in the date form field
|
||||
*
|
||||
* @param string $childname
|
||||
* @return string
|
||||
*/
|
||||
protected function get_child_field_value(string $childname): string {
|
||||
return $this->field->find('css', "*[name$='[{$childname}]']")->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Date time form field class.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2013 David Monllaó
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/behat_form_date.php');
|
||||
@@ -39,6 +30,20 @@ require_once(__DIR__ . '/behat_form_date.php');
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_form_date_time extends behat_form_date {
|
||||
/**
|
||||
* Returns the current value of the field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_value() {
|
||||
return make_timestamp(
|
||||
$this->get_child_field_value('year'),
|
||||
$this->get_child_field_value('month'),
|
||||
$this->get_child_field_value('day'),
|
||||
$this->get_child_field_value('hour'),
|
||||
$this->get_child_field_value('minute'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the date field identifiers and the values that should be assigned to them.
|
||||
|
||||
@@ -167,4 +167,44 @@ class dataformat {
|
||||
|
||||
return get_file_storage()->create_file_from_pathname($filerecord, $filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape formula spreadsheet values.
|
||||
*
|
||||
* Check values being used in spreadsheets and make them safe for inclusion.
|
||||
* Following OWASP recommendations {@link https://owasp.org/www-community/attacks/CSV_Injection}.
|
||||
*
|
||||
* @param string|null $value Value to check.
|
||||
* @return string|null Return escaped formula if detected.
|
||||
*/
|
||||
public static function escape_spreadsheet_formula($value): ?string {
|
||||
// Only escape strings; leave numbers and other types unchanged.
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Moodle's null placeholder: exactly one dash.
|
||||
if ($value === '-') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Trim only for checking, not for modifying output.
|
||||
$trimmed = ltrim($value);
|
||||
|
||||
if ($trimmed === '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Characters that trigger formula parsing in Excel/Sheets.
|
||||
$formulacharacters = ['=', '+', '-', '@'];
|
||||
|
||||
// If trimmed version starts with formula character, escape it.
|
||||
if (in_array($trimmed[0], $formulacharacters, true)) {
|
||||
// Preserve original whitespace. Do not alter actual content.
|
||||
return "'" . $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -120,7 +120,11 @@ abstract class spout_base extends \core\dataformat\base {
|
||||
* @param int $rownum
|
||||
*/
|
||||
public function write_record($record, $rownum) {
|
||||
$row = \Box\Spout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray($this->format_record($record));
|
||||
$rowvalues = $this->format_record($record);
|
||||
foreach ($rowvalues as $key => $value) {
|
||||
$rowvalues[$key] = \core\dataformat::escape_spreadsheet_formula($value);
|
||||
}
|
||||
$row = \Box\Spout\Writer\Common\Creator\WriterEntityFactory::createRowFromArray($rowvalues);
|
||||
$this->writer->addRow($row);
|
||||
}
|
||||
|
||||
|
||||
@@ -293,11 +293,11 @@ class site_registration_form extends \moodleform {
|
||||
if ($data = parent::get_data()) {
|
||||
// Never return '*newemail' checkboxes, always return 'emailalertemail' and 'commnewsemail' even if not applicable.
|
||||
if (empty($data->emailalert) || empty($data->emailalertnewemail)) {
|
||||
$data->emailalertemail = null;
|
||||
$data->emailalertemail = '';
|
||||
}
|
||||
unset($data->emailalertnewemail);
|
||||
if (empty($data->commnews) || empty($data->commnewsnewemail)) {
|
||||
$data->commnewsemail = null;
|
||||
$data->commnewsemail = '';
|
||||
}
|
||||
unset($data->commnewsnewemail);
|
||||
// Always return 'contactable'.
|
||||
|
||||
@@ -39,6 +39,9 @@ class core_shutdown_manager {
|
||||
/** @var bool is this manager already registered? */
|
||||
protected static $registered = false;
|
||||
|
||||
/** @var array A list of pcntl handlers */
|
||||
protected static array $pcntlhandlers = [];
|
||||
|
||||
/**
|
||||
* Register self as main shutdown handler.
|
||||
*
|
||||
@@ -47,6 +50,7 @@ class core_shutdown_manager {
|
||||
public static function initialize() {
|
||||
if (self::$registered) {
|
||||
debugging('Shutdown manager is already initialised!');
|
||||
return;
|
||||
}
|
||||
self::$registered = true;
|
||||
register_shutdown_function(array('core_shutdown_manager', 'shutdown_handler'));
|
||||
@@ -58,8 +62,19 @@ class core_shutdown_manager {
|
||||
if (CLI_SCRIPT && extension_loaded('pcntl') && function_exists('pcntl_async_signals')) {
|
||||
// We capture and handle SIGINT (Ctrl+C) and SIGTERM (termination requested).
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, ['core_shutdown_manager', 'signal_handler']);
|
||||
pcntl_signal(SIGTERM, ['core_shutdown_manager', 'signal_handler']);
|
||||
|
||||
$signals = [SIGINT, SIGTERM];
|
||||
|
||||
foreach ($signals as $signal) {
|
||||
if (function_exists('pcntl_signal_get_handler')) {
|
||||
$handler = pcntl_signal_get_handler($signal);
|
||||
if (is_callable($handler)) {
|
||||
// We can restore the original handler later if needed.
|
||||
self::$pcntlhandlers[$signal] = $handler;
|
||||
}
|
||||
}
|
||||
pcntl_signal($signal, ['core_shutdown_manager', 'signal_handler']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +121,10 @@ class core_shutdown_manager {
|
||||
error_log('Exception ignored in signal function ' . get_callable_name($callback) . ': ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
if (array_key_exists($signo, self::$pcntlhandlers)) {
|
||||
$handler = self::$pcntlhandlers[$signo];
|
||||
$handler($signo);
|
||||
}
|
||||
|
||||
if ($shouldexit) {
|
||||
exit ($exitcode);
|
||||
|
||||
@@ -449,6 +449,9 @@ class csv_export_writer {
|
||||
}
|
||||
}
|
||||
$delimiter = csv_import_reader::get_delimiter($this->delimiter);
|
||||
foreach ($row as $key => $value) {
|
||||
$row[$key] = \core\dataformat::escape_spreadsheet_formula($value);
|
||||
}
|
||||
fputcsv($this->fp, $row, $delimiter, $this->csvenclosure);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,12 @@ $action = required_param('action', PARAM_ALPHA);
|
||||
|
||||
if ($action === 'filtertext') {
|
||||
$text = required_param('text', PARAM_RAW);
|
||||
$striptags = optional_param('striptags', false, PARAM_BOOL);
|
||||
|
||||
// Strip all HTML tags before filtering the text (avoiding XSS risk).
|
||||
if ($striptags) {
|
||||
$text = clean_param($text, PARAM_NOTAGS);
|
||||
}
|
||||
|
||||
$result = filter_manager::instance()->filter_text($text, $context);
|
||||
echo $OUTPUT->header();
|
||||
|
||||
+2
-1
@@ -475,7 +475,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
|
||||
sesskey: M.cfg.sesskey,
|
||||
contextid: this.get('contextid'),
|
||||
action: 'filtertext',
|
||||
text: equation
|
||||
text: equation,
|
||||
striptags: true,
|
||||
};
|
||||
|
||||
Y.io(url, {
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+2
-1
@@ -475,7 +475,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
|
||||
sesskey: M.cfg.sesskey,
|
||||
contextid: this.get('contextid'),
|
||||
action: 'filtertext',
|
||||
text: equation
|
||||
text: equation,
|
||||
striptags: true,
|
||||
};
|
||||
|
||||
Y.io(url, {
|
||||
|
||||
@@ -473,7 +473,8 @@ Y.namespace('M.atto_equation').Button = Y.Base.create('button', Y.M.editor_atto.
|
||||
sesskey: M.cfg.sesskey,
|
||||
contextid: this.get('contextid'),
|
||||
action: 'filtertext',
|
||||
text: equation
|
||||
text: equation,
|
||||
striptags: true,
|
||||
};
|
||||
|
||||
Y.io(url, {
|
||||
|
||||
@@ -5,6 +5,6 @@ define("tiny_equation/repository",["exports","core/ajax"],(function(_exports,_aj
|
||||
* @module tiny_equation/repository
|
||||
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=(contextId,content)=>{const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content}};return _ajax.default.call([request])[0]}}));
|
||||
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=function(contextId,content){let stripTags=arguments.length>2&&void 0!==arguments[2]&&arguments[2];const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content,striptags:stripTags}};return _ajax.default.call([request])[0]}}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @return {promise}\n */\nexport const filterEquation = (contextId, content) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","request","methodname","args","contextid","Ajax","call"],"mappings":";;;;;;;wKA+B8B,CAACA,UAAWC,iBAChCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWL,UACXC,QAASA,iBAIVK,cAAKC,KAAK,CAACL,UAAU"}
|
||||
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @param {Bool} stripTags Whether to strip HTML tags (optional)\n * @return {promise}\n */\nexport const filterEquation = (contextId, content, stripTags = false) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n striptags: stripTags,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","stripTags","request","methodname","args","contextid","striptags","Ajax","call"],"mappings":";;;;;;;wKAgC8B,SAACA,UAAWC,aAASC,wEACzCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWN,UACXC,QAASA,QACTM,UAAWL,mBAIZM,cAAKC,KAAK,CAACN,UAAU"}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -27,14 +27,16 @@ import Ajax from 'core/ajax';
|
||||
*
|
||||
* @param {Number} contextId The context id
|
||||
* @param {String} content Content to filter
|
||||
* @param {Bool} stripTags Whether to strip HTML tags (optional)
|
||||
* @return {promise}
|
||||
*/
|
||||
export const filterEquation = (contextId, content) => {
|
||||
export const filterEquation = (contextId, content, stripTags = false) => {
|
||||
const request = {
|
||||
methodname: 'tiny_equation_filter',
|
||||
args: {
|
||||
contextid: contextId,
|
||||
content: content,
|
||||
striptags: stripTags,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ const updatePreview = (contextId) => {
|
||||
equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
|
||||
|
||||
equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;
|
||||
TinyEquationRepository.filterEquation(contextId, equation).then((data) => {
|
||||
TinyEquationRepository.filterEquation(contextId, equation, true).then((data) => {
|
||||
preview.innerHTML = data.content;
|
||||
notifyFilter(preview);
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ class filter extends external_api {
|
||||
public static function execute_parameters(): external_function_parameters {
|
||||
return new external_function_parameters([
|
||||
'contextid' => new external_value(PARAM_INT, 'The context ID', VALUE_REQUIRED),
|
||||
'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED)
|
||||
'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED),
|
||||
'striptags' => new external_value(PARAM_BOOL, 'Whether to strip HTML tags', VALUE_DEFAULT, false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -58,18 +59,26 @@ class filter extends external_api {
|
||||
*
|
||||
* @param int $contextid Context ID.
|
||||
* @param string $content Equation content.
|
||||
* @param string $striptags Strip HTML tags.
|
||||
* @return array
|
||||
* @since Moodle 4.1
|
||||
*/
|
||||
public static function execute(int $contextid, string $content): array {
|
||||
public static function execute(int $contextid, string $content, bool $striptags = false): array {
|
||||
[
|
||||
'contextid' => $contextid,
|
||||
'content' => $content
|
||||
'content' => $content,
|
||||
'striptags' => $striptags,
|
||||
] = self::validate_parameters(self::execute_parameters(), [
|
||||
'contextid' => $contextid,
|
||||
'content' => $content
|
||||
'content' => $content,
|
||||
'striptags' => $striptags,
|
||||
]);
|
||||
|
||||
// Strip all HTML tags before filtering the text (avoiding XSS risk).
|
||||
if ($striptags) {
|
||||
$content = clean_param($content, PARAM_NOTAGS);
|
||||
}
|
||||
|
||||
$context = context::instance_by_id($contextid);
|
||||
self::validate_context($context);
|
||||
$result = filter_manager::instance()->filter_text($content, $context);
|
||||
|
||||
@@ -202,6 +202,7 @@ class MoodleExcelWorksheet {
|
||||
public function write_string($row, $col, $str, $format = null) {
|
||||
// For PhpSpreadsheet library, the column indexes start on 1 (instead of 0 as before).
|
||||
$col += 1;
|
||||
$str = \core\dataformat::escape_spreadsheet_formula($str);
|
||||
|
||||
$this->worksheet->getStyleByColumnAndRow($col, $row + 1)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT);
|
||||
$this->worksheet->setCellValueExplicitByColumnAndRow($col, $row + 1, $str, DataType::TYPE_STRING);
|
||||
|
||||
+16
-12
@@ -791,7 +791,7 @@ function file_get_drafarea_files($draftitemid, $filepath = '/') {
|
||||
}
|
||||
// find the file this draft file was created from and count all references in local
|
||||
// system pointing to that file
|
||||
$source = @unserialize($file->get_source() ?? '');
|
||||
$source = unserialize_object($file->get_source() ?? '');
|
||||
if (isset($source->original)) {
|
||||
$item->refcount = $fs->search_references_count($source->original);
|
||||
}
|
||||
@@ -911,9 +911,9 @@ function file_get_submitted_draft_itemid($elname) {
|
||||
* @return stored_file
|
||||
*/
|
||||
function file_restore_source_field_from_draft_file($storedfile) {
|
||||
$source = @unserialize($storedfile->get_source() ?? '');
|
||||
if (!empty($source)) {
|
||||
if (is_object($source)) {
|
||||
if (!empty($storedfile->get_source())) {
|
||||
$source = unserialize_object($storedfile->get_source());
|
||||
if (isset($source->source)) {
|
||||
$restoredsource = $source->source;
|
||||
$storedfile->set_source($restoredsource);
|
||||
} else {
|
||||
@@ -1205,7 +1205,7 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
|
||||
// Let's check if we can update this file or we need to delete and create.
|
||||
if ($newfile->is_directory()) {
|
||||
// Directories are always ok to just update.
|
||||
} else if (($source = @unserialize($newfile->get_source() ?? '')) && isset($source->original)) {
|
||||
} else if (($source = unserialize_object($newfile->get_source() ?? '')) && isset($source->original)) {
|
||||
// File has the 'original' - we need to update the file (it may even have not been changed at all).
|
||||
$original = file_storage::unpack_reference($source->original);
|
||||
if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) {
|
||||
@@ -1241,8 +1241,10 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
|
||||
// Field files.source for draftarea files contains serialised object with source and original information.
|
||||
// We only store the source part of it for non-draft file area.
|
||||
$newsource = $newfile->get_source();
|
||||
if ($source = @unserialize($newfile->get_source() ?? '')) {
|
||||
$newsource = $source->source;
|
||||
if ($source = unserialize_object($newfile->get_source() ?? '')) {
|
||||
if (isset($source->source)) {
|
||||
$newsource = $source->source;
|
||||
}
|
||||
}
|
||||
if ($oldfile->get_source() !== $newsource) {
|
||||
$oldfile->set_source($newsource);
|
||||
@@ -1275,10 +1277,12 @@ function file_save_draft_area_files($draftitemid, $contextid, $component, $filea
|
||||
// the size and subdirectory tests are extra safety only, the UI should prevent it
|
||||
foreach ($newhashes as $file) {
|
||||
$file_record = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'timemodified'=>time());
|
||||
if ($source = @unserialize($file->get_source() ?? '')) {
|
||||
if ($source = unserialize_object($file->get_source() ?? '')) {
|
||||
// Field files.source for draftarea files contains serialised object with source and original information.
|
||||
// We only store the source part of it for non-draft file area.
|
||||
$file_record['source'] = $source->source;
|
||||
if (isset($source->source)) {
|
||||
$file_record['source'] = $source->source;
|
||||
}
|
||||
}
|
||||
|
||||
if ($file->is_external_file()) {
|
||||
@@ -2928,7 +2932,7 @@ function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $ex
|
||||
|
||||
$fs = get_file_storage();
|
||||
// Remember original file source field.
|
||||
$source = @unserialize($existingfile->get_source() ?? '');
|
||||
$source = unserialize_object($existingfile->get_source() ?? '');
|
||||
// Remember the original sortorder.
|
||||
$sortorder = $existingfile->get_sortorder();
|
||||
if ($newfile->is_external_file()) {
|
||||
@@ -2952,7 +2956,7 @@ function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $ex
|
||||
$newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);
|
||||
// Preserve original file location (stored in source field) for handling references.
|
||||
if (isset($source->original)) {
|
||||
if (!($newfilesource = @unserialize($newfile->get_source() ?? ''))) {
|
||||
if (!($newfilesource = unserialize_object($newfile->get_source() ?? ''))) {
|
||||
$newfilesource = new stdClass();
|
||||
}
|
||||
$newfilesource->original = $source->original;
|
||||
@@ -4333,7 +4337,7 @@ class curl_cache {
|
||||
$fp = fopen($this->dir.$filename, 'r');
|
||||
$size = filesize($this->dir.$filename);
|
||||
$content = fread($fp, $size);
|
||||
return unserialize($content);
|
||||
return unserialize_array($content);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -27,7 +27,20 @@ Feature: Setting and validating date fields
|
||||
| dategroup2[group2optionaldatetime][enabled] | 1 |
|
||||
| Group2 optional date and time | ## 2023-08-31 14:45 ## |
|
||||
When I press "Send form"
|
||||
Then I should see "simpledateonly: 1690732800"
|
||||
Then the following fields match these values:
|
||||
| Simple only date | ## 2023-07-31 ## |
|
||||
| Simple optional only date | ## 2023-08-31 ## |
|
||||
| Simple date and time | ## 2023-07-31 11:15 ## |
|
||||
| Simple optional date and time | ## 2023-08-31 14:45 ## |
|
||||
| Group1 only date | ## 2023-07-31 ## |
|
||||
| Group1 optional only date | ## 2023-08-31 ## |
|
||||
| Group1 date and time | ## 2023-07-31 11:15 ## |
|
||||
| Group1 optional date and time | ## 2023-08-31 14:45 ## |
|
||||
| Group2 only date | ## 2023-07-31 ## |
|
||||
| Group2 optional only date | ## 2023-08-31 ## |
|
||||
| Group2 date and time | ## 2023-07-31 11:15 ## |
|
||||
| Group2 optional date and time | ## 2023-08-31 14:45 ## |
|
||||
And I should see "simpledateonly: 1690732800"
|
||||
And I should see "simpleoptionaldateonly: 1693411200"
|
||||
And I should see "simpledatetime: 1690773300"
|
||||
And I should see "simpleoptionaldatetime: 1693464300"
|
||||
@@ -62,7 +75,20 @@ Feature: Setting and validating date fields
|
||||
| dategroup2[group2optionaldatetime][enabled] | 1 |
|
||||
| dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## |
|
||||
When I press "Send form"
|
||||
Then I should see "simpledateonly: 1690732800"
|
||||
Then the following fields match these values:
|
||||
| simpledateonly | ## 2023-07-31 ## |
|
||||
| simpleoptionaldateonly | ## 2023-08-31 ## |
|
||||
| simpledatetime | ## 2023-07-31 11:15 ## |
|
||||
| simpleoptionaldatetime | ## 2023-08-31 14:45 ## |
|
||||
| group1dateonly | ## 2023-07-31 ## |
|
||||
| group1optionaldateonly | ## 2023-08-31 ## |
|
||||
| group1datetime | ## 2023-07-31 11:15 ## |
|
||||
| group1optionaldatetime | ## 2023-08-31 14:45 ## |
|
||||
| dategroup2[group2dateonly] | ## 2023-07-31 ## |
|
||||
| dategroup2[group2optionaldateonly] | ## 2023-08-31 ## |
|
||||
| dategroup2[group2datetime] | ## 2023-07-31 11:15 ## |
|
||||
| dategroup2[group2optionaldatetime] | ## 2023-08-31 14:45 ## |
|
||||
And I should see "simpledateonly: 1690732800"
|
||||
And I should see "simpleoptionaldateonly: 1693411200"
|
||||
And I should see "simpledatetime: 1690773300"
|
||||
And I should see "simpleoptionaldatetime: 1693464300"
|
||||
|
||||
+4
-2
@@ -1106,8 +1106,10 @@ function clean_param($param, $type) {
|
||||
// Absolute, and matches our wwwroot.
|
||||
} else {
|
||||
// Relative - let's make sure there are no tricks.
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') &&
|
||||
!preg_match('/javascript(?:.*\/{2,})?:/i', rawurldecode($param))) {
|
||||
if (
|
||||
validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') &&
|
||||
!preg_match('/\bjavascript\b[\s\S]*?(?:\:|\/)/i', rawurldecode($param))
|
||||
) {
|
||||
// Valid relative local URL.
|
||||
} else {
|
||||
$param = '';
|
||||
|
||||
@@ -998,7 +998,21 @@ class navigation_node implements renderable {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all static data.
|
||||
*
|
||||
* @throws coding_exception if called outside of a unit test
|
||||
*/
|
||||
public static function reset_all_data(): void {
|
||||
if (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST) {
|
||||
throw new coding_exception('Resetting all data is not allowed outside of PHPUnit tests.');
|
||||
}
|
||||
|
||||
self::$fullmeurl = null;
|
||||
self::$autofindactive = true;
|
||||
self::$loadadmintree = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,8 @@ class MoodleODSWorksheet {
|
||||
if (is_array($format)) {
|
||||
$format = new MoodleODSFormat($format);
|
||||
}
|
||||
$str = \core\dataformat::escape_spreadsheet_formula($str);
|
||||
|
||||
$this->data[$row][$col]->value = $str;
|
||||
$this->data[$row][$col]->type = 'string';
|
||||
$this->data[$row][$col]->format = $format;
|
||||
|
||||
@@ -199,6 +199,7 @@ class phpunit_util extends testing_util {
|
||||
// reinitialise following globals
|
||||
$OUTPUT = new bootstrap_renderer();
|
||||
$PAGE = new moodle_page();
|
||||
\navigation_node::reset_all_data();
|
||||
$FULLME = null;
|
||||
$ME = null;
|
||||
$SCRIPT = null;
|
||||
|
||||
@@ -145,6 +145,29 @@ STRING;
|
||||
self::assertTag(['id' => 'testid'], "<div><div>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the navigation node URL is overridden correctly.
|
||||
*/
|
||||
public function test_set_navigation_url(): void {
|
||||
\navigation_node::override_active_url(new \moodle_url('/foo/bar/baz'));
|
||||
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$this->assertNotNull($property->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the after-test teardown correctly resets the navigation node URL.
|
||||
*
|
||||
* @depends test_set_navigation_url
|
||||
*/
|
||||
public function test_navigation_url_reset(): void {
|
||||
$property = new \ReflectionProperty(\navigation_node::class, 'fullmeurl');
|
||||
$property->setAccessible(true);
|
||||
|
||||
$this->assertNull($property->getValue());
|
||||
}
|
||||
|
||||
// Uncomment following tests to see logging of unexpected changes in global state and database.
|
||||
/*
|
||||
public function test_db_modification() {
|
||||
|
||||
@@ -140,7 +140,7 @@ class behat_download extends behat_base {
|
||||
* @throws ExpectationException if verification fails.
|
||||
*/
|
||||
protected function verify_file_contains_text(string $filecontent, string $expectedcontent): void {
|
||||
if (!str_contains($filecontent, $expectedcontent)) {
|
||||
if (strpos($filecontent, $expectedcontent) === false) {
|
||||
throw new ExpectationException(
|
||||
"The string '$expectedcontent' was not found in the file content.",
|
||||
$this->getSession(),
|
||||
|
||||
@@ -78,7 +78,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on homepage$/
|
||||
*/
|
||||
public function i_am_on_homepage() {
|
||||
$this->execute('behat_general::i_visit', ['/']);
|
||||
$this->execute([self::class, 'i_visit'], ['/']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on site homepage$/
|
||||
*/
|
||||
public function i_am_on_site_homepage() {
|
||||
$this->execute('behat_general::i_visit', ['/?redirect=0']);
|
||||
$this->execute([self::class, 'i_visit'], ['/?redirect=0']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +96,7 @@ class behat_general extends behat_base {
|
||||
* @Given /^I am on course index$/
|
||||
*/
|
||||
public function i_am_on_course_index() {
|
||||
$this->execute('behat_general::i_visit', ['/course/index.php']);
|
||||
$this->execute([self::class, 'i_visit'], ['/course/index.php']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +442,7 @@ class behat_general extends behat_base {
|
||||
*/
|
||||
public function i_click_on_confirming_the_dialogue($element, $selectortype) {
|
||||
$this->i_click_on($element, $selectortype);
|
||||
$this->execute('behat_general::accept_currently_displayed_alert_dialog', []);
|
||||
$this->execute([self::class, 'accept_currently_displayed_alert_dialog'], []);
|
||||
$this->wait_until_the_page_is_ready();
|
||||
}
|
||||
|
||||
@@ -456,7 +456,7 @@ class behat_general extends behat_base {
|
||||
*/
|
||||
public function i_click_on_dismissing_the_dialogue($element, $selectortype) {
|
||||
$this->i_click_on($element, $selectortype);
|
||||
$this->execute('behat_general::dismiss_currently_displayed_alert_dialog', []);
|
||||
$this->execute([self::class, 'dismiss_currently_displayed_alert_dialog'], []);
|
||||
$this->wait_until_the_page_is_ready();
|
||||
}
|
||||
|
||||
@@ -1145,7 +1145,7 @@ EOF;
|
||||
* @Given /^I trigger cron$/
|
||||
*/
|
||||
public function i_trigger_cron() {
|
||||
$this->execute('behat_general::i_visit', ['/admin/cron.php']);
|
||||
$this->execute([self::class, 'i_visit'], ['/admin/cron.php']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2138,8 +2138,8 @@ EOF;
|
||||
}
|
||||
// Gets the node based on the requested selector type and locator.
|
||||
$node = $this->get_selected_node($selectortype, $element);
|
||||
$this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
|
||||
$this->execute([self::class, 'i_click_on'], [$node, 'NodeElement']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2252,9 +2252,9 @@ EOF;
|
||||
*/
|
||||
public function i_manually_press_tab($shift = '') {
|
||||
if (empty($shift)) {
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'tab']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'tab']);
|
||||
} else {
|
||||
$this->execute('behat_general::i_press_named_key', ['shift', 'tab']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['shift', 'tab']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2317,7 +2317,7 @@ EOF;
|
||||
* @throws DriverException
|
||||
*/
|
||||
public function i_manually_press_enter() {
|
||||
$this->execute('behat_general::i_press_named_key', ['', 'enter']);
|
||||
$this->execute([self::class, 'i_press_named_key'], ['', 'enter']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2352,7 +2352,7 @@ EOF;
|
||||
*/
|
||||
public function i_click_on_the_dynamic_tab(string $tabname): void {
|
||||
$xpath = "//*[@id='dynamictabs-tabs'][descendant::a[contains(text(), '" . $this->escape($tabname) . "')]]";
|
||||
$this->execute('behat_general::i_click_on_in_the',
|
||||
$this->execute([self::class, 'i_click_on_in_the'],
|
||||
[$tabname, 'link', $xpath, 'xpath_element']);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,4 +112,80 @@ class dataformat_test extends \advanced_testcase {
|
||||
$this->assertStringStartsWith($filerecord['filename'], $file->get_filename());
|
||||
$this->assertGreaterThan(0, $file->get_filesize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_escape_spreadsheet_formula.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function escape_spreadsheet_formula_provider(): array {
|
||||
return [
|
||||
'null stays null' => [
|
||||
null,
|
||||
null,
|
||||
],
|
||||
'empty string stays empty' => [
|
||||
'',
|
||||
'',
|
||||
],
|
||||
'Formula with tab' => [
|
||||
'value' => "\t=SUM(1+1)",
|
||||
'expected' => "'\t=SUM(1+1)",
|
||||
],
|
||||
'Formula with carriage return' => [
|
||||
'value' => "\r=SUM(1+1)",
|
||||
'expected' => "'\r=SUM(1+1)",
|
||||
],
|
||||
'Formula with new line' => [
|
||||
'value' => "\n=SUM(1+1)",
|
||||
'expected' => "'\n=SUM(1+1)",
|
||||
],
|
||||
'Formula starting with "="' => [
|
||||
'value' => "=SUM(1+1)",
|
||||
'expected' => "'=SUM(1+1)",
|
||||
],
|
||||
'Formula starting with "+"' => [
|
||||
'value' => "+1+1",
|
||||
'expected' => "'+1+1",
|
||||
],
|
||||
'Formula starting with "-"' => [
|
||||
'value' => "-1+1",
|
||||
'expected' => "'-1+1",
|
||||
],
|
||||
'Formula starting with "@"' => [
|
||||
'value' => "@A5",
|
||||
'expected' => "'@A5",
|
||||
],
|
||||
'Null placeholder' => [
|
||||
'value' => "-",
|
||||
'expected' => "-",
|
||||
],
|
||||
'dash with leading space is not placeholder, so escaped' => [
|
||||
' -',
|
||||
"' -",
|
||||
],
|
||||
'dash with trailing space is not placeholder, so escaped' => [
|
||||
'- ',
|
||||
"'- ",
|
||||
],
|
||||
'Non-formula' => [
|
||||
'value' => "Hello there",
|
||||
'expected' => "Hello there",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test escape_spreadsheet_formula.
|
||||
*
|
||||
* @dataProvider escape_spreadsheet_formula_provider
|
||||
* @param string|null $value The value to test.
|
||||
* @param string|null $expected The expected value after escaping.
|
||||
*/
|
||||
public function test_escape_spreadsheet_formula(?string $value, ?string $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$escapedvalue = dataformat::escape_spreadsheet_formula($value);
|
||||
$this->assertEquals($expected, $escapedvalue);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>This is an example HTML</h1>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque cursus lectus eu magna finibus fringilla. Maecenas in ipsum non velit vehicula blandit. Nunc consequat felis sit amet lectus pellentesque bibendum. Vestibulum tincidunt, sem eu vehicula rhoncus, libero erat consequat massa, non auctor nunc nisi ac sem. Proin eu mattis elit. Morbi nec luctus ante. Nullam non augue diam. Etiam vestibulum massa et est tempor, ac iaculis justo tempor. Fusce eu magna id ligula lacinia molestie. Cras facilisis velit quis purus dapibus placerat sit amet at odio. Aliquam luctus commodo ligula iaculis dignissim. Vivamus placerat, ligula ut auctor efficitur, purus urna accumsan nisl, in placerat libero leo at mauris. Integer a diam quis purus iaculis semper ac suscipit lacus. Mauris ut orci lacinia, sollicitudin odio ut, pharetra augue.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,6 +48,9 @@ final class navigationlib_test extends \advanced_testcase {
|
||||
protected function setup_node() {
|
||||
global $PAGE, $SITE;
|
||||
|
||||
// Perform a reset between tests to reset the PAGE.
|
||||
$this->resetAfterTest();
|
||||
|
||||
$PAGE->set_url('/');
|
||||
$PAGE->set_course($SITE);
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ use core_qrcode;
|
||||
* @author <juan@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(core_qrcode::class)]
|
||||
final class qrcode_test extends \basic_testcase {
|
||||
|
||||
/**
|
||||
* Basic test to generate a QR code and check that the library is not broken.
|
||||
*/
|
||||
@@ -36,9 +36,9 @@ final class qrcode_test extends \basic_testcase {
|
||||
// binary file can be different. This is why tests are limited.
|
||||
|
||||
$text = 'abc';
|
||||
$color = 'black';
|
||||
$qrcode = new core_qrcode($text, $color);
|
||||
$svgdata = $qrcode->getBarcodeSVGcode(1, 1);
|
||||
$color = 'green';
|
||||
$qrcode = new core_qrcode($text);
|
||||
$svgdata = $qrcode->getBarcodeSVGcode(1, 1, $color);
|
||||
|
||||
// Just check the SVG was generated.
|
||||
$this->assertStringContainsString('<desc>' . $text . '</desc>', $svgdata);
|
||||
|
||||
@@ -829,4 +829,33 @@ final class tablelib_test extends \advanced_testcase {
|
||||
$this->expectOutputRegex('/' . '<caption class="inline">' . $caption . '<\/caption>' . '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test formulas are escaped in exported tables.
|
||||
*/
|
||||
public function test_table_exports_escaped_formulas(): void {
|
||||
$table = new flexible_table('tablelib_test_export');
|
||||
$table->define_baseurl('/invalid.php');
|
||||
$table->define_columns(['c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6']);
|
||||
|
||||
ob_start();
|
||||
$table->is_downloadable(true);
|
||||
$table->is_downloading('csv');
|
||||
|
||||
$table->setup();
|
||||
$table->add_data([
|
||||
'column0' => " =SUM(1+1)", // With spaces.
|
||||
'column1' => "=SUM(1+1)",
|
||||
'column2' => "=1+1",
|
||||
'column3' => "+1+1",
|
||||
'column4' => "-1+1",
|
||||
'column5' => "@A1",
|
||||
'column6' => "-", // Single dash (should not be escaped).
|
||||
]);
|
||||
|
||||
$output = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$matchregex = "/\"?' =SUM\(1\+1\)\"?,'=SUM\(1\+1\),'=1\+1,'\+1\+1,'-1\+1,'@A1,-/";
|
||||
$this->assertMatchesRegularExpression($matchregex, $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
This files describes API changes in core libraries and APIs,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.22 ===
|
||||
* message: When responding to pcntl signals, call existing signal handlers.
|
||||
|
||||
=== 4.1.21 ===
|
||||
|
||||
* Moodle's Behat `::execute()` method now supports the use of array callable syntax for calling other steps.
|
||||
|
||||
=== 4.1.20 ===
|
||||
|
||||
* Add a new method has_valid_group in \core\report_helper that will return true or false depending if the user has a valid group. This is mainly
|
||||
|
||||
@@ -1025,7 +1025,7 @@ class moodle_xhprofrun implements iXHProfRuns {
|
||||
// First pass, we are going to remove all the elements which
|
||||
// both parent and child are __Mustache function calls.
|
||||
foreach ($sorted as $key => $value) {
|
||||
if (!str_contains($key, '==>')) {
|
||||
if (strpos($key, '==>') === false) {
|
||||
$parent = 'NULL';
|
||||
$child = $key;
|
||||
} else {
|
||||
@@ -1047,7 +1047,7 @@ class moodle_xhprofrun implements iXHProfRuns {
|
||||
// one unique pass is enough. Without it, we may need to perform
|
||||
// N (while loop) second passes.
|
||||
foreach ($sorted as $key => $value) {
|
||||
if (!str_contains($key, '==>')) {
|
||||
if (strpos($key, '==>') === false) {
|
||||
$parent = 'NULL';
|
||||
$child = $key;
|
||||
} else {
|
||||
@@ -1092,7 +1092,7 @@ class moodle_xhprofrun implements iXHProfRuns {
|
||||
if (isset($visited[$key])) {
|
||||
continue;
|
||||
}
|
||||
if (!str_contains($key, '==>')) {
|
||||
if (strpos($key, '==>') === false) {
|
||||
// It's a root element, we can add it to the sorted array.
|
||||
$sorted[$key] = $info[$key];
|
||||
$visited[$key] = true;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
@core
|
||||
Feature: Login using email address
|
||||
Users should be able to access their site
|
||||
As a user
|
||||
I should be able to login using email
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | password | firstname | lastname | email |
|
||||
| testuser | test | Test | User | user@example.com |
|
||||
|
||||
Scenario Outline: A user can login using their email address
|
||||
Given the following config values are set as admin:
|
||||
| authloginviaemail | <authloginviaemail> |
|
||||
When I follow "Log in"
|
||||
And I set the field "Username" to "<login>"
|
||||
And I set the field "Password" to "test"
|
||||
And I press "Log in"
|
||||
Then I should see "<message>"
|
||||
|
||||
Examples:
|
||||
| authloginviaemail | login | message |
|
||||
| 0 | testuser | You are logged in as |
|
||||
| 0 | user@example.com | Invalid login, please try again |
|
||||
| 1 | testuser | You are logged in as |
|
||||
| 1 | user@example.com | You are logged in as |
|
||||
@@ -89,6 +89,16 @@ class user_submission_actionmenu implements templatable, renderable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the submission started.
|
||||
*
|
||||
* @return bool The status of the submission; true if started, otherwise false.
|
||||
*/
|
||||
protected function is_submission_started(): bool {
|
||||
return (is_object($this->teamsubmission) && isset($this->teamsubmission->timestarted))
|
||||
|| (is_object($this->submission) && isset($this->submission->timestarted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the submission buttons for the page.
|
||||
*
|
||||
@@ -125,19 +135,20 @@ class user_submission_actionmenu implements templatable, renderable {
|
||||
$data['edit']['help'] = $newattempthelp->export_for_template($output);
|
||||
}
|
||||
if ($status === ASSIGN_SUBMISSION_STATUS_NEW) {
|
||||
|
||||
if ($this->timelimit && empty($this->submission->timestarted)) {
|
||||
$timelimitenabled = get_config('assign', 'enabletimelimit');
|
||||
if ($timelimitenabled && $this->timelimit && !$this->is_submission_started()) {
|
||||
$confirmation = new \confirm_action(
|
||||
get_string('confirmstart', 'assign', format_time($this->timelimit)),
|
||||
null,
|
||||
get_string('beginassignment', 'assign')
|
||||
);
|
||||
$urlparams = array('id' => $this->cmid, 'action' => 'editsubmission');
|
||||
// The 'begin' flag indicates that the user is starting a timed assignment.
|
||||
$urlparams = ['id' => $this->cmid, 'action' => 'editsubmission', 'begin' => 1];
|
||||
$beginbutton = new \action_link(
|
||||
new moodle_url('/mod/assign/view.php', $urlparams),
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
);
|
||||
$data['edit']['button'] = $beginbutton->export_for_template($output);
|
||||
$data['edit']['begin'] = true;
|
||||
|
||||
@@ -27,22 +27,33 @@ class PdfType
|
||||
* @param PdfType $value
|
||||
* @param PdfParser $parser
|
||||
* @param bool $stopAtIndirectObject
|
||||
* @param array $ensuredObjectsList A list of all ensured indirect objects to prevent recursion
|
||||
* @return PdfType
|
||||
* @throws CrossReferenceException
|
||||
* @throws PdfParserException
|
||||
*/
|
||||
public static function resolve(PdfType $value, PdfParser $parser, $stopAtIndirectObject = false)
|
||||
{
|
||||
public static function resolve(
|
||||
PdfType $value,
|
||||
PdfParser $parser,
|
||||
$stopAtIndirectObject = false,
|
||||
array &$ensuredObjectsList = []
|
||||
) {
|
||||
if ($value instanceof PdfIndirectObjectReference) {
|
||||
$value = $parser->getIndirectObject($value->value);
|
||||
}
|
||||
|
||||
if ($value instanceof PdfIndirectObject) {
|
||||
if ($stopAtIndirectObject === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return self::resolve($value->value, $parser, $stopAtIndirectObject);
|
||||
}
|
||||
|
||||
if ($value instanceof PdfIndirectObjectReference) {
|
||||
return self::resolve($parser->getIndirectObject($value->value), $parser, $stopAtIndirectObject);
|
||||
if (\in_array($value->objectNumber, $ensuredObjectsList, true)) {
|
||||
throw new PdfParserException(
|
||||
\sprintf('Indirect reference recursion detected (%s).', $value->objectNumber)
|
||||
);
|
||||
}
|
||||
$ensuredObjectsList[] = $value->objectNumber;
|
||||
return self::resolve($value->value, $parser, $stopAtIndirectObject, $ensuredObjectsList);
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
@@ -139,12 +139,19 @@ class PdfReader
|
||||
$page = $this->pages[$pageNumber - 1];
|
||||
|
||||
if ($page instanceof PdfIndirectObjectReference) {
|
||||
$readPages = function ($kids) use (&$readPages) {
|
||||
$alreadyReadKids = [];
|
||||
$readPages = function ($kids) use (&$readPages, &$alreadyReadKids) {
|
||||
$kids = PdfArray::ensure($kids);
|
||||
|
||||
/** @noinspection LoopWhichDoesNotLoopInspection */
|
||||
foreach ($kids->value as $reference) {
|
||||
$reference = PdfIndirectObjectReference::ensure($reference);
|
||||
|
||||
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||
}
|
||||
$alreadyReadKids[] = $reference->value;
|
||||
|
||||
$object = $this->parser->getIndirectObject($reference->value);
|
||||
$type = PdfDictionary::get($object->value, 'Type');
|
||||
|
||||
@@ -168,6 +175,7 @@ class PdfReader
|
||||
if ($type->value === 'Pages') {
|
||||
$kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser);
|
||||
try {
|
||||
$alreadyReadKids[] = $page->objectNumber;
|
||||
$page = $this->pages[$pageNumber - 1] = $readPages($kids);
|
||||
} catch (PdfReaderException $e) {
|
||||
if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) {
|
||||
@@ -202,7 +210,8 @@ class PdfReader
|
||||
return;
|
||||
}
|
||||
|
||||
$readPages = function ($kids, $count) use (&$readPages, $readAll) {
|
||||
$alreadyReadKids = [];
|
||||
$readPages = function ($kids, $count) use (&$readPages, &$alreadyReadKids, $readAll) {
|
||||
$kids = PdfArray::ensure($kids);
|
||||
$isLeaf = ($count->value === \count($kids->value));
|
||||
|
||||
@@ -214,6 +223,11 @@ class PdfReader
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\in_array($reference->value, $alreadyReadKids, true)) {
|
||||
throw new PdfReaderException('Recursive pages dictionary detected.');
|
||||
}
|
||||
$alreadyReadKids[] = $reference->value;
|
||||
|
||||
$object = $this->parser->getIndirectObject($reference->value);
|
||||
$type = PdfDictionary::get($object->value, 'Type');
|
||||
|
||||
|
||||
+11
-19
@@ -1242,11 +1242,11 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
if (empty($row->recordid)) {
|
||||
$row->recordid = $this->assignment->get_uniqueid_for_user($row->userid);
|
||||
}
|
||||
$urlparams['blindid'] = $row->recordid;
|
||||
$useridurlparam = ['blindid' => $row->recordid];
|
||||
} else {
|
||||
$urlparams['userid'] = $row->userid;
|
||||
$useridurlparam = ['userid' => $row->userid];
|
||||
}
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$noimage = null;
|
||||
|
||||
if (!$row->grade) {
|
||||
@@ -1285,11 +1285,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
|
||||
if (!$row->locked) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'lock',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
|
||||
$description = get_string('preventsubmissionsshort', 'assign');
|
||||
$actions['lock'] = new action_menu_link_secondary(
|
||||
@@ -1299,11 +1298,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
);
|
||||
} else {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'unlock',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('allowsubmissionsshort', 'assign');
|
||||
$actions['unlock'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1317,11 +1315,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
$USER->id != $row->id &&
|
||||
$caneditsubmission) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'editsubmission',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('editsubmission', 'assign');
|
||||
$actions['editsubmission'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1333,11 +1330,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
$caneditsubmission &&
|
||||
!empty($row->status)) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'removesubmissionconfirm',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('removesubmission', 'assign');
|
||||
$actions['removesubmission'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1350,11 +1346,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
$this->assignment->get_instance()->cutoffdate) &&
|
||||
$this->hasgrantextension) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'grantextension',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('grantextension', 'assign');
|
||||
$actions['grantextension'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1365,11 +1360,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
if ($row->status == ASSIGN_SUBMISSION_STATUS_SUBMITTED &&
|
||||
$this->assignment->get_instance()->submissiondrafts) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'reverttodraft',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('reverttodraftshort', 'assign');
|
||||
$actions['reverttodraft'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1383,11 +1377,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
$submissionsopen &&
|
||||
$row->id != $USER->id) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'submitotherforgrading',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('submitforgrading', 'assign');
|
||||
$actions['submitforgrading'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
@@ -1404,11 +1397,10 @@ class assign_grading_table extends table_sql implements renderable {
|
||||
|
||||
if ($ismanual && $hassubmission && $notreopened && $hasattempts) {
|
||||
$urlparams = array('id' => $this->assignment->get_course_module()->id,
|
||||
'userid' => $row->id,
|
||||
'action' => 'addattempt',
|
||||
'sesskey' => sesskey(),
|
||||
'page' => $this->currpage);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
$url = new moodle_url('/mod/assign/view.php', $urlparams + $useridurlparam);
|
||||
$description = get_string('addattempt', 'assign');
|
||||
$actions['addattempt'] = new action_menu_link_secondary(
|
||||
$url,
|
||||
|
||||
+137
-18
@@ -533,7 +533,16 @@ class assign {
|
||||
$nextpageparams['action'] = 'view';
|
||||
}
|
||||
} else if ($action == 'addattempt') {
|
||||
$this->process_add_attempt(required_param('userid', PARAM_INT));
|
||||
$userid = false;
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
// Fallback to userid, if not found by blindid.
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
}
|
||||
$this->process_add_attempt($userid);
|
||||
$action = 'redirect';
|
||||
$nextpageparams['action'] = 'grading';
|
||||
} else if ($action == 'reverttodraft') {
|
||||
@@ -2997,7 +3006,14 @@ class assign {
|
||||
|
||||
$users = optional_param('userid', 0, PARAM_INT);
|
||||
if (!$users) {
|
||||
$users = required_param('selectedusers', PARAM_SEQUENCE);
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
$users = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
// We need users, if not found by blindid.
|
||||
if (!$users) {
|
||||
$users = required_param('selectedusers', PARAM_SEQUENCE);
|
||||
}
|
||||
}
|
||||
$userlist = explode(',', $users);
|
||||
|
||||
@@ -3145,7 +3161,9 @@ class assign {
|
||||
if ($create) {
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
if ($action == 'editsubmission') {
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
|
||||
$starttimer = optional_param('begin', 0, PARAM_INT);
|
||||
// Only start the timer if the user has clicked the 'Begin assignment' button.
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
|
||||
$submission->timestarted = time();
|
||||
$DB->update_record('assign_submission', $submission);
|
||||
}
|
||||
@@ -3788,7 +3806,9 @@ class assign {
|
||||
if ($create) {
|
||||
$action = optional_param('action', '', PARAM_TEXT);
|
||||
if ($action == 'editsubmission') {
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit) {
|
||||
$starttimer = optional_param('begin', 0, PARAM_INT);
|
||||
// Only start the timer if the user has clicked the 'Begin assignment' button.
|
||||
if (empty($submission->timestarted) && $this->get_instance()->timelimit && $starttimer) {
|
||||
$submission->timestarted = time();
|
||||
$DB->update_record('assign_submission', $submission);
|
||||
}
|
||||
@@ -4337,7 +4357,29 @@ class assign {
|
||||
protected function view_remove_submission_confirm() {
|
||||
global $USER, $PAGE;
|
||||
|
||||
$userid = optional_param('userid', $USER->id, PARAM_INT);
|
||||
$userid = optional_param('userid', 0, PARAM_INT);
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
|
||||
// Construct the base URL parameters for the confirm and cancel actions.
|
||||
$urlparams = [
|
||||
'id' => $this->get_course_module()->id,
|
||||
'action' => 'removesubmission',
|
||||
'sesskey' => sesskey(),
|
||||
];
|
||||
|
||||
// Determine the correct user ID.
|
||||
if ($userid) {
|
||||
// Real user ID was explicitly provided.
|
||||
$urlparams['userid'] = $userid;
|
||||
} elseif ($this->is_blind_marking() && $blindid) {
|
||||
// Blind marking is in use. Resolve anonymized (blind) ID to the real user ID to obtain the user object later.
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
$urlparams['blindid'] = $blindid;
|
||||
} else {
|
||||
// Default to the currently logged-in user (e.g. user deleting their own submission scenario).
|
||||
$userid = $USER->id;
|
||||
$urlparams['userid'] = $userid;
|
||||
}
|
||||
|
||||
$PAGE->set_pagelayout('standard');
|
||||
|
||||
@@ -4353,10 +4395,6 @@ class assign {
|
||||
$this->get_course_module()->id);
|
||||
$o .= $this->get_renderer()->render($header);
|
||||
|
||||
$urlparams = array('id' => $this->get_course_module()->id,
|
||||
'action' => 'removesubmission',
|
||||
'userid' => $userid,
|
||||
'sesskey' => sesskey());
|
||||
$confirmurl = new moodle_url('/mod/assign/view.php', $urlparams);
|
||||
|
||||
$urlparams = array('id' => $this->get_course_module()->id,
|
||||
@@ -4667,7 +4705,7 @@ class assign {
|
||||
$userid = optional_param('userid', 0, PARAM_INT);
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
|
||||
if (!$userid && $blindid) {
|
||||
if ($this->is_blind_marking() && !$userid && $blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
|
||||
@@ -4796,12 +4834,22 @@ class assign {
|
||||
* @return string The page output.
|
||||
*/
|
||||
protected function view_edit_submission_page($mform, $notices) {
|
||||
global $CFG, $USER, $DB, $PAGE;
|
||||
global $CFG, $USER, $DB, $PAGE, $OUTPUT;
|
||||
|
||||
$o = '';
|
||||
require_once($CFG->dirroot . '/mod/assign/submission_form.php');
|
||||
// Need submit permission to submit an assignment.
|
||||
$userid = optional_param('userid', $USER->id, PARAM_INT);
|
||||
$userid = optional_param('userid', 0, PARAM_INT);
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
|
||||
if ($this->is_blind_marking() && !$userid && $blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
// If no userid specified, default to current user.
|
||||
if (!$userid) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
|
||||
$user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST);
|
||||
$timelimitenabled = get_config('assign', 'enabletimelimit');
|
||||
|
||||
@@ -4881,7 +4929,30 @@ class assign {
|
||||
$o .= $this->get_renderer()->notification($notice);
|
||||
}
|
||||
|
||||
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
|
||||
if (
|
||||
$submission->status == ASSIGN_SUBMISSION_STATUS_NEW && $this->get_instance()->timelimit &&
|
||||
empty($submission->timestarted)
|
||||
) {
|
||||
// Timed assignment should always get a confirmation that the user wants to start it.
|
||||
$confirmation = new \confirm_action(
|
||||
get_string('confirmstart', 'assign', format_time($this->get_instance()->timelimit)),
|
||||
null,
|
||||
get_string('beginassignment', 'assign')
|
||||
);
|
||||
// The 'begin' flag indicates that the user is starting a timed assignment.
|
||||
$urlparams = ['id' => $this->get_course_module()->id, 'action' => 'editsubmission', 'begin' => 1];
|
||||
$beginbutton = new \action_link(
|
||||
new moodle_url('/mod/assign/view.php', $urlparams),
|
||||
get_string('beginassignment', 'assign'),
|
||||
$confirmation,
|
||||
['class' => 'btn btn-primary']
|
||||
);
|
||||
|
||||
$o .= $OUTPUT->render($beginbutton);
|
||||
} else {
|
||||
$o .= $this->get_renderer()->render(new assign_form('editsubmissionform', $mform));
|
||||
}
|
||||
|
||||
$o .= $this->view_footer();
|
||||
|
||||
\mod_assign\event\submission_form_viewed::create_from_user($this, $user)->trigger();
|
||||
@@ -6842,7 +6913,17 @@ class assign {
|
||||
|
||||
require_sesskey();
|
||||
|
||||
$userid = optional_param('userid', $USER->id, PARAM_INT);
|
||||
$userid = optional_param('userid', 0, PARAM_INT);
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
|
||||
if ($this->is_blind_marking() && !$userid && $blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
|
||||
// If no userid specified, default to current user.
|
||||
if (!$userid) {
|
||||
$userid = $USER->id;
|
||||
}
|
||||
|
||||
if (!$this->submissions_open($userid)) {
|
||||
$notices[] = get_string('submissionsclosed', 'assign');
|
||||
@@ -8346,7 +8427,15 @@ class assign {
|
||||
require_sesskey();
|
||||
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
|
||||
// Userid is required, if not found by blindid.
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->remove_submission($userid);
|
||||
@@ -8363,7 +8452,17 @@ class assign {
|
||||
require_sesskey();
|
||||
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
if ($blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
}
|
||||
|
||||
// Userid is required, if not found by blindid.
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->revert_to_draft($userid);
|
||||
@@ -8539,7 +8638,17 @@ class assign {
|
||||
require_sesskey();
|
||||
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
if ($blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
}
|
||||
|
||||
// Userid is required, if not found by blindid.
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lock_submission($userid);
|
||||
@@ -8588,7 +8697,17 @@ class assign {
|
||||
require_sesskey();
|
||||
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
if ($this->is_blind_marking()) {
|
||||
$blindid = optional_param('blindid', 0, PARAM_INT);
|
||||
if ($blindid) {
|
||||
$userid = $this->get_user_id_for_uniqueid($blindid);
|
||||
}
|
||||
}
|
||||
|
||||
// Userid is required, if not found by blindid.
|
||||
if (!$userid) {
|
||||
$userid = required_param('userid', PARAM_INT);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->unlock_submission($userid);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@mod @mod_assign
|
||||
Feature: In a timed assignment, students should confirm before starting the timer
|
||||
In order to submit a timed assignment
|
||||
As a student
|
||||
I need to confirm to begin the assignment before the timer starts
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
| config | value | plugin |
|
||||
| enabletimelimit | 1 | assign |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
| student2 | Student | 2 | student2@example.com |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name | assignsubmission_file_enabled | assignsubmission_file_maxfiles | assignsubmission_file_maxsizebytes | teamsubmission | duedate | timelimit |
|
||||
| assign | C1 | Timed assignment 1 | 1 | 1 | 2097152 | 0 | ##+1 hours 30 minutes## | 60 |
|
||||
| assign | C1 | Group assignment 2 | 1 | 1 | 2097152 | 1 | ##+1 hours 30 minutes## | 60 |
|
||||
|
||||
Scenario: Access a timed assignment from the course page
|
||||
Given I am on the "Timed assignment 1" Activity page logged in as student1
|
||||
And "Begin assignment" "link" should exist
|
||||
When I reload the page
|
||||
Then "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
|
||||
@javascript
|
||||
Scenario: Access a timed assignment from the Dashboard
|
||||
Given I am logged in as student1
|
||||
When I click on "Timed assignment 1" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
Then "Begin assignment" "link" should exist
|
||||
And I reload the page
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Repeat the steps to confirm timer doesn't start automatically.
|
||||
And I follow "Dashboard"
|
||||
And I click on "Timed assignment 1" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Now start the timer.
|
||||
And I click on "Begin assignment" "link"
|
||||
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
|
||||
And "#mod_assign_timelimit_block" "css_element" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Access a timed group assignment from the Dashboard
|
||||
Given the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | CG1 |
|
||||
And the following "group members" exist:
|
||||
| user | group |
|
||||
| student1 | CG1 |
|
||||
| student2 | CG1 |
|
||||
And I am logged in as student1
|
||||
When I click on "Group assignment 2" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
Then "Begin assignment" "link" should exist
|
||||
And I reload the page
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Repeat the steps to confirm timer doesn't start automatically.
|
||||
And I follow "Dashboard"
|
||||
And I click on "Group assignment 2" "link" in the "Calendar" "block"
|
||||
And I click on "Add submission" "link" in the ".modal-footer" "css_element"
|
||||
And "Begin assignment" "link" should exist
|
||||
And "#mod_assign_timelimit_block" "css_element" should not exist
|
||||
# Now start the timer.
|
||||
And I click on "Begin assignment" "link"
|
||||
And I click on "Begin assignment" "button" in the ".confirmation-buttons" "css_element"
|
||||
And "#mod_assign_timelimit_block" "css_element" should exist
|
||||
And I log out
|
||||
# Now check the submission has started for the other group member too.
|
||||
And I am on the "Group assignment 2" "assign activity" page logged in as student2
|
||||
And "Begin assignment" "link" should not exist
|
||||
And "Add submission" "button" should exist
|
||||
@@ -2430,6 +2430,8 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
|
||||
|
||||
// Set optional param to indicate start time required.
|
||||
$_GET['action'] = 'editsubmission';
|
||||
// Set optional param to indicate that assignment submission timer should start.
|
||||
$_GET['begin'] = 1;
|
||||
$cm = get_coursemodule_from_instance('assign', $instance->id);
|
||||
$context = \context_module::instance($cm->id);
|
||||
(new \assign($context, $cm, $cm->course))->get_user_submission(0, true);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
@mod @mod_choice
|
||||
Feature: Specify any number of choice responses
|
||||
In order to make a choice activity
|
||||
As a teacher
|
||||
I need to be able to add any number of responses
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | option |
|
||||
| choice | Choice 1 | Test choice description | C1 | one, two, three |
|
||||
|
||||
Scenario: Teacher can add and display any number of responses in a choice activity
|
||||
Given I am on the "Choice 1" "choice activity" page logged in as "teacher1"
|
||||
And I should see "one"
|
||||
And I should see "two"
|
||||
And I should see "three"
|
||||
And I should not see "four"
|
||||
And I should not see "five"
|
||||
And I should not see "six"
|
||||
When I am on the "Choice 1" "choice activity editing" page
|
||||
And I press "Add 3 field(s) to form"
|
||||
And I set the following fields to these values:
|
||||
| Option 4 | four |
|
||||
| Option 5 | five |
|
||||
| Option 6 | six |
|
||||
And I press "Save and display"
|
||||
Then I should see "one"
|
||||
And I should see "two"
|
||||
And I should see "three"
|
||||
And I should see "four"
|
||||
And I should see "five"
|
||||
And I should see "six"
|
||||
+14
-5
@@ -1612,7 +1612,7 @@ function data_rating_validate($params) {
|
||||
* @throws rating_exception
|
||||
*/
|
||||
function mod_data_rating_can_see_item_ratings($params) {
|
||||
global $DB;
|
||||
global $DB, $USER;
|
||||
|
||||
// Check the component is mod_data.
|
||||
if (!isset($params['component']) || $params['component'] != 'mod_data') {
|
||||
@@ -1628,7 +1628,7 @@ function mod_data_rating_can_see_item_ratings($params) {
|
||||
throw new rating_exception('invaliditemid');
|
||||
}
|
||||
|
||||
$datasql = "SELECT d.id as dataid, d.course, r.groupid
|
||||
$datasql = "SELECT d.id as dataid, d.course, r.userid, r.groupid
|
||||
FROM {data_records} r
|
||||
JOIN {data} d ON r.dataid = d.id
|
||||
WHERE r.id = :itemid";
|
||||
@@ -1638,14 +1638,23 @@ function mod_data_rating_can_see_item_ratings($params) {
|
||||
throw new rating_exception('invaliditemid');
|
||||
}
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST);
|
||||
$context = context_module::instance($cm->id);
|
||||
|
||||
if (!empty($info->userid)) {
|
||||
$ratingpermissions = data_rating_permissions($context->id, 'mod_data', 'entry');
|
||||
$requiredpermission = ($info->userid != $USER->id) ? 'viewall' : 'view';
|
||||
if (!$ratingpermissions[$requiredpermission]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// User can see ratings of all participants.
|
||||
if ($info->groupid == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $info->course), '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('data', $info->dataid, $course->id, false, MUST_EXIST);
|
||||
|
||||
// Make sure groups allow this user to see the item they're rating.
|
||||
return groups_group_visible($info->groupid, $course, $cm);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,19 @@ class mod_feedback_generator extends testing_module_generator {
|
||||
$data['values'] = $this->format_item_values($questiontype, $data['values']);
|
||||
}
|
||||
|
||||
if (isset($data['dependitem'])) {
|
||||
$select = 'label = ? OR name = ?';
|
||||
$params = [
|
||||
$data['dependitem'],
|
||||
$data['dependitem'],
|
||||
];
|
||||
$dependid = $DB->get_field_select('feedback_item', 'id', $select, $params, MUST_EXIST);
|
||||
$data['dependitem'] = $dependid;
|
||||
} else {
|
||||
// Make sure `dependvalue` is not set if we are not creating a dependent item.
|
||||
unset($data['dependvalue']);
|
||||
}
|
||||
|
||||
return call_user_func([$this, "create_item_{$questiontype}"], $feedback, $data);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
@mod @mod_folder @_file_upload @core_filepicker
|
||||
Feature: Folder files can be viewed without downloading
|
||||
In order to view folder files without downloading
|
||||
As a teacher
|
||||
I need to be able to disable 'Force download of files'
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| folder | C1 | Folder 1 |
|
||||
And I am on the "Folder 1" "folder activity editing" page logged in as teacher1
|
||||
And I upload "lib/tests/fixtures/test.html" file to "Files" filemanager
|
||||
And I upload "course/tests/fixtures/image.jpg" file to "Files" filemanager
|
||||
And I press "Save and return to course"
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Files within a folder activity is automatically downloaded
|
||||
When I am on the "Folder 1" "folder activity" page
|
||||
# As a teacher, confirm that file is downloaded automatically.
|
||||
Then following "<filename>" should download a file that:
|
||||
| Has mimetype | <mimetype> |
|
||||
# Confirm the same behaviour as a student.
|
||||
And I am on the "Folder 1" "folder activity" page logged in as student1
|
||||
And following "<filename>" should download a file that:
|
||||
| Has mimetype | <mimetype> |
|
||||
|
||||
Examples:
|
||||
| filename | mimetype |
|
||||
| image.jpg | image/jpeg |
|
||||
| test.html | text/html |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: Teacher can disable force download for folder activity
|
||||
Given I am on the "Folder 1" "folder activity editing" page
|
||||
And I set the following fields to these values:
|
||||
| forcedownload | 0 |
|
||||
And I press "Save and display"
|
||||
When I click on "<filename>" "link"
|
||||
# As a teacher, confirm that file is not downloaded.
|
||||
Then "<displayedtext>" "<elementtype>" should exist
|
||||
# Confirm the same behaviour as a student.
|
||||
And I am on the "Folder 1" "folder activity" page logged in as student1
|
||||
And I click on "<filename>" "link"
|
||||
And "<displayedtext>" "<elementtype>" should exist
|
||||
|
||||
Examples:
|
||||
| filename | displayedtext | elementtype |
|
||||
| image.jpg | //img[contains(@src, '/image.jpg')] | xpath_element |
|
||||
| test.html | This is an example HTML | text |
|
||||
@@ -2300,6 +2300,12 @@ function mod_forum_rating_can_see_item_ratings($params) {
|
||||
$forum = $DB->get_record('forum', array('id' => $discussion->forum), '*', MUST_EXIST);
|
||||
$course = $DB->get_record('course', array('id' => $forum->course), '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('forum', $forum->id, $course->id , false, MUST_EXIST);
|
||||
$context = context_module::instance($cm->id);
|
||||
$ratingpermissions = forum_rating_permissions($context->id, 'mod_forum', 'post');
|
||||
$requiredpermission = ($post->userid != $USER->id) ? 'viewall' : 'view';
|
||||
if (!$ratingpermissions[$requiredpermission]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform some final capability checks.
|
||||
if (!forum_user_can_see_post($forum, $discussion, $post, $USER, $cm)) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@mod @mod_forum @core_rss
|
||||
Feature: Verify that default RSS settings for forums are correctly applied.
|
||||
In order to ensure consistent RSS configuration across new forums
|
||||
As an admin
|
||||
I need to verify that the default RSS values set at site level appear when creating a new forum.
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| RSS Test Course | RSS101 |
|
||||
And the following config values are set as admin:
|
||||
| enablerssfeeds | 1 |
|
||||
| forum_enablerssfeeds | 1 |
|
||||
| forum_rsstype | 1 |
|
||||
| forum_rssarticles | 5 |
|
||||
|
||||
Scenario: Admin sets default forum RSS values and verifies them on new forum creation
|
||||
Given I log in as "admin"
|
||||
When I am on "RSS101" course homepage with editing mode on
|
||||
And I add a forum activity to course "RSS101" section "New section"
|
||||
And I expand all fieldsets
|
||||
Then the field "RSS feed for this activity" matches value "Discussions"
|
||||
And the field "Number of RSS recent articles" matches value "5"
|
||||
@@ -4507,3 +4507,52 @@ function mod_glossary_prepare_entry_for_edition(stdClass $entry): stdClass {
|
||||
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current user can see ratings for a given itemid.
|
||||
*
|
||||
* @param array $params submitted data
|
||||
* contextid => int contextid [required]
|
||||
* component => The component for this module - should always be mod_glossary [required]
|
||||
* ratingarea => Should always be entry (the only rating area in glossary) [required]
|
||||
* itemid => int the ID of the entry being rated [required]
|
||||
* @return bool
|
||||
*/
|
||||
function mod_glossary_rating_can_see_item_ratings(array $params): bool {
|
||||
global $DB, $USER;
|
||||
|
||||
if (!isset($params['component']) || $params['component'] != 'mod_glossary') {
|
||||
throw new rating_exception('invalidcomponent');
|
||||
}
|
||||
|
||||
if (!isset($params['ratingarea']) || $params['ratingarea'] != 'entry') {
|
||||
throw new rating_exception('invalidratingarea');
|
||||
}
|
||||
|
||||
if (!isset($params['itemid'])) {
|
||||
throw new rating_exception('invaliditemid');
|
||||
}
|
||||
|
||||
$entry = $DB->get_record('glossary_entries', ['id' => $params['itemid']], '*', MUST_EXIST);
|
||||
$glossary = $DB->get_record('glossary', ['id' => $entry->glossaryid], '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('glossary', $glossary->id, $glossary->course, false, MUST_EXIST);
|
||||
$cminfo = get_fast_modinfo($glossary->course)->instances['glossary'][$cm->instance];
|
||||
|
||||
if ($cminfo->context->id != $params['contextid']) {
|
||||
throw new rating_exception('invalidcontext');
|
||||
}
|
||||
|
||||
$context = context::instance_by_id($params['contextid']);
|
||||
|
||||
$ratingpermissions = glossary_rating_permissions($context->id, 'mod_glossary', 'entry');
|
||||
$requiredpermission = ($entry->userid != $USER->id) ? 'viewall' : 'view';
|
||||
if (!$ratingpermissions[$requiredpermission]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!glossary_can_view_entry($entry, $cminfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
This files describes API changes in /mod/glossary/*,
|
||||
information provided here is intended especially for developers.
|
||||
|
||||
=== 4.1.22 ===
|
||||
* Function mod_glossary_rating_can_see_item_ratings is now implemented for checking permissions to view ratings.
|
||||
|
||||
=== 3.10 ===
|
||||
* External function get_entries_by_id now returns and additional "permissions" field indicating the user permissions for managing
|
||||
the entry.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
@mod_quiz @quizaccess @quizaccess_seb
|
||||
Feature: SEB settings in quiz access rule
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student1 | Student | 1 | student1@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
|
||||
Scenario Outline: Require the use of Safe Exam Browser set to Yes – Configure manually shows message to students
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | seb_requiresafeexambrowser | idnumber |
|
||||
| quiz | C1 | Quiz 1 | <sebsetting> | quiz1 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | quiz1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | Reading | Can you read this? |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Reading | 1 |
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
Then I <messagevisibility> see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
|
||||
And "Attempt quiz" "button" <attemptbuttonvisibility> exist
|
||||
|
||||
Examples:
|
||||
| sebsetting | messagevisibility | attemptbuttonvisibility |
|
||||
| 0 | should not | should |
|
||||
| 1 | should | should not |
|
||||
|
||||
Scenario Outline: Show Safe Exam Browser download button setting controls visibility of download link
|
||||
Given the following "activities" exist:
|
||||
| activity | course | name | idnumber | seb_requiresafeexambrowser | seb_showsebdownloadlink |
|
||||
| quiz | C1 | Quiz 1 | quiz1 | 1 | <seb_showsebdownloadlink> |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | quiz1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | Reading | Can you read this? |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Reading | 1 |
|
||||
When I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
Then "Download Safe Exam Browser" "button" <downloadseblinkvisibility> exist
|
||||
And I should see "This quiz has been configured so that students may only attempt it using the Safe Exam Browser."
|
||||
|
||||
Examples:
|
||||
| seb_showsebdownloadlink | downloadseblinkvisibility |
|
||||
| 0 | should not |
|
||||
| 1 | should |
|
||||
@@ -559,4 +559,64 @@ final class attempt_test extends \advanced_testcase {
|
||||
// Check column review.
|
||||
$this->assertMatchesRegularExpression('/<td\b[^>]*>.+?Review<\/a><\/td>/', $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that enabling shuffle on the first quiz section randomizes question order between attempts.
|
||||
*
|
||||
* This test creates a quiz with multiple questions, sets the first section to shuffle,
|
||||
* and verifies that two separate attempts by the same user result in different question orders.
|
||||
*
|
||||
* @return void
|
||||
* @covers ::quiz_start_new_attempt
|
||||
*/
|
||||
public function test_question_shuffle(): void {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Create user and course.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Create a quiz.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$quiz = $quizgenerator->create_instance([
|
||||
'course' => $course->id,
|
||||
'grade' => 100.0,
|
||||
'sumgrades' => 3,
|
||||
]);
|
||||
$quizobj = quiz::create($quiz->id, $user->id);
|
||||
|
||||
// Create ten questions, each on a separate page.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||
quiz_add_quiz_question($question->id, $quiz, $i + 1);
|
||||
}
|
||||
|
||||
// Update the section to be shuffled and set heading.
|
||||
$structure = $quizobj->get_structure();
|
||||
$sections = $structure->get_sections();
|
||||
$firstsection = reset($sections);
|
||||
$structure->set_section_heading($firstsection->id, 'Shuffled section');
|
||||
$structure->set_section_shuffle($firstsection->id, 1);
|
||||
|
||||
// Start two attempts.
|
||||
$this->setUser($user);
|
||||
$attempt1 = quiz_prepare_and_start_new_attempt($quizobj, 1, null, false, [], [], $user->id);
|
||||
$this->setUser($user);
|
||||
$attempt2 = quiz_prepare_and_start_new_attempt($quizobj, 2, null, false, [], [], $user->id);
|
||||
|
||||
$attemptobj1 = quiz_attempt::create($attempt1->id);
|
||||
$attemptobj2 = quiz_attempt::create($attempt2->id);
|
||||
|
||||
$order1 = array_map(function ($slot) use ($attemptobj1) {
|
||||
return $attemptobj1->get_question_attempt($slot)->get_question()->id;
|
||||
}, $attemptobj1->get_slots());
|
||||
|
||||
$order2 = array_map(function ($slot) use ($attemptobj2) {
|
||||
return $attemptobj2->get_question_attempt($slot)->get_question()->id;
|
||||
}, $attemptobj2->get_slots());
|
||||
|
||||
$this->assertNotEquals($order1, $order2, 'Question order should be shuffled between attempts.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
@mod @mod_quiz
|
||||
Feature: Attempt quiz with password and time limit
|
||||
In order to attempt a quiz with password and time limit
|
||||
As a teacher
|
||||
I need to be able to set password and time limit to quiz
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| teacher1 | Teacher | One | teacher1@example.com |
|
||||
| student1 | Student | One | student1@example.com |
|
||||
| student2 | Student | Two | student2@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | quizpassword | browsersecurity | timelimit |
|
||||
| quiz | Quiz 1 | C1 | Passw0rd | securewindow | 60 |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Activity module | Quiz 1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | TF1 | First question |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| TF1 | 1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Attempt password restricted quiz
|
||||
Given I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
When I press "Attempt quiz"
|
||||
# TODO: remove. This is an extra step due to the bug reported in MDL-81377.
|
||||
And I press "Start attempt"
|
||||
# This confirms that quiz is attempted in a separate window.
|
||||
And I switch to a second window
|
||||
# Confirm that the time limit warning is displayed.
|
||||
Then I should see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
|
||||
And I set the field "Quiz password" to "Passw0rd"
|
||||
And I press "Start attempt"
|
||||
# Confirm that quiz timer is displayed when attempting the quiz.
|
||||
And I should see "Time left"
|
||||
|
||||
@javascript
|
||||
Scenario: Attempt quiz after entering incorrect password
|
||||
Given I am on the "Quiz 1" "quiz activity" page logged in as student1
|
||||
When I press "Attempt quiz"
|
||||
# TODO: remove. This is an extra step due to the bug reported in MDL-81377.
|
||||
And I press "Start attempt"
|
||||
# This confirms that quiz is attempted in a separate window.
|
||||
And I switch to a second window
|
||||
# Incorrect password is entered.
|
||||
And I set the field "Quiz password" to "password"
|
||||
And I press "Start attempt"
|
||||
# Confirm that error is displayed.
|
||||
Then I should see "The password entered was incorrect"
|
||||
# Confirm that the time limit warning is displayed.
|
||||
And I should see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
|
||||
# Enter the correct password to attempt quiz.
|
||||
And I set the field "Quiz password" to "Passw0rd"
|
||||
And I press "Start attempt"
|
||||
# Confirm that quiz attempt proceeded.
|
||||
And I should not see "The password entered was incorrect"
|
||||
And I should not see "Your attempt will have a time limit of 1 min. When you start, the timer will begin to count down and cannot be paused. You must finish your attempt before it expires. Are you sure you wish to start now?"
|
||||
And I should see "Time left"
|
||||
@@ -58,7 +58,7 @@ abstract class attempt_walkthrough_testcase extends \advanced_testcase {
|
||||
*/
|
||||
protected static function get_component(): string {
|
||||
// If the late-static class name is namespaced, use the first part of the namespace.
|
||||
if (str_contains(static::class, '\\')) {
|
||||
if (strpos(static::class, '\\') !== false) {
|
||||
return explode('\\', static::class)[0];
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class qtype_essay_question extends question_with_responses {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (str_contains($this->responseformat, 'editor')) {
|
||||
if (strpos($this->responseformat, 'editor') !== false) {
|
||||
return ['answer' => text_to_html($summary), 'answerformat' => FORMAT_HTML];
|
||||
} else {
|
||||
return ['answer' => $summary, 'answerformat' => FORMAT_PLAIN];
|
||||
|
||||
@@ -31,6 +31,7 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
* @package qtype_essay
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \qtype_essay
|
||||
*/
|
||||
final class question_test extends \advanced_testcase {
|
||||
public function test_get_question_summary(): void {
|
||||
@@ -100,6 +101,58 @@ final class question_test extends \advanced_testcase {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test un_summarise_response().
|
||||
*
|
||||
* @dataProvider un_summarise_response_provider
|
||||
* @param string $responseformat
|
||||
* @param string $summary
|
||||
* @param array $expected
|
||||
*/
|
||||
public function test_un_summarise_response(string $responseformat, string $summary, array $expected): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$essay = \test_question_maker::make_an_essay_question();
|
||||
$essay->start_attempt(new question_attempt_step(), 1);
|
||||
|
||||
$essay->responseformat = $responseformat;
|
||||
|
||||
$this->assertEquals($expected, $essay->un_summarise_response($summary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for test_un_summarise_response().
|
||||
*
|
||||
* Covers multiple response formats, empty responses,
|
||||
* case sensitivity, unicode text, and HTML content.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function un_summarise_response_provider(): array {
|
||||
return [
|
||||
'empty summary returns empty array' => [
|
||||
'plain',
|
||||
'',
|
||||
[],
|
||||
],
|
||||
'plain format returns plain text' => [
|
||||
'plain',
|
||||
'Hello Moodle',
|
||||
['answer' => 'Hello Moodle', 'answerformat' => FORMAT_PLAIN],
|
||||
],
|
||||
'editor format converts to HTML' => [
|
||||
'editor',
|
||||
'Hello Moodle',
|
||||
['answer' => text_to_html('Hello Moodle'), 'answerformat' => FORMAT_HTML],
|
||||
],
|
||||
'plain format preserves HTML tags literally' => [
|
||||
'plain',
|
||||
'<b>bold</b>',
|
||||
['answer' => '<b>bold</b>', 'answerformat' => FORMAT_PLAIN],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_is_same_response() {
|
||||
$essay = \test_question_maker::make_an_essay_question();
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ switch ($action) {
|
||||
if (!$file) {
|
||||
echo json_encode(false);
|
||||
} else {
|
||||
$source = unserialize($file->get_source());
|
||||
$source = unserialize_object($file->get_source());
|
||||
$return = array('filename' => $filename, 'filepath' => $filepath, 'references' => array());
|
||||
$browser = get_file_browser();
|
||||
if (isset($source->original)) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
@core @core_filepicker
|
||||
Feature: Cancel file and folder upload
|
||||
In order to cancel file and folder upload
|
||||
As a user
|
||||
I need to be able to close the filepicker
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "blocks" exist:
|
||||
| blockname | contextlevel | reference | pagetypepattern | defaultregion |
|
||||
| private_files | System | 1 | my-index | side-post |
|
||||
And I log in as "admin"
|
||||
And I follow "Dashboard"
|
||||
And I follow "Manage private files"
|
||||
|
||||
@javascript @_file_upload
|
||||
Scenario: Cancel a file upload by pressing close button
|
||||
Given I follow "Add..."
|
||||
And I follow "Upload a file"
|
||||
When I set the field "Attachment" to "#dirroot#/lib/tests/fixtures/empty.txt"
|
||||
And I set the field "Save as" to "empty_upload.txt"
|
||||
And I click on "Close" "button" in the "File picker" "dialogue"
|
||||
Then I should not see "empty_upload.txt"
|
||||
And I should not see "empty.txt"
|
||||
|
||||
@javascript
|
||||
Scenario: Cancel folder creation by pressing close button
|
||||
# Add the folder into the private files repository
|
||||
Given I follow "Create folder"
|
||||
When I set the field "New folder name" to "Test folder 1"
|
||||
And I click on "button.fp-dlg-butcreate" "css_element" in the "div.fp-mkdir-dlg" "css_element"
|
||||
# Ensure that folder is created
|
||||
Then I should see "Test folder 1"
|
||||
# Close the dialogue box using X button instead of saving
|
||||
And I click on "Close" "button" in the "Manage private files" "dialogue"
|
||||
# Confirm folder was not added
|
||||
And I should not see "Test folder 1"
|
||||
+1
-1
@@ -15,4 +15,4 @@ Policy: https://moodledev.io/general/development/process/security
|
||||
|
||||
# Expiry date of this document
|
||||
# This information is considered current and up to date until 3 weeks after the next major Moodle LMS release
|
||||
Expires: 2025-10-27T01:00:00.000Z
|
||||
Expires: 2026-05-11T01:00:00.000Z
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user