Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3828f4b484 | |||
| 4c5288b32c | |||
| 284b201a28 | |||
| 31c078e183 | |||
| 6b8961b7ae | |||
| 7ef79076fe | |||
| 1bf4ac0c11 | |||
| 2c4cc3caf6 | |||
| 3fd34dd999 | |||
| 4dcb3b244c | |||
| 6272227934 | |||
| 36e0a49c00 | |||
| 1ee3d6323c | |||
| 4ed613fbc2 | |||
| 123fc91eea | |||
| 83c9f41a47 | |||
| 3bec78df9b | |||
| 6793732790 | |||
| 9ceb538216 | |||
| 5073d3542b | |||
| 1125231af0 | |||
| 8a147fee4b | |||
| 1d4a1bc258 | |||
| 4119bd99a9 | |||
| 6102f8553a | |||
| 982dda9417 | |||
| e5744719c9 | |||
| 1b513e8c42 | |||
| 1aa9a5b11f | |||
| a313e53b54 | |||
| 983a79347b | |||
| f6a3cbe339 | |||
| cccef66f0c | |||
| ad58a4c3c3 | |||
| 3583300da5 | |||
| 2d9ee56211 | |||
| 5ba22c14a1 | |||
| b3fab1d80e | |||
| d5cd3c95b0 | |||
| 7b841a0f39 | |||
| 25519bc4d8 | |||
| 5f6923adf1 | |||
| a08fcb8d13 | |||
| 9fbbdc8a0a | |||
| 56781dec40 | |||
| 15b31bfb39 | |||
| fc65a2e4a2 | |||
| 3436523f17 | |||
| 8062e43941 | |||
| a28e929a74 | |||
| 838f1bfa7b | |||
| 88a18df37f | |||
| cad3403e71 | |||
| 34b551b4e7 | |||
| a62e36a9f6 | |||
| 7dd9098338 | |||
| a69d81beaa | |||
| 4434756fd6 | |||
| 08e64ba626 | |||
| caa944907a | |||
| 19815c2c2c | |||
| 8de4b14be7 | |||
| 2016d64b83 | |||
| 3eedb8a9c9 | |||
| 7f8d209ba1 | |||
| 3f2ffcce08 | |||
| 18f46b32a2 | |||
| ffb762aae0 | |||
| 9b6a36114c | |||
| 8ef1574c47 | |||
| 09a93cb6eb | |||
| b1fa2a9976 | |||
| 310da5eee7 | |||
| f9e7aa5820 | |||
| 91c1bba220 | |||
| 8984ffef4f | |||
| 419e169fa5 | |||
| 73003c7c96 | |||
| 4619b5128d | |||
| 9a5389830e | |||
| 44cd084562 | |||
| ca6f1479ad | |||
| 2e592749aa | |||
| e261c7ed25 | |||
| 57c492a6a5 | |||
| 442e1f33c9 | |||
| 9cde5a75bb | |||
| 191351d2a3 | |||
| fec4afdbd3 | |||
| 02daea22d5 | |||
| df61870abc | |||
| cb42a0ad6a | |||
| 9b3be75879 | |||
| 1b9df59e3c |
@@ -3539,6 +3539,8 @@
|
||||
</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_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.0" requires="3.6">
|
||||
@@ -3729,6 +3731,200 @@
|
||||
<ON_CHECK message="iscustomadminwarnings" />
|
||||
</FEEDBACK>
|
||||
</CUSTOM_CHECK>
|
||||
<CUSTOM_CHECK file="lib/upgradelib.php" function="check_xmlrpc_usage" level="optional">
|
||||
</CUSTOM_CHECK>
|
||||
</CUSTOM_CHECKS>
|
||||
</MOODLE>
|
||||
<MOODLE version="4.1" requires="3.9">
|
||||
<UNICODE level="required">
|
||||
<FEEDBACK>
|
||||
<ON_ERROR message="unicoderequired" />
|
||||
</FEEDBACK>
|
||||
</UNICODE>
|
||||
<DATABASE level="required">
|
||||
<VENDOR name="mariadb" version="10.4" />
|
||||
<VENDOR name="mysql" version="5.7" />
|
||||
<VENDOR name="postgres" version="12" />
|
||||
<VENDOR name="mssql" version="14.0" />
|
||||
<VENDOR name="oracle" version="19" />
|
||||
</DATABASE>
|
||||
<PHP version="7.4.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="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</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="optional"/>
|
||||
<PHP_EXTENSION name="exif" level="optional"/>
|
||||
</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_SETTINGS>
|
||||
<CUSTOM_CHECKS>
|
||||
<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="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="sixtyfourbitswarning" />
|
||||
</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_CHECKS>
|
||||
</MOODLE>
|
||||
</COMPATIBILITY_MATRIX>
|
||||
|
||||
@@ -7,8 +7,8 @@ Feature: Behat steps for interacting with form work
|
||||
@javascript
|
||||
Scenario: Test fields in containers
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname |
|
||||
| Course 1 | C1 |
|
||||
| fullname | shortname | summary | summaryformat |
|
||||
| Course 1 | C1 | Red | 1 |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
# Just get to any form.
|
||||
@@ -17,11 +17,15 @@ Feature: Behat steps for interacting with form work
|
||||
And I set the following fields in the "Appearance" "fieldset" to these values:
|
||||
| Show activity reports | Yes |
|
||||
| Number of announcements | 1 |
|
||||
And I set the following fields in the "Description" "fieldset" to these values:
|
||||
| Course summary | Green |
|
||||
Then the field "Show activity reports" in the "Appearance" "fieldset" matches value "Yes"
|
||||
And the field "Show activity reports" in the "Appearance" "fieldset" does not match value "No"
|
||||
And the following fields in the "region-main" "region" match these values:
|
||||
| Course full name | Frog |
|
||||
| Number of announcements | 1 |
|
||||
| Course summary | Green |
|
||||
And the following fields in the "region-main" "region" do not match these values:
|
||||
| Course full name | Course 1 |
|
||||
| Number of announcements | 5 |
|
||||
| Course summary | Red |
|
||||
|
||||
@@ -63,4 +63,67 @@ class behat_tool_lp extends behat_base {
|
||||
$this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
|
||||
*
|
||||
* Recognised page names are:
|
||||
* | pagetype | name meaning | description |
|
||||
* | Course competencies | Course name | The course competencies page |
|
||||
*
|
||||
* @param string $page identifies which type of page this is, e.g. 'Course competencies'.
|
||||
* @param string $identifier identifies the particular page, e.g. 'C1'.
|
||||
* @return moodle_url the corresponding URL.
|
||||
* @throws Exception with a meaningful error message if the specified page cannot be found.
|
||||
*/
|
||||
protected function resolve_page_instance_url(string $page, string $identifier): moodle_url {
|
||||
switch (strtolower($page)) {
|
||||
case 'course competencies':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
return new moodle_url('/admin/tool/lp/coursecompetencies.php', [
|
||||
'courseid' => $courseid,
|
||||
]);
|
||||
default:
|
||||
throw new Exception("Unrecognised page type '{$page}'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the exact named selectors for the component.
|
||||
*
|
||||
* @return behat_component_named_selector[]
|
||||
*/
|
||||
public static function get_exact_named_selectors(): array {
|
||||
return [
|
||||
new behat_component_named_selector('competency', [
|
||||
"//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[contains(., //a[@title='View details'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan', [
|
||||
"//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]".
|
||||
"//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]",
|
||||
]),
|
||||
new behat_component_named_selector('competency description', [
|
||||
"//td/p[contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency grade', [
|
||||
"//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan rating', [
|
||||
"//td[position()=2][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('learning plan proficiency', [
|
||||
"//td[position()=3][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page proficiency', [
|
||||
"//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page rating', [
|
||||
"//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]",
|
||||
]),
|
||||
new behat_component_named_selector('competency page related competency', [
|
||||
"//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]",
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
@tool @tool_lp @javascript
|
||||
Feature: View competencies
|
||||
In order to access competency information
|
||||
As a user
|
||||
I need to view user competencies
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname |
|
||||
| student1 | Student | first |
|
||||
| teacher1 | Teacher | first |
|
||||
And the following "system role assigns" exist:
|
||||
| user | role | contextlevel |
|
||||
| teacher1 | editingteacher | System |
|
||||
And the following "permission overrides" exist:
|
||||
| capability | permission | role | contextlevel | reference |
|
||||
| moodle/competency:planview | Allow | editingteacher | System | |
|
||||
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 "scales" exist:
|
||||
| name | scale |
|
||||
| Test Scale | Bad, Good, Great |
|
||||
And the following "core_competency > frameworks" exist:
|
||||
| shortname | idnumber | scale |
|
||||
| Cookery | cookery | Test Scale |
|
||||
| Literacy | literacy | Test Scale |
|
||||
And the following "core_competency > competencies" exist:
|
||||
| shortname | idnumber | description | competencyframework |
|
||||
| Salads | salads | Salads are important | cookery |
|
||||
| Desserts | desserts | Desserts are important | cookery |
|
||||
| Cakes | cakes | Cakes are important | cookery |
|
||||
| Reading | reading | Reading is important | literacy |
|
||||
| Writing | writing | Writing is important | literacy |
|
||||
And the following "core_competency > related_competencies" exist:
|
||||
| competency | relatedcompetency |
|
||||
| desserts | cakes |
|
||||
And the following "core_competency > plans" exist:
|
||||
| name | description | competencies | user |
|
||||
| Cookery | Cookery is important | salads, desserts, cakes | student1 |
|
||||
| Literacy | Literacy is important | reading, writing | student1 |
|
||||
And the following "core_competency > course_competencies" exist:
|
||||
| course | competency |
|
||||
| C1 | salads |
|
||||
| C1 | desserts |
|
||||
| C1 | cakes |
|
||||
| C1 | reading |
|
||||
| C1 | writing |
|
||||
And the following "core_competency > user_competency" exist:
|
||||
| competency | user | grade |
|
||||
| salads | student1 | Good |
|
||||
| desserts | student1 | Great |
|
||||
| cakes | student1 | Great |
|
||||
And the following "core_competency > user_competency_courses" exist:
|
||||
| course | competency | user | grade |
|
||||
| C1 | salads | student1 | Good |
|
||||
| C1 | desserts | student1 | Great |
|
||||
| C1 | cakes | student1 | Great |
|
||||
|
||||
Scenario: Student view
|
||||
# Course competencies
|
||||
Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1"
|
||||
Then I should see "You are proficient in 3 out of 5 competencies in this course"
|
||||
|
||||
And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency"
|
||||
|
||||
And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency"
|
||||
|
||||
And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency"
|
||||
|
||||
And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency"
|
||||
|
||||
And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency"
|
||||
And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency"
|
||||
|
||||
# Course competencies details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Course competencies summary
|
||||
And I click on "Cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I follow "Profile" in the user menu
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes cakes" "link"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
Scenario: Teacher view
|
||||
# Participant competencies
|
||||
Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1"
|
||||
Then I should see "Student first"
|
||||
And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown"
|
||||
And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown"
|
||||
And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown"
|
||||
|
||||
# Participant competencies details
|
||||
And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Participant competencies summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
|
||||
# Participant learning plans
|
||||
And I click on "Close" "button" in the "Cakes" "dialogue"
|
||||
And I click on "Close" "button" in the "User competency summary" "dialogue"
|
||||
And I navigate to course participants
|
||||
And I click on "Student first" "link"
|
||||
And I click on "Learning plans" "link"
|
||||
And I should see "Cookery"
|
||||
And I should see "Literacy"
|
||||
|
||||
# Participant learning plans details
|
||||
And I click on "Cookery" "link"
|
||||
And I should see "Cookery is important"
|
||||
And I should see "3 out of 3 competencies are proficient"
|
||||
|
||||
And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan"
|
||||
|
||||
And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan"
|
||||
|
||||
And I should not see "Literacy"
|
||||
And I should not see "Reading"
|
||||
And I should not see "Writing"
|
||||
|
||||
# Learning plans competency details
|
||||
And I click on "Desserts" "link"
|
||||
And I should see "Desserts are important"
|
||||
And "Yes" "tool_lp > competency page proficiency" should exist
|
||||
And "Great" "tool_lp > competency page rating" should exist
|
||||
|
||||
# Learning plans competency summary
|
||||
And I click on "Cakes" "tool_lp > competency page related competency"
|
||||
And I should see "Cakes are important"
|
||||
@@ -12,9 +12,10 @@ Feature: Restore Moodle 2 course backups
|
||||
| Course 3 | C3 | 0 | topics | 2 | 0 |
|
||||
| Course 4 | C4 | 0 | topics | 20 | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | intro | section |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 |
|
||||
| activity | course | idnumber | name | intro | section | externalurl |
|
||||
| assign | C3 | assign1 | Test assign name | Assign description | 1 | |
|
||||
| data | C3 | data1 | Test database name | Database description | 2 | |
|
||||
| url | C1 | url1 | Test URL name | Test URL description | 3 | http://www.moodle.org |
|
||||
And I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Forum" to section "1" and I fill the form with:
|
||||
@@ -108,11 +109,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -134,11 +131,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course keeping the target course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Confirmation | Filename | test_backup.mbz |
|
||||
@@ -160,11 +153,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and retaining the backup course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -189,11 +178,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents and keeping the current course settings
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
@@ -218,11 +203,7 @@ Feature: Restore Moodle 2 course backups
|
||||
|
||||
@javascript
|
||||
Scenario: Restore a backup in an existing course deleting contents decreasing the number of sections
|
||||
Given I add a "URL" to section "3" and I fill the form with:
|
||||
| Name | Test URL name |
|
||||
| Description | Test URL description |
|
||||
| id_externalurl | http://www.moodle.org |
|
||||
And I hide section "3"
|
||||
Given I hide section "3"
|
||||
And I hide section "7"
|
||||
When I backup "Course 1" course using this options:
|
||||
| Initial | Include enrolled users | 0 |
|
||||
|
||||
@@ -207,7 +207,7 @@ class issued_badge implements renderable {
|
||||
$data->hasrelatedbadges = true;
|
||||
$data->relatedbadges = [];
|
||||
foreach ($relatedbadges as $related) {
|
||||
if (isloggedin() && !is_guest($context)) {
|
||||
if (isloggedin() && ($context instanceof context_course && !is_guest($context))) {
|
||||
$related->url = (new moodle_url('/badges/overview.php', ['id' => $related->id]))->out(false);
|
||||
}
|
||||
$data->relatedbadges[] = (array)$related;
|
||||
|
||||
@@ -30,9 +30,7 @@ Feature: Award badges with separate groups
|
||||
| teacher1 | CB |
|
||||
| student2 | CA |
|
||||
| teacher2 | CA |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
And I am on the "Course 1" "course editing" page logged in as "teacher1"
|
||||
And I expand all fieldsets
|
||||
And I set the field "Group mode" to "Separate groups"
|
||||
And I press "Save and display"
|
||||
|
||||
@@ -119,10 +119,19 @@ class block_news_items extends block_base {
|
||||
if (!empty($CFG->forum_enabletimedposts) && ($discussion->timestart > $posttime)) {
|
||||
$posttime = $discussion->timestart;
|
||||
}
|
||||
|
||||
// If the user who created the discussion post has been deleted, indicate so.
|
||||
if ($discussion->userdeleted) {
|
||||
$userfullname = get_string('deleteduser', 'mod_forum');
|
||||
} else {
|
||||
$userfullname = fullname($discussion, has_capability('moodle/site:viewfullnames', $context));
|
||||
}
|
||||
|
||||
$text .= '<li class="post">'.
|
||||
'<div class="head clearfix">'.
|
||||
'<div class="date">'.userdate($posttime, $strftimerecent).'</div>'.
|
||||
'<div class="name">'.fullname($discussion).'</div></div>'.
|
||||
'<div class="name">'.$userfullname.'</div>'.
|
||||
'</div>'.
|
||||
'<div class="info"><a href="'.$CFG->wwwroot.'/mod/forum/discuss.php?d='.$discussion->discussion.'">'.$discussion->subject.'</a></div>'.
|
||||
"</li>\n";
|
||||
}
|
||||
|
||||
@@ -10,11 +10,18 @@ Feature: Add URL to main menu block
|
||||
And I am on site homepage
|
||||
And I turn editing mode on
|
||||
And I add the "Main menu" block
|
||||
When I add a "URL" to section "0" and I fill the form with:
|
||||
| Name | reference link |
|
||||
| Description | mooooooooodle |
|
||||
| External URL | http://www.moodle.com |
|
||||
| id_display | In pop-up |
|
||||
And the following "activity" exists:
|
||||
| activity | url |
|
||||
| course | Acceptance test site |
|
||||
| name | reference link |
|
||||
| intro | mooooooooodle |
|
||||
| externalurl | http://www.moodle.com |
|
||||
| section | 0 |
|
||||
When I am on the "reference link" "url activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| id_display | In pop-up |
|
||||
And I press "Save and return to course"
|
||||
Then "reference link" "link" should exist in the "Main menu" "block"
|
||||
And "Add an activity or resource" "button" should exist in the "Main menu" "block"
|
||||
|
||||
@@ -33,12 +40,18 @@ Feature: Add URL to main menu block
|
||||
And I set the following fields to these values:
|
||||
| Page contexts | Display throughout the entire site |
|
||||
And I press "Save changes"
|
||||
When I am on the "C1" Course page
|
||||
And I add a "URL" to section "0" and I fill the form with:
|
||||
| Name | reference link |
|
||||
| Description | mooooooooodle |
|
||||
| External URL | http://www.moodle.com |
|
||||
| Display description on course page | 1 |
|
||||
| id_display | In pop-up |
|
||||
When the following "activity" exists:
|
||||
| activity | url |
|
||||
| course | C1 |
|
||||
| name | reference link |
|
||||
| intro | mooooooooodle |
|
||||
| externalurl | http://www.moodle.com |
|
||||
| section | 0 |
|
||||
| showdescription | 1 |
|
||||
And I am on the "reference link" "url activity editing" page
|
||||
And I expand all fieldsets
|
||||
And I set the following fields to these values:
|
||||
| id_display | In pop-up |
|
||||
And I press "Save and return to course"
|
||||
Then "reference link" "link" should not exist in the "Main menu" "block"
|
||||
And I should see "mooooooooodle" in the "region-main" "region"
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ class cachestore_file_addinstance_form extends cachestore_addinstance_form {
|
||||
$form = $this->_form;
|
||||
|
||||
$form->addElement('text', 'path', get_string('path', 'cachestore_file'));
|
||||
$form->setType('path', PARAM_SAFEPATH);
|
||||
$form->setType('path', PARAM_RAW);
|
||||
$form->addHelpButton('path', 'path', 'cachestore_file');
|
||||
|
||||
$form->addElement('checkbox', 'autocreate', get_string('autocreate', 'cachestore_file'));
|
||||
|
||||
Vendored
+1
-1
@@ -5,6 +5,6 @@
|
||||
* @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
define("core_calendar/crud",["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],(function($,Str,Notification,CustomEvents,Modal,ModalRegistry,ModalFactory,ModalEvents,ModalEventForm,CalendarRepository,CalendarEvents,ModalDelete,CalendarSelectors,Pending){return{registerRemove:function(root){root.on("click",CalendarSelectors.actions.remove,(function(e){var eventSource=$(this).closest(CalendarSelectors.eventItem);!function(eventId,eventTitle,eventCount){var deletePromise,pendingPromise=new Pending("core_calendar/crud:confirmDeletion"),deleteStrings=[{key:"deleteevent",component:"calendar"}],isRepeatedEvent=(eventCount=parseInt(eventCount,10))>1;isRepeatedEvent?(deleteStrings.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:eventTitle,count:eventCount}}),deletePromise=ModalFactory.create({type:ModalDelete.TYPE})):(deleteStrings.push({key:"confirmeventdelete",component:"calendar",param:eventTitle}),deletePromise=ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL}));var stringsPromise=Str.get_strings(deleteStrings);$.when(stringsPromise,deletePromise).then((function(strings,deleteModal){return deleteModal.setRemoveOnClose(!0),deleteModal.setTitle(strings[0]),deleteModal.setBody(strings[1]),isRepeatedEvent||deleteModal.setSaveButtonText(strings[0]),deleteModal.show(),deleteModal.getRoot().on(ModalEvents.save,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedevent");CalendarRepository.deleteEvent(eventId,!1).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!1])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal.getRoot().on(CalendarEvents.deleteAll,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedallevent");CalendarRepository.deleteEvent(eventId,!0).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!0])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)}(eventSource.data("eventId"),eventSource.data("eventTitle"),eventSource.data("eventCount")),e.preventDefault()}))},registerEditListeners:function(root,eventFormModalPromise){var pendingPromise=new Pending("core_calendar/crud:registerEditListeners");return eventFormModalPromise.then((function(modal){return $("body").on(CalendarEvents.editEvent,(function(e,eventId){var calendarWrapper=root.find(CalendarSelectors.wrapper);modal.setEventId(eventId),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})),modal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)},registerEventFormModal:function(root){var eventFormPromise=ModalFactory.create({type:ModalEventForm.TYPE,large:!0});return root.on("click",CalendarSelectors.actions.create,(function(e){eventFormPromise.then((function(modal){var wrapper=root.find(CalendarSelectors.wrapper),categoryId=wrapper.data("categoryid");void 0!==categoryId&&modal.setCategoryId(categoryId);var today=root.find(CalendarSelectors.today),firstDay=root.find(CalendarSelectors.day);!today.length&&firstDay.length&&modal.setStartTime(firstDay.data("newEventTimestamp")),modal.setContextId(wrapper.data("contextId")),modal.setCourseId(wrapper.data("courseid")),modal.show()})).fail(Notification.exception),e.preventDefault()})),root.on("click",CalendarSelectors.actions.edit,(function(e){e.preventDefault();var target=$(e.currentTarget),calendarWrapper=target.closest(CalendarSelectors.wrapper),eventWrapper=target.closest(CalendarSelectors.eventItem);eventFormPromise.then((function(modal){modal.setEventId(eventWrapper.data("eventId")),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})).fail(Notification.exception)})),eventFormPromise}}}));
|
||||
define("core_calendar/crud",["jquery","core/str","core/notification","core/custom_interaction_events","core/modal","core/modal_registry","core/modal_factory","core/modal_events","core_calendar/modal_event_form","core_calendar/repository","core_calendar/events","core_calendar/modal_delete","core_calendar/selectors","core/pending"],(function($,Str,Notification,CustomEvents,Modal,ModalRegistry,ModalFactory,ModalEvents,ModalEventForm,CalendarRepository,CalendarEvents,ModalDelete,CalendarSelectors,Pending){return{registerRemove:function(root){root.on("click",CalendarSelectors.actions.remove,(function(e){var eventSource=$(this).closest(CalendarSelectors.eventItem);!function(eventId,eventTitle,eventCount){var deletePromise,pendingPromise=new Pending("core_calendar/crud:confirmDeletion"),deleteStrings=[{key:"deleteevent",component:"calendar"}],isRepeatedEvent=(eventCount=parseInt(eventCount,10))>1;isRepeatedEvent?(deleteStrings.push({key:"confirmeventseriesdelete",component:"calendar",param:{name:eventTitle,count:eventCount}}),deletePromise=ModalFactory.create({type:ModalDelete.TYPE})):(deleteStrings.push({key:"confirmeventdelete",component:"calendar",param:eventTitle}),deletePromise=ModalFactory.create({type:ModalFactory.types.SAVE_CANCEL}));var stringsPromise=Str.get_strings(deleteStrings);$.when(stringsPromise,deletePromise).then((function(strings,deleteModal){return deleteModal.setRemoveOnClose(!0),deleteModal.setTitle(strings[0]),deleteModal.setBody(strings[1]),isRepeatedEvent||deleteModal.setSaveButtonText(strings[0]),deleteModal.show(),deleteModal.getRoot().on(ModalEvents.save,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedevent");CalendarRepository.deleteEvent(eventId,!1).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!1])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal.getRoot().on(CalendarEvents.deleteAll,(function(){var pendingPromise=new Pending("calendar/crud:initModal:deletedallevent");CalendarRepository.deleteEvent(eventId,!0).then((function(){$("body").trigger(CalendarEvents.deleted,[eventId,!0])})).then(pendingPromise.resolve).catch(Notification.exception)})),deleteModal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)}(eventSource.data("eventId"),eventSource.data("eventTitle"),eventSource.data("eventCount")),e.preventDefault()}))},registerEditListeners:function(root,eventFormModalPromise){var pendingPromise=new Pending("core_calendar/crud:registerEditListeners");return eventFormModalPromise.then((function(modal){return $("body").on(CalendarEvents.editEvent,(function(e,eventId){var calendarWrapper=root.find(CalendarSelectors.wrapper);modal.setEventId(eventId),modal.setContextId(calendarWrapper.data("contextId")),modal.show(),e.stopImmediatePropagation()})),modal})).then((function(modal){return pendingPromise.resolve(),modal})).catch(Notification.exception)},registerEventFormModal:function(root){var eventFormPromise=ModalFactory.create({type:ModalEventForm.TYPE,large:!0});return root.on("click",CalendarSelectors.actions.create,(function(e){eventFormPromise.then((function(modal){var wrapper=root.find(CalendarSelectors.wrapper),categoryId=wrapper.data("categoryid");void 0!==categoryId&&modal.setCategoryId(categoryId);var today=root.find(CalendarSelectors.today),firstDay=root.find(CalendarSelectors.day);!today.length&&firstDay.length&&modal.setStartTime(firstDay.data("newEventTimestamp")),modal.setContextId(wrapper.data("contextId")),modal.setCourseId(wrapper.data("courseid")),modal.show()})).fail(Notification.exception),e.preventDefault()})),root.on("click",CalendarSelectors.actions.edit,(function(e){e.preventDefault();var target=$(e.currentTarget),calendarWrapper=target.closest(CalendarSelectors.wrapper),eventWrapper=target.closest(CalendarSelectors.eventItem);eventFormPromise.then((function(modal){modal.setEventId(eventWrapper.data("eventId")),modal.setContextId(calendarWrapper.data("contextId")),modal.setCourseId(eventWrapper.data("courseId")),modal.show(),e.stopImmediatePropagation()})).fail(Notification.exception)})),eventFormPromise}}}));
|
||||
|
||||
//# sourceMappingURL=crud.min.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -202,6 +202,7 @@ function(
|
||||
modal.setEventId(eventWrapper.data('eventId'));
|
||||
|
||||
modal.setContextId(calendarWrapper.data('contextId'));
|
||||
modal.setCourseId(eventWrapper.data('courseId'));
|
||||
modal.show();
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
@@ -109,6 +109,9 @@ class create_update_form_mapper implements create_update_form_mapper_interface {
|
||||
} else {
|
||||
$properties->courseid = $data->courseid;
|
||||
}
|
||||
if (empty($properties->groupid)) {
|
||||
$properties->groupid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the form fields back into valid event property.
|
||||
|
||||
@@ -16,11 +16,13 @@ Feature: Perform basic calendar functionality
|
||||
| Course 1 | C1 | topics |
|
||||
| Course 2 | C2 | topics |
|
||||
| Course 3 | C3 | topics |
|
||||
| Course 4 | C4 | topics |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student1 | C1 | student |
|
||||
| student3 | C1 | student |
|
||||
| teacher1 | C1 | teacher |
|
||||
| admin | C1 | editingteacher |
|
||||
And the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
| Group 1 | C1 | G1 |
|
||||
@@ -195,7 +197,6 @@ Feature: Perform basic calendar functionality
|
||||
Scenario: Admin can only see all courses if calendar_adminseesall setting is enabled.
|
||||
Given I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I enrol "admin" user as "Teacher"
|
||||
And I am viewing site calendar
|
||||
And I click on "New event" "button"
|
||||
And I set the field "Type of event" to "Course"
|
||||
@@ -247,3 +248,69 @@ Feature: Perform basic calendar functionality
|
||||
Then the page should meet accessibility standards
|
||||
And the page should meet "wcag131, wcag143, wcag412" accessibility standards
|
||||
And the page should meet accessibility standards with "wcag131, wcag143, wcag412" extra tests
|
||||
|
||||
@javascript
|
||||
Scenario: Admin can create and edit course events if calendar_adminseesall setting is disabled
|
||||
Given I log in as "admin"
|
||||
And the following config values are set as admin:
|
||||
| calendar_adminseesall | 0 |
|
||||
And I am on "Course 4" course homepage with editing mode on
|
||||
And I add the "Upcoming events" block
|
||||
And I click on "Go to calendar..." "link" in the "Upcoming events" "block"
|
||||
And I click on "New event" "button"
|
||||
And I should see "Course" in the "Type of event" "select"
|
||||
And "Course 4" "autocomplete_selection" should exist
|
||||
And I set the field "Event title" to "Test course event"
|
||||
And I click on "Save" "button"
|
||||
And I should see "Test course event"
|
||||
When I click on "Edit" "link" in the "region-main" "region"
|
||||
Then the field "Event title" matches value "Test course event"
|
||||
And the field "Type of event" matches value "Course"
|
||||
And "Course 4" "autocomplete_selection" should exist
|
||||
|
||||
@javascript
|
||||
Scenario: Changing the event type should clear previous data
|
||||
Given I log in as "admin"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "This month"
|
||||
And I press "New event"
|
||||
And I set the following fields to these values:
|
||||
| Event title | Group 1 event |
|
||||
| Type of event | Group |
|
||||
And I press "Save"
|
||||
And I am on "Course 1" course homepage
|
||||
And I follow "This month"
|
||||
And I click on "Group 1 event" "link"
|
||||
And I should see "Group event"
|
||||
And I should see "Group 1"
|
||||
When I click on "Edit" "button"
|
||||
And I set the following fields to these values:
|
||||
| Event title | My own user event |
|
||||
| Type of event | user |
|
||||
And I press "Save"
|
||||
And I click on "My own user event" "link"
|
||||
Then I should see "User event"
|
||||
And I should not see "Group 1"
|
||||
And I click on "Edit" "button"
|
||||
And I set the following fields to these values:
|
||||
| Event title | Site event |
|
||||
| Type of event | site |
|
||||
And I press "Save"
|
||||
And I click on "Site event" "link"
|
||||
And I should see "Site event"
|
||||
And I click on "Edit" "button"
|
||||
And I set the following fields to these values:
|
||||
| Event title | Course 1 event |
|
||||
| Type of event | course |
|
||||
And I expand the "Course" autocomplete
|
||||
And I click on "Course 1" item in the autocomplete list
|
||||
And I press "Save"
|
||||
And I click on "Course 1 event" "link"
|
||||
And I should see "Course event"
|
||||
And I click on "Edit" "button"
|
||||
And I set the following fields to these values:
|
||||
| Event title | Category event |
|
||||
| Type of event | category |
|
||||
And I press "Save"
|
||||
And I click on "Category event" "link"
|
||||
And I should see "Category event"
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use core_competency\competency;
|
||||
use core_competency\competency_framework;
|
||||
use core_competency\plan;
|
||||
|
||||
/**
|
||||
* Behat data generator for core_competency.
|
||||
*
|
||||
* @package core_competency
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_core_competency_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that Behat can create using the generator step.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'competencies' => [
|
||||
'singular' => 'competency',
|
||||
'datagenerator' => 'competency',
|
||||
'required' => ['shortname', 'competencyframework'],
|
||||
'switchids' => ['competencyframework' => 'competencyframeworkid'],
|
||||
],
|
||||
'course_competencies' => [
|
||||
'singular' => 'course_competency',
|
||||
'datagenerator' => 'course_competency',
|
||||
'required' => ['course', 'competency'],
|
||||
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid'],
|
||||
],
|
||||
'frameworks' => [
|
||||
'singular' => 'framework',
|
||||
'datagenerator' => 'framework',
|
||||
'required' => ['shortname'],
|
||||
'switchids' => ['scale' => 'scaleid'],
|
||||
],
|
||||
'plans' => [
|
||||
'singular' => 'plan',
|
||||
'datagenerator' => 'plan',
|
||||
'required' => ['name'],
|
||||
'switchids' => ['user' => 'userid'],
|
||||
],
|
||||
'related_competencies' => [
|
||||
'singular' => 'related_competency',
|
||||
'datagenerator' => 'related_competency',
|
||||
'required' => ['competency', 'relatedcompetency'],
|
||||
'switchids' => ['competency' => 'competencyid', 'relatedcompetency' => 'relatedcompetencyid'],
|
||||
],
|
||||
'user_competency' => [
|
||||
'singular' => 'user_competency',
|
||||
'datagenerator' => 'user_competency',
|
||||
'required' => ['competency', 'user'],
|
||||
'switchids' => ['competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
'user_competency_courses' => [
|
||||
'singular' => 'user_competency_course',
|
||||
'datagenerator' => 'user_competency_course',
|
||||
'required' => ['course', 'competency', 'user'],
|
||||
'switchids' => ['course' => 'courseid', 'competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
'user_competency_plans' => [
|
||||
'singular' => 'user_competency_plan',
|
||||
'datagenerator' => 'user_competency_plan',
|
||||
'required' => ['plan', 'competency', 'user'],
|
||||
'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy framework id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy framework id
|
||||
*/
|
||||
protected function get_competencyframework_id(string $idnumber): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_framework', 'id', ['idnumber' => $idnumber])) {
|
||||
throw new Exception('The specified competency framework with idnumber "' . $idnumber . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the competecy id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The competecy id
|
||||
*/
|
||||
protected function get_competency_id(string $idnumber): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency', 'id', ['idnumber' => $idnumber])) {
|
||||
throw new Exception('The specified competency with idnumber "' . $idnumber . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the learning plan id using a name.
|
||||
*
|
||||
* @param string $name
|
||||
* @return int The learning plan id
|
||||
*/
|
||||
protected function get_plan_id(string $name): int {
|
||||
global $DB;
|
||||
|
||||
if (!$id = $DB->get_field('competency_plan', 'id', ['name' => $name])) {
|
||||
throw new Exception('The specified learning plan with name "' . $name . '" could not be found.');
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related competecy id using an idnumber.
|
||||
*
|
||||
* @param string $idnumber
|
||||
* @return int The related competecy id
|
||||
*/
|
||||
protected function get_relatedcompetency_id(string $idnumber): int {
|
||||
return $this->get_competency_id($idnumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a plan.
|
||||
*
|
||||
* @param array $data Plan data.
|
||||
*/
|
||||
public function process_plan(array $data): void {
|
||||
$generator = $this->get_data_generator();
|
||||
$competencyids = $data['competencyids'] ?? [];
|
||||
|
||||
unset($data['competencyids']);
|
||||
|
||||
$plan = $generator->create_plan($data);
|
||||
|
||||
foreach ($competencyids as $competencyid) {
|
||||
$generator->create_plan_competency([
|
||||
'planid' => $plan->get('id'),
|
||||
'competencyid' => $competencyid,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user course competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency_course(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess user learning plan competency data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_user_competency_plan(array $data): array {
|
||||
$this->prepare_grading($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess plan data.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array Processed data.
|
||||
*/
|
||||
protected function preprocess_plan(array $data): array {
|
||||
if (isset($data['competencies'])) {
|
||||
$competencies = array_map('trim', str_getcsv($data['competencies']));
|
||||
$data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies);
|
||||
|
||||
unset($data['competencies']);
|
||||
}
|
||||
|
||||
global $USER;
|
||||
|
||||
return $data + [
|
||||
'userid' => $USER->id,
|
||||
'status' => plan::STATUS_ACTIVE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare grading attributes for record data.
|
||||
*
|
||||
* @param array $data Record data.
|
||||
*/
|
||||
protected function prepare_grading(array &$data): void {
|
||||
if (!isset($data['grade'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $DB;
|
||||
|
||||
$competency = competency::get_record(['id' => $data['competencyid']]);
|
||||
$competencyframework = competency_framework::get_record(['id' => $competency->get('competencyframeworkid')]);
|
||||
$scale = $DB->get_field('scale', 'scale', ['id' => $competencyframework->get('scaleid')]);
|
||||
$grades = array_map('trim', explode(',', $scale));
|
||||
$grade = array_search($data['grade'], $grades);
|
||||
|
||||
if ($grade === false) {
|
||||
throw new Exception('The grade "'.$data['grade'].'" was not found in the "'.
|
||||
$competencyframework->get('shortname').'" competency framework.');
|
||||
}
|
||||
|
||||
$data['proficiency'] = true;
|
||||
$data['grade'] = $grade + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the module data generator.
|
||||
*
|
||||
* @return core_competency_generator Competency data generator.
|
||||
*/
|
||||
protected function get_data_generator(): core_competency_generator {
|
||||
return $this->componentdatagenerator;
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,11 @@ class core_competency_generator extends component_generator_base {
|
||||
* @return competency_framework
|
||||
*/
|
||||
public function create_framework($record = null) {
|
||||
$generator = phpunit_util::get_data_generator();
|
||||
if (defined('BEHAT_TEST') && BEHAT_TEST) {
|
||||
$generator = behat_util::get_data_generator();
|
||||
} else {
|
||||
$generator = phpunit_util::get_data_generator();
|
||||
}
|
||||
$this->frameworkcount++;
|
||||
$i = $this->frameworkcount;
|
||||
$record = (object) $record;
|
||||
@@ -584,4 +588,3 @@ class core_competency_generator extends component_generator_base {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
+2
-5
@@ -2477,9 +2477,7 @@ function update_course($data, $editoroptions = NULL) {
|
||||
function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
|
||||
global $DB;
|
||||
|
||||
$params = [
|
||||
'siteid' => SITEID,
|
||||
];
|
||||
$params = [];
|
||||
|
||||
$sql = "SELECT DISTINCT ue.userid, e.courseid
|
||||
FROM {user_enrolments} ue
|
||||
@@ -2490,8 +2488,7 @@ function average_number_of_participants(bool $onlyactive = false, int $lastlogin
|
||||
$sql .= "JOIN {user} u ON u.id = ue.userid ";
|
||||
}
|
||||
|
||||
$sql .= "WHERE e.courseid <> :siteid
|
||||
AND c.visible = 1 ";
|
||||
$sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
|
||||
|
||||
if ($onlyactive) {
|
||||
$sql .= "AND ue.status = :active
|
||||
|
||||
@@ -14,11 +14,11 @@ Feature: Force group mode in a course
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Chat room |
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Chat room |
|
||||
| Description | Chat description |
|
||||
And I navigate to "Edit settings" in current page administration
|
||||
|
||||
@javascript
|
||||
|
||||
@@ -43,15 +43,15 @@ Feature: Course paged mode
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | format | coursedisplay | numsections | startdate |
|
||||
| Course 1 | C1 | 0 | <courseformat> | 1 | 3 | 0 |
|
||||
And I log in as "admin"
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Chat room |
|
||||
When I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
And I click on <section1> "link" in the <section1> "section"
|
||||
And I should see <section1> in the "div.single-section" "css_element"
|
||||
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
|
||||
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
|
||||
When I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Chat room |
|
||||
| Description | Chat description |
|
||||
Then I should see <section1> in the "div.single-section" "css_element"
|
||||
And I should see <section2> in the ".single-section span.mdl-right" "css_element"
|
||||
And I should not see <prevunexistingsection> in the ".single-section" "css_element"
|
||||
|
||||
@@ -14,6 +14,9 @@ Feature: Restrict activities availability
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
And the following "activities" exist:
|
||||
| activity | course | name |
|
||||
| chat | C1 | Test chat name |
|
||||
|
||||
@javascript
|
||||
Scenario: Activities can be added with the default permissions
|
||||
@@ -22,9 +25,6 @@ Feature: Restrict activities availability
|
||||
When I add a "Glossary" to section "1" and I fill the form with:
|
||||
| Name | Test glossary name |
|
||||
| Description | Test glossary description |
|
||||
And I add a "Chat" to section "1" and I fill the form with:
|
||||
| Name of this chat room | Test chat name |
|
||||
| Description | Test chat description |
|
||||
Then I should see "Test glossary name"
|
||||
And I should see "Test chat name"
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ class manage_table extends \table_sql {
|
||||
* @return string
|
||||
*/
|
||||
public function col_name($tool) {
|
||||
$name = helper::get_name($tool);
|
||||
$toolcontext = \context::instance_by_id($tool->contextid, IGNORE_MISSING);
|
||||
$name = $toolcontext ? helper::get_name($tool) : $this->get_deleted_activity_name_html($tool);
|
||||
|
||||
return $this->get_display_text($tool, $name);
|
||||
}
|
||||
@@ -243,9 +244,39 @@ class manage_table extends \table_sql {
|
||||
*/
|
||||
protected function get_display_text($tool, $text) {
|
||||
if ($tool->status != ENROL_INSTANCE_ENABLED) {
|
||||
return \html_writer::tag('span', $text, array('class' => 'dimmed_text'));
|
||||
return \html_writer::tag('div', $text, array('class' => 'dimmed_text'));
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a warning icon, with tooltip, describing enrolment instances sharing activities which have been deleted.
|
||||
*
|
||||
* @param \stdClass $tool the tool instance record.
|
||||
* @return string the HTML for the name column.
|
||||
*/
|
||||
protected function get_deleted_activity_name_html(\stdClass $tool): string {
|
||||
global $OUTPUT;
|
||||
$icon = \html_writer::tag(
|
||||
'a',
|
||||
$OUTPUT->pix_icon('enrolinstancewarning', get_string('deletedactivityalt' , 'enrol_lti'), 'enrol_lti'), [
|
||||
"class" => "btn btn-link p-0",
|
||||
"role" => "button",
|
||||
"data-container" => "body",
|
||||
"data-toggle" => "popover",
|
||||
"data-placement" => right_to_left() ? "left" : "right",
|
||||
"data-content" => get_string('deletedactivitydescription', 'enrol_lti'),
|
||||
"data-html" => "true",
|
||||
"tabindex" => "0",
|
||||
"data-trigger" => "focus"
|
||||
]
|
||||
);
|
||||
$name = \html_writer::span($icon . get_string('deletedactivity', 'enrol_lti'));
|
||||
if ($tool->name) {
|
||||
$name .= \html_writer::empty_tag('br') . \html_writer::empty_tag('br') . $tool->name;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,6 @@ class sync_members extends scheduled_task {
|
||||
/** @var array Array of user photos. */
|
||||
protected $userphotos = [];
|
||||
|
||||
/** @var array Array of current LTI users. */
|
||||
protected $currentusers = [];
|
||||
|
||||
/** @var data_connector $dataconnector A data_connector instance. */
|
||||
protected $dataconnector;
|
||||
|
||||
@@ -111,15 +108,15 @@ class sync_members extends scheduled_task {
|
||||
|
||||
// Fetched members count.
|
||||
$membercount = count($members);
|
||||
$usercount += $membercount;
|
||||
mtrace("$membercount members received.\n");
|
||||
|
||||
// Process member information.
|
||||
list($usercount, $enrolcount) = $this->sync_member_information($tool, $consumer, $members);
|
||||
}
|
||||
list($users, $enrolledcount) = $this->sync_member_information($tool, $consumer, $members);
|
||||
$enrolcount += $enrolledcount;
|
||||
|
||||
// Now we check if we have to unenrol users who were not listed.
|
||||
if ($this->should_sync_unenrol($tool->membersyncmode)) {
|
||||
$unenrolcount = $this->sync_unenrol($tool);
|
||||
// Now sync unenrolments for the consumer.
|
||||
$unenrolcount += $this->sync_unenrol($tool, $consumer->getKey(), $users);
|
||||
}
|
||||
|
||||
mtrace("Completed - Synced members for tool '$tool->id' in the course '$tool->courseid'. " .
|
||||
@@ -216,17 +213,15 @@ class sync_members extends scheduled_task {
|
||||
* @param stdClass $tool
|
||||
* @param ToolConsumer $consumer
|
||||
* @param User[] $members
|
||||
* @return array An array containing the number of members that were processed and the number of members that were enrolled.
|
||||
* @return array An array of users from processed members and the number that were enrolled.
|
||||
*/
|
||||
protected function sync_member_information(stdClass $tool, ToolConsumer $consumer, $members) {
|
||||
global $DB;
|
||||
$usercount = 0;
|
||||
$users = [];
|
||||
$enrolcount = 0;
|
||||
|
||||
// Process member information.
|
||||
foreach ($members as $member) {
|
||||
$usercount++;
|
||||
|
||||
// Set the user data.
|
||||
$user = new stdClass();
|
||||
$user->username = helper::create_username($consumer->getKey(), $member->ltiUserId);
|
||||
@@ -248,9 +243,7 @@ class sync_members extends scheduled_task {
|
||||
user_update_user($user);
|
||||
|
||||
// Add the information to the necessary arrays.
|
||||
if (!in_array($user->id, $this->currentusers)) {
|
||||
$this->currentusers[] = $user->id;
|
||||
}
|
||||
$users[$user->id] = $user;
|
||||
$this->userphotos[$user->id] = $member->image;
|
||||
} else {
|
||||
if ($this->should_sync_enrol($tool->membersyncmode)) {
|
||||
@@ -264,7 +257,7 @@ class sync_members extends scheduled_task {
|
||||
$user->id = user_create_user($user);
|
||||
|
||||
// Add the information to the necessary arrays.
|
||||
$this->currentusers[] = $user->id;
|
||||
$users[$user->id] = $user;
|
||||
$this->userphotos[$user->id] = $member->image;
|
||||
}
|
||||
}
|
||||
@@ -290,16 +283,18 @@ class sync_members extends scheduled_task {
|
||||
}
|
||||
}
|
||||
|
||||
return [$usercount, $enrolcount];
|
||||
return [$users, $enrolcount];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs unenrolment of users that are no longer enrolled in the consumer side.
|
||||
*
|
||||
* @param stdClass $tool The tool record object.
|
||||
* @param string $consumerkey ensure we only unenrol users from this tool consumer.
|
||||
* @param array $currentusers The list of current users.
|
||||
* @return int The number of users that have been unenrolled.
|
||||
*/
|
||||
protected function sync_unenrol(stdClass $tool) {
|
||||
protected function sync_unenrol(stdClass $tool, string $consumerkey, array $currentusers) {
|
||||
global $DB;
|
||||
|
||||
$ltiplugin = enrol_get_plugin('lti');
|
||||
@@ -308,16 +303,18 @@ class sync_members extends scheduled_task {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (empty($this->currentusers)) {
|
||||
if (empty($currentusers)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$unenrolcount = 0;
|
||||
|
||||
$ltiusersrs = $DB->get_recordset('enrol_lti_users', array('toolid' => $tool->id), 'lastaccess DESC', 'userid');
|
||||
$select = "toolid = :toolid AND " . $DB->sql_compare_text('consumerkey', 255) . " = :consumerkey";
|
||||
$ltiusersrs = $DB->get_recordset_select('enrol_lti_users', $select, ['toolid' => $tool->id, 'consumerkey' => $consumerkey],
|
||||
'lastaccess DESC', 'userid');
|
||||
// Go through the users and check if any were never listed, if so, remove them.
|
||||
foreach ($ltiusersrs as $ltiuser) {
|
||||
if (!in_array($ltiuser->userid, $this->currentusers)) {
|
||||
if (!array_key_exists($ltiuser->userid, $currentusers)) {
|
||||
$instance = new stdClass();
|
||||
$instance->id = $tool->enrolid;
|
||||
$instance->courseid = $tool->courseid;
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
* @return boolean
|
||||
*/
|
||||
function xmldb_enrol_lti_upgrade($oldversion) {
|
||||
global $CFG;
|
||||
global $CFG, $DB;
|
||||
|
||||
// Automatically generated Moodle v3.6.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
@@ -57,5 +57,17 @@ function xmldb_enrol_lti_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v3.11.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2021051701) {
|
||||
// Disable all orphaned enrolment method instances.
|
||||
$sql = "id IN (SELECT t.enrolid
|
||||
FROM {enrol_lti_tools} t
|
||||
LEFT JOIN {context} c ON (t.contextid = c.id)
|
||||
WHERE c.id IS NULL)";
|
||||
$DB->set_field_select('enrol', 'status', 1, $sql);
|
||||
|
||||
// Lti savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2021051701, 'enrol', 'lti');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
$string['allowframeembedding'] = 'Note: It is recommended that the site administration setting \'Allow frame embedding\' is enabled, so that tools are displayed within a frame rather than in a new window.';
|
||||
$string['authltimustbeenabled'] = 'Note: This plugin requires the LTI authentication plugin to be enabled too.';
|
||||
$string['couldnotestablishproxy'] = 'Could not establish proxy with consumer.';
|
||||
$string['deletedactivity'] = 'Deleted activity';
|
||||
$string['deletedactivityalt'] = 'The activity shared by this instance has been deleted';
|
||||
$string['deletedactivitydescription'] = 'The activity shared by this instance has been deleted. You can select another activity to share by editing the instance or, if no longer required, you can simply delete the instance. Deleting the instance will remove any associated user enrolments.';
|
||||
$string['enrolenddate'] = 'End date';
|
||||
$string['enrolenddate_help'] = 'If enabled, users can access until this date only.';
|
||||
$string['enrolenddateerror'] = 'Enrolment end date cannot be earlier than start date';
|
||||
|
||||
@@ -415,3 +415,27 @@ function enrol_lti_extend_navigation_course($navigation, $course, $context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon mapping for font-awesome.
|
||||
*/
|
||||
function enrol_lti_get_fontawesome_icon_map() {
|
||||
return [
|
||||
'enrol_lti:enrolinstancewarning' => 'fa-exclamation-circle text-danger',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-delete course module hook which disables any methods referring to the deleted module, preventing launches and allowing remap.
|
||||
*
|
||||
* @param stdClass $cm The deleted course module record.
|
||||
*/
|
||||
function enrol_lti_pre_course_module_delete(stdClass $cm) {
|
||||
global $DB;
|
||||
$sql = "id IN (SELECT t.enrolid
|
||||
FROM {enrol_lti_tools} t
|
||||
JOIN {context} c ON (t.contextid = c.id)
|
||||
WHERE c.contextlevel = :contextlevel
|
||||
AND c.instanceid = :cmid)";
|
||||
$DB->set_field_select('enrol', 'status', ENROL_INSTANCE_DISABLED, $sql, ['contextlevel' => CONTEXT_MODULE, 'cmid' => $cm->id]);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMid meet"><path d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5-103 385.5-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103zm128 1247v-190q0-14-9-23.5t-22-9.5h-192q-13 0-23 10t-10 23v190q0 13 10 23t23 10h192q13 0 22-9.5t9-23.5zm-2-344l18-621q0-12-10-18-10-8-24-8h-220q-14 0-24 8-10 6-10 18l17 621q0 10 10 17.5t24 7.5h185q14 0 23.5-7.5t10.5-17.5z" fill="#999"/></svg>
|
||||
|
After Width: | Height: | Size: 560 B |
@@ -169,4 +169,36 @@ class lib_test extends \advanced_testcase {
|
||||
// LTI enrolment has 1 enrol actions for active users -- unenrol.
|
||||
$this->assertCount(1, $actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the behaviour of an enrolment method when the activity to which it provides access is deleted.
|
||||
*
|
||||
* @covers \enrol_lti_pre_course_module_delete
|
||||
*/
|
||||
public function test_course_module_deletion() {
|
||||
// Create two modules and publish them.
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$mod = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
|
||||
$mod2 = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
|
||||
$tooldata = [
|
||||
'cmid' => $mod->cmid,
|
||||
'courseid' => $course->id,
|
||||
];
|
||||
$tool = $this->getDataGenerator()->create_lti_tool((object)$tooldata);
|
||||
$tooldata['cmid'] = $mod2->cmid;
|
||||
$tool2 = $this->getDataGenerator()->create_lti_tool((object)$tooldata);
|
||||
|
||||
// Verify the instances are both enabled.
|
||||
$modinstance = helper::get_lti_tool($tool->id);
|
||||
$mod2instance = helper::get_lti_tool($tool2->id);
|
||||
$this->assertEquals(ENROL_INSTANCE_ENABLED, $modinstance->status);
|
||||
$this->assertEquals(ENROL_INSTANCE_ENABLED, $mod2instance->status);
|
||||
|
||||
// Delete a module and verify the associated instance is disabled.
|
||||
course_delete_module($mod->cmid);
|
||||
$modinstance = helper::get_lti_tool($tool->id);
|
||||
$mod2instance = helper::get_lti_tool($tool2->id);
|
||||
$this->assertEquals(ENROL_INSTANCE_DISABLED, $modinstance->status);
|
||||
$this->assertEquals(ENROL_INSTANCE_ENABLED, $mod2instance->status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,10 +213,10 @@ class sync_members_test extends \advanced_testcase {
|
||||
* Test for sync_members::sync_member_information().
|
||||
*/
|
||||
public function test_sync_member_information() {
|
||||
list($totalcount, $enrolledcount) = $this->task->sync_member_information($this->tool, $this->consumer, $this->members);
|
||||
list($users, $enrolledcount) = $this->task->sync_member_information($this->tool, $this->consumer, $this->members);
|
||||
$membercount = count($this->members);
|
||||
$this->assertCount(10, $this->members);
|
||||
$this->assertEquals($membercount, $totalcount);
|
||||
$this->assertCount($membercount, $users);
|
||||
$this->assertEquals($membercount, $enrolledcount);
|
||||
}
|
||||
|
||||
@@ -225,10 +225,10 @@ class sync_members_test extends \advanced_testcase {
|
||||
*/
|
||||
public function test_sync_profile_images() {
|
||||
$task = $this->task;
|
||||
list($totalcount, $enrolledcount) = $task->sync_member_information($this->tool, $this->consumer, $this->members);
|
||||
list($users, $enrolledcount) = $task->sync_member_information($this->tool, $this->consumer, $this->members);
|
||||
$membercount = count($this->members);
|
||||
$this->assertCount(10, $this->members);
|
||||
$this->assertEquals($membercount, $totalcount);
|
||||
$this->assertCount($membercount, $users);
|
||||
$this->assertEquals($membercount, $enrolledcount);
|
||||
|
||||
// Suppress output.
|
||||
@@ -244,14 +244,14 @@ class sync_members_test extends \advanced_testcase {
|
||||
$tool = $this->tool;
|
||||
$task = $this->task;
|
||||
|
||||
$task->sync_member_information($tool, $this->consumer, $this->members);
|
||||
list($users) = $task->sync_member_information($tool, $this->consumer, $this->members);
|
||||
|
||||
// Simulate that the fetched list of current users has been reduced by 3.
|
||||
$unenrolcount = 3;
|
||||
for ($i = 0; $i < $unenrolcount; $i++) {
|
||||
$task->pop_current_users();
|
||||
array_pop($users);
|
||||
}
|
||||
$this->assertEquals($unenrolcount, $task->sync_unenrol($tool));
|
||||
$this->assertEquals($unenrolcount, $task->sync_unenrol($tool, 'Consumer1Key', $users));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,13 +297,6 @@ class dummy_sync_members_task extends sync_members {
|
||||
return $this->dataconnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that removes an element in the array of current users.
|
||||
*/
|
||||
public function pop_current_users() {
|
||||
array_pop($this->currentusers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes sync_members::do_context_membership_request()
|
||||
*
|
||||
@@ -390,10 +383,12 @@ class dummy_sync_members_task extends sync_members {
|
||||
* Exposes sync_members::sync_unenrol()
|
||||
*
|
||||
* @param \stdClass $tool
|
||||
* @param string $consumerkey
|
||||
* @param array $currentusers
|
||||
* @return int
|
||||
*/
|
||||
public function sync_unenrol(\stdClass $tool) {
|
||||
$count = parent::sync_unenrol($tool);
|
||||
public function sync_unenrol(\stdClass $tool, string $consumerkey, array $currentusers) {
|
||||
$count = parent::sync_unenrol($tool, $consumerkey, $currentusers);
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2021051700; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2021051701; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2021051100; // Requires this Moodle version.
|
||||
$plugin->component = 'enrol_lti'; // Full name of the plugin (used for diagnostics).
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
.gradingform_guide.editor .addcriterion input,
|
||||
.gradingform_guide.editor .addcomment input {
|
||||
background-position: 5px 8px;
|
||||
background-position: left 5px top 8px;
|
||||
height: 30px;
|
||||
line-height: 29px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
+6
-1
@@ -3050,7 +3050,7 @@ abstract class grade_helper {
|
||||
* @return array
|
||||
*/
|
||||
public static function get_plugins_reports($courseid) {
|
||||
global $SITE;
|
||||
global $SITE, $CFG;
|
||||
|
||||
if (self::$gradereports !== null) {
|
||||
return self::$gradereports;
|
||||
@@ -3064,6 +3064,11 @@ abstract class grade_helper {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove outcomes report if outcomes not enabled.
|
||||
if ($plugin === 'outcomes' && empty($CFG->enableoutcomes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove ones we can't see
|
||||
if (!has_capability('gradereport/'.$plugin.':view', $context)) {
|
||||
continue;
|
||||
|
||||
@@ -39,6 +39,10 @@ $context = context_course::instance($course->id);
|
||||
|
||||
require_capability('gradereport/outcomes:view', $context);
|
||||
|
||||
if (empty($CFG->enableoutcomes)) {
|
||||
redirect(course_get_url($course), get_string('outcomesdisabled', 'core_grades'));
|
||||
}
|
||||
|
||||
// First make sure we have proper final grades.
|
||||
grade_regrade_final_grades_if_required($course);
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ Feature: We can enter in grades and view reports from the gradebook
|
||||
Scenario: Grade a grade item and ensure the results display correctly in the gradebook
|
||||
When I navigate to "View > User report" in the course gradebook
|
||||
And "Grader report" "link" should exist
|
||||
And "Outcomes report" "link" should exist
|
||||
And the "Select all or one user" select box should contain "All users (1)"
|
||||
And I log out
|
||||
And I log in as "student1"
|
||||
|
||||
@@ -277,7 +277,13 @@ class core extends H5PCore {
|
||||
$file->delete();
|
||||
|
||||
$librarykey = static::libraryToString($library);
|
||||
$libraryid = $factory->get_storage()->h5pC->librariesJsonData[$librarykey]["libraryId"];
|
||||
|
||||
$libraryjson = $factory->get_storage()->h5pC->librariesJsonData[$librarykey];
|
||||
if (!array_key_exists('libraryId', $libraryjson)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$libraryid = $libraryjson['libraryId'];
|
||||
|
||||
// Update example and tutorial (if any of them are defined in $library).
|
||||
$params = ['id' => $libraryid];
|
||||
|
||||
@@ -93,11 +93,13 @@ class libraries implements renderable, templatable {
|
||||
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
|
||||
'id' => $version->id,
|
||||
'action' => 'disable',
|
||||
'sesskey' => sesskey(),
|
||||
]);
|
||||
} else {
|
||||
$version->toggleenabledurl = new moodle_url('/h5p/libraries.php', [
|
||||
'id' => $version->id,
|
||||
'action' => 'enable',
|
||||
'sesskey' => sesskey(),
|
||||
]);
|
||||
}
|
||||
$installed[] = $version;
|
||||
|
||||
@@ -66,6 +66,8 @@ if ($deletelibrary) {
|
||||
}
|
||||
|
||||
if (!is_null($action)) {
|
||||
require_sesskey();
|
||||
|
||||
if ($action == 'enable' || $action == 'disable') {
|
||||
// If action is enable or disable, library id is required too.
|
||||
$libraryid = required_param('id', PARAM_INT);
|
||||
|
||||
@@ -31,4 +31,4 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['parentlanguage'] = 'de';
|
||||
$string['thisdirection'] = 'ltr';
|
||||
$string['thislanguage'] = 'Deutsch (Du)';
|
||||
$string['thislanguage'] = 'Deutsch (du)';
|
||||
|
||||
@@ -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 http://docs.moodle.org/dev/Languages/AMOS}) using the
|
||||
* list of strings defined in /install/stringnames.txt.
|
||||
*
|
||||
* @package installer
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['thislanguage'] = 'isiXhosa';
|
||||
@@ -1506,6 +1506,9 @@ $string['warningiconvbuggy'] = 'Your version of the iconv library does not suppo
|
||||
$string['webproxy'] = 'Web proxy';
|
||||
$string['webproxyinfo'] = 'Fill in the following options if your Moodle server cannot access the internet directly. Internet access is required for the download of environment data, language packs, RSS feeds, timezones, etc.<br /><em>The PHP cURL extension is highly recommended.</em>';
|
||||
$string['xmlrpcrecommended'] = 'The XMLRPC extension is useful for web services and Moodle networking.';
|
||||
$string['xmlrpcmaharaenabled'] = 'It has been detected that the Mahara ePortfolio is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['xmlrpcmnetenabled'] = 'It has been detected that the Moodle Networking is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['xmlrpcwebserviceenabled'] = 'It has been detected that the XML-RPC Web Service protocol is enabled on your site. This feature relies on the PHP XML-RPC extension which is no longer maintained by PHP.';
|
||||
$string['yuicomboloading'] = 'YUI combo loading';
|
||||
$string['ziprequired'] = 'The Zip PHP extension is now required by Moodle, info-ZIP binaries or PclZip library are not used anymore.';
|
||||
|
||||
|
||||
@@ -599,6 +599,7 @@ $string['outcomescoursecustom'] = 'Custom used (no remove)';
|
||||
$string['outcomescoursenotused'] = 'Standard not used';
|
||||
$string['outcomescourseused'] = 'Standard used (no remove)';
|
||||
$string['outcomescustom'] = 'Custom outcomes';
|
||||
$string['outcomesdisabled'] = 'Outcomes are not enabled';
|
||||
$string['outcomeshortname'] = 'Short name';
|
||||
$string['outcomesstandard'] = 'Standard outcomes';
|
||||
$string['outcomesstandardavailable'] = 'Available standard outcomes';
|
||||
|
||||
@@ -944,8 +944,11 @@ EOF;
|
||||
}
|
||||
|
||||
} else {
|
||||
$errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
|
||||
$this->get_debug_text($errorinfoboxes[1]->getHtml());
|
||||
$errorinfo = implode("\n", [
|
||||
$this->get_debug_text($errorinfoboxes[0]->getHtml()),
|
||||
$this->get_debug_text($errorinfoboxes[1]->getHtml()),
|
||||
html_to_text($errorinfoboxes[2]->find('css', 'ul')->getHtml()),
|
||||
]);
|
||||
}
|
||||
|
||||
$msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
|
||||
|
||||
@@ -55,8 +55,9 @@ class mustache_shorten_text_helper {
|
||||
$length = trim($length);
|
||||
$text = trim($text);
|
||||
|
||||
// Allow mustache tags in the text.
|
||||
// Allow mustache tags in the length and text.
|
||||
$text = $helper->render($text);
|
||||
$length = $helper->render($length);
|
||||
|
||||
return shorten_text($text, $length);
|
||||
}
|
||||
|
||||
@@ -274,6 +274,25 @@ trait moodle_read_slave_trait {
|
||||
$this->select_db_handle($type, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* This should be called immediately after each db query. It does a clean up of resources.
|
||||
*
|
||||
* @param mixed $result The db specific result obtained from running a query.
|
||||
* @return void
|
||||
*/
|
||||
protected function query_end($result) {
|
||||
if ($this->written) {
|
||||
// Adjust the written time.
|
||||
array_walk($this->written, function (&$val) {
|
||||
if ($val === true) {
|
||||
$val = microtime(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parent::query_end($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select appropriate db handle - readwrite or readonly
|
||||
* @param int $type type of query
|
||||
@@ -339,10 +358,8 @@ trait moodle_read_slave_trait {
|
||||
return true;
|
||||
case SQL_QUERY_INSERT:
|
||||
case SQL_QUERY_UPDATE:
|
||||
// If we are in transaction we cannot set the written time yet.
|
||||
$now = $this->slavelatency && !$this->transactions ? microtime(true) : true;
|
||||
foreach ($this->table_names($sql) as $tablename) {
|
||||
$this->written[$tablename] = $now;
|
||||
$this->written[$tablename] = true;
|
||||
}
|
||||
return false;
|
||||
case SQL_QUERY_STRUCTURE:
|
||||
@@ -370,16 +387,10 @@ trait moodle_read_slave_trait {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->slavelatency) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = null;
|
||||
foreach ($this->written as $tablename => $when) {
|
||||
if ($when === true) {
|
||||
$now = $now ?: microtime(true);
|
||||
$this->written[$tablename] = $now;
|
||||
}
|
||||
$now = $now ?: microtime(true);
|
||||
$this->written[$tablename] = $now;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class pgsql_native_moodle_database extends moodle_database {
|
||||
select_db_handle as read_slave_select_db_handle;
|
||||
can_use_readonly as read_slave_can_use_readonly;
|
||||
query_start as read_slave_query_start;
|
||||
query_end as read_slave_query_end;
|
||||
}
|
||||
|
||||
/** @var array $dbhcursor keep track of open cursors */
|
||||
@@ -184,12 +185,20 @@ class pgsql_native_moodle_database extends moodle_database {
|
||||
}
|
||||
|
||||
ob_start();
|
||||
if (empty($this->dboptions['dbpersist'])) {
|
||||
$this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
|
||||
} else {
|
||||
$this->pgsql = pg_pconnect($connection, PGSQL_CONNECT_FORCE_NEW);
|
||||
// It seems that pg_connect() handles some errors differently.
|
||||
// For example, name resolution error will raise an exception, and non-existing
|
||||
// database or wrong credentials will just return false.
|
||||
// We need to cater for both.
|
||||
try {
|
||||
if (empty($this->dboptions['dbpersist'])) {
|
||||
$this->pgsql = pg_connect($connection, PGSQL_CONNECT_FORCE_NEW);
|
||||
} else {
|
||||
$this->pgsql = pg_pconnect($connection, PGSQL_CONNECT_FORCE_NEW);
|
||||
}
|
||||
$dberr = ob_get_contents();
|
||||
} catch (\Exception $e) {
|
||||
$dberr = $e->getMessage();
|
||||
}
|
||||
$dberr = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
$status = $this->pgsql ? pg_connection_status($this->pgsql) : false;
|
||||
@@ -326,7 +335,7 @@ class pgsql_native_moodle_database extends moodle_database {
|
||||
// reset original debug level
|
||||
error_reporting($this->last_error_reporting);
|
||||
try {
|
||||
parent::query_end($result);
|
||||
$this->read_slave_query_end($result);
|
||||
if ($this->savepointpresent and $this->last_type != SQL_QUERY_AUX and $this->last_type != SQL_QUERY_SELECT) {
|
||||
$res = @pg_query($this->pgsql, "RELEASE SAVEPOINT moodle_pg_savepoint; SAVEPOINT moodle_pg_savepoint");
|
||||
if ($res) {
|
||||
|
||||
@@ -34,8 +34,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_mysqli.php');
|
||||
* @category dml
|
||||
* @copyright 2018 Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mysqli_native_moodle_database
|
||||
*/
|
||||
class core_dml_mysqli_read_slave_testcase extends base_testcase {
|
||||
class dml_mysqli_read_slave_test extends base_testcase {
|
||||
/**
|
||||
* Test readonly handle is not used for reading from special pg_*() call queries,
|
||||
* pg_try_advisory_lock and pg_advisory_unlock.
|
||||
|
||||
@@ -34,8 +34,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_mock_pgsql.php');
|
||||
* @category dml
|
||||
* @copyright 2018 Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \pgsql_native_moodle_database
|
||||
*/
|
||||
class core_dml_pgsql_read_slave_testcase extends base_testcase {
|
||||
class dml_pgsql_read_slave_test extends base_testcase {
|
||||
/**
|
||||
* Test correct database handles are used for cursors
|
||||
*
|
||||
|
||||
@@ -35,8 +35,9 @@ require_once(__DIR__.'/fixtures/read_slave_moodle_database_special.php');
|
||||
* @category dml
|
||||
* @copyright 2018 Catalyst IT
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \moodle_read_slave_trait
|
||||
*/
|
||||
class core_dml_read_slave_testcase extends base_testcase {
|
||||
class dml_read_slave_test extends base_testcase {
|
||||
|
||||
/** @var float */
|
||||
static private $dbreadonlylatency = 0.8;
|
||||
@@ -311,6 +312,50 @@ class core_dml_read_slave_testcase extends base_testcase {
|
||||
$this->assert_readonly_handle($handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test readonly handle is not used immediately after update
|
||||
* Test last written time is adjusted post-write,
|
||||
* so the latency parameter is applied properly.
|
||||
*
|
||||
* @return void
|
||||
* @covers ::can_use_readonly
|
||||
* @covers ::query_end
|
||||
*/
|
||||
public function test_long_update(): void {
|
||||
$DB = $this->new_db(true);
|
||||
|
||||
$this->assertNull($DB->get_dbhwrite());
|
||||
|
||||
$skip = false;
|
||||
|
||||
list($sql, $params, $ptype) = $DB->fix_sql_params("UPDATE {table} SET a = 1 WHERE id = 1");
|
||||
$DB->with_query_start_end($sql, $params, SQL_QUERY_UPDATE, function ($dbh) use (&$now) {
|
||||
sleep(1);
|
||||
$now = microtime(true);
|
||||
});
|
||||
|
||||
// This condition should always evaluate true, however we need to
|
||||
// safeguard from an unaccounted delay that can break this test.
|
||||
if (microtime(true) - $now < self::$dbreadonlylatency) {
|
||||
// Not enough time passed, use rw handle.
|
||||
$handle = $DB->get_records_sql("SELECT * FROM {table}");
|
||||
$this->assertEquals('test_rw::test:test', $handle);
|
||||
|
||||
// Make sure enough time passes.
|
||||
sleep(1);
|
||||
} else {
|
||||
$skip = true;
|
||||
}
|
||||
|
||||
// Exceeded latency time, use ro handle.
|
||||
$handle = $DB->get_records_sql("SELECT * FROM {table}");
|
||||
$this->assert_readonly_handle($handle);
|
||||
|
||||
if ($skip) {
|
||||
$this->markTestSkipped("Delay too long to test write handle immediately after transaction");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test failed readonly connection falls back to write connection.
|
||||
*
|
||||
|
||||
+5
-1
@@ -91,11 +91,15 @@ class read_slave_moodle_database extends test_moodle_database {
|
||||
* @param string $sql
|
||||
* @param array $params
|
||||
* @param int $querytype
|
||||
* @param ?callable $callback
|
||||
* @return string $handle handle property
|
||||
*/
|
||||
private function with_query_start_end($sql, array $params = null, $querytype) {
|
||||
public function with_query_start_end($sql, array $params = null, $querytype, $callback = null) {
|
||||
$this->query_start($sql, $params, $querytype);
|
||||
$ret = $this->handle;
|
||||
if ($callback) {
|
||||
call_user_func($callback, $ret);
|
||||
}
|
||||
$this->query_end(null);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ trait test_moodle_read_slave_trait {
|
||||
$ro = fopen("php://memory", 'r+');
|
||||
fputs($ro, 'ro');
|
||||
|
||||
$this->prefix = 'test_'; // Default, not to leave empty.
|
||||
$this->wantreadslave = true;
|
||||
$this->dbhwrite = $rw;
|
||||
$this->dbhreadonly = $ro;
|
||||
@@ -109,6 +110,7 @@ trait test_moodle_read_slave_trait {
|
||||
* @param mixed $result
|
||||
*/
|
||||
public function query_end($result) {
|
||||
parent::query_end($result);
|
||||
$this->set_db_handle($this->dbhwrite);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,22 @@ Feature: Atto edit HTML
|
||||
And I click on "Show more buttons" "button"
|
||||
And I click on "HTML" "button"
|
||||
Then the field "Description" matches value "<p style=\"color: blue;\">Smurf</p>"
|
||||
|
||||
@javascript
|
||||
Scenario: Validation of empty string when the form is submitted with HTML source mode.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | format |
|
||||
| Course 1 | C1 | weeks |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | essay | Essay 01 new | Write about whatever you want |
|
||||
And I am on the "Essay 01 new" "core_question > edit" page logged in as admin
|
||||
And I click on "Show more buttons" "button" in the "Question text" "form_row"
|
||||
And I click on "HTML" "button" in the "Question text" "form_row"
|
||||
And I press the shift + end key
|
||||
And I press the delete key
|
||||
When I press "id_submitbutton"
|
||||
Then I should see "You must supply a value here." in the "Question text" "form_row"
|
||||
|
||||
Vendored
+7
@@ -132,6 +132,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
_hideCodeMirror: function() {
|
||||
var host = this.get('host');
|
||||
|
||||
// Disable placeholder for empty content.
|
||||
// This will prevent the editor to copy and submit the empty placeholder.
|
||||
host.disablePlaceholderForEmptyContent();
|
||||
|
||||
// Enable all plugins.
|
||||
host.enablePlugins();
|
||||
|
||||
@@ -164,6 +168,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
|
||||
// And re-mark everything as updated.
|
||||
this.markUpdated();
|
||||
|
||||
// Enable placeholder for empty content.
|
||||
host.enablePlaceholderForEmptyContent();
|
||||
}
|
||||
}, {
|
||||
ATTRS: {
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
YUI.add("moodle-atto_html-button",function(o,t){o.namespace("M.atto_html").Button=o.Base.create("button",o.M.editor_atto.EditorPlugin,[],{_codeMirror:null,initializer:function(){this.addButton({icon:"e/source_code",callback:this._toggleHTML});var t=this.get("host").textarea.ancestor("form");t&&t.on("submit",function(){this.get("isHTML")&&this._hideCodeMirror()},this)},_toggleHTML:function(){this.set("isHTML",!this.get("isHTML")),this.get("isHTML")?this._showCodeMirror():this._hideCodeMirror()},_showCodeMirror:function(){var t,e,i=this.get("host");i.disablePlugins(),i.enablePlugins(this.name),i.updateOriginal(),t={width:this.editor.getComputedStyle("width"),height:this.editor.getComputedStyle("height")},e=o.M.atto_html.beautify.html_beautify(i.textarea.get("value"),{indent_size:4,indent_inner_html:!0}),i.textarea.set("value",e),this._codeMirror=o.M.atto_html.CodeMirror.fromTextArea(i.textarea.getDOMNode(),{lineNumbers:!0,mode:"htmlmixed",tabSize:2,lineWrapping:!0}),this._codeMirror.on("change",function(t){t.save()}),this.editor.hide(),this._codeMirror.setSize(t.width,t.height),this._codeMirror.focus()},_hideCodeMirror:function(){var t=this.get("host");t.enablePlugins(),t.updateFromTextArea(),this._codeMirror&&(o.one(this._codeMirror.getWrapperElement()).hide(),this._codeMirror.toTextArea(),o.Object.each(this._codeMirror._handlers,function(t,e){o.Array.each(t,function(t){this.off(e,t)},this)},this._codeMirror),delete this._codeMirror,this._codeMirror=null),t.textarea.hide(),this.editor.show(),t.focus(),this.markUpdated()}},{ATTRS:{isHTML:{value:!1}}})},"@VERSION@",{requires:["promise","moodle-editor_atto-plugin","moodle-atto_html-beautify","moodle-atto_html-codemirror","event-valuechange"]});
|
||||
YUI.add("moodle-atto_html-button",function(o,t){o.namespace("M.atto_html").Button=o.Base.create("button",o.M.editor_atto.EditorPlugin,[],{_codeMirror:null,initializer:function(){this.addButton({icon:"e/source_code",callback:this._toggleHTML});var t=this.get("host").textarea.ancestor("form");t&&t.on("submit",function(){this.get("isHTML")&&this._hideCodeMirror()},this)},_toggleHTML:function(){this.set("isHTML",!this.get("isHTML")),this.get("isHTML")?this._showCodeMirror():this._hideCodeMirror()},_showCodeMirror:function(){var t,e,i=this.get("host");i.disablePlugins(),i.enablePlugins(this.name),i.updateOriginal(),t={width:this.editor.getComputedStyle("width"),height:this.editor.getComputedStyle("height")},e=o.M.atto_html.beautify.html_beautify(i.textarea.get("value"),{indent_size:4,indent_inner_html:!0}),i.textarea.set("value",e),this._codeMirror=o.M.atto_html.CodeMirror.fromTextArea(i.textarea.getDOMNode(),{lineNumbers:!0,mode:"htmlmixed",tabSize:2,lineWrapping:!0}),this._codeMirror.on("change",function(t){t.save()}),this.editor.hide(),this._codeMirror.setSize(t.width,t.height),this._codeMirror.focus()},_hideCodeMirror:function(){var t=this.get("host");t.disablePlaceholderForEmptyContent(),t.enablePlugins(),t.updateFromTextArea(),this._codeMirror&&(o.one(this._codeMirror.getWrapperElement()).hide(),this._codeMirror.toTextArea(),o.Object.each(this._codeMirror._handlers,function(t,e){o.Array.each(t,function(t){this.off(e,t)},this)},this._codeMirror),delete this._codeMirror,this._codeMirror=null),t.textarea.hide(),this.editor.show(),t.focus(),this.markUpdated(),t.enablePlaceholderForEmptyContent()}},{ATTRS:{isHTML:{value:!1}}})},"@VERSION@",{requires:["promise","moodle-editor_atto-plugin","moodle-atto_html-beautify","moodle-atto_html-codemirror","event-valuechange"]});
|
||||
+7
@@ -132,6 +132,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
_hideCodeMirror: function() {
|
||||
var host = this.get('host');
|
||||
|
||||
// Disable placeholder for empty content.
|
||||
// This will prevent the editor to copy and submit the empty placeholder.
|
||||
host.disablePlaceholderForEmptyContent();
|
||||
|
||||
// Enable all plugins.
|
||||
host.enablePlugins();
|
||||
|
||||
@@ -164,6 +168,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
|
||||
// And re-mark everything as updated.
|
||||
this.markUpdated();
|
||||
|
||||
// Enable placeholder for empty content.
|
||||
host.enablePlaceholderForEmptyContent();
|
||||
}
|
||||
}, {
|
||||
ATTRS: {
|
||||
|
||||
@@ -130,6 +130,10 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
_hideCodeMirror: function() {
|
||||
var host = this.get('host');
|
||||
|
||||
// Disable placeholder for empty content.
|
||||
// This will prevent the editor to copy and submit the empty placeholder.
|
||||
host.disablePlaceholderForEmptyContent();
|
||||
|
||||
// Enable all plugins.
|
||||
host.enablePlugins();
|
||||
|
||||
@@ -162,6 +166,9 @@ Y.namespace('M.atto_html').Button = Y.Base.create('button', Y.M.editor_atto.Edit
|
||||
|
||||
// And re-mark everything as updated.
|
||||
this.markUpdated();
|
||||
|
||||
// Enable placeholder for empty content.
|
||||
host.enablePlaceholderForEmptyContent();
|
||||
}
|
||||
}, {
|
||||
ATTRS: {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
This files describes API changes in the editor_atto code.
|
||||
|
||||
=== 3.11 ===
|
||||
|
||||
* Two new helper functions have been added to lib/editor/atto
|
||||
to handle enable/disable the empty placeholder for empty content in the HTML source view:
|
||||
- enablePlaceholderForEmptyContent() - Handle to enable the empty placeholder for empty content
|
||||
- disablePlaceholderForEmptyContent() - Handle to disable the empty placeholder for empty content.
|
||||
|
||||
* One property have been added to lib/editor/atto to handle the state of the empty placeholder content.
|
||||
- enableAppropriateEmptyContent - Enable/disable the empty placeholder content.
|
||||
|
||||
=== 3.4 ===
|
||||
|
||||
* The following functions, previously used (exclusively) by upgrade steps are not available
|
||||
|
||||
+31
-1
@@ -158,6 +158,14 @@ Y.extend(Editor, Y.Base, {
|
||||
*/
|
||||
coreDirection: null,
|
||||
|
||||
/**
|
||||
* Enable/disable the empty placeholder content.
|
||||
*
|
||||
* @property enableAppropriateEmptyContent
|
||||
* @type Boolean
|
||||
*/
|
||||
enableAppropriateEmptyContent: null,
|
||||
|
||||
/**
|
||||
* Event Handles to clear on editor destruction.
|
||||
*
|
||||
@@ -207,6 +215,9 @@ Y.extend(Editor, Y.Base, {
|
||||
// Set diretcion according to current page language.
|
||||
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
|
||||
|
||||
// Enable the placeholder for empty content.
|
||||
this.enablePlaceholderForEmptyContent();
|
||||
|
||||
// Add everything to the wrapper.
|
||||
this.setupToolbar();
|
||||
|
||||
@@ -420,6 +431,20 @@ Y.extend(Editor, Y.Base, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable the empty placeholder for empty content.
|
||||
*/
|
||||
enablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable the empty placeholder for empty content.
|
||||
*/
|
||||
disablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register an event handle for disposal in the destructor.
|
||||
*
|
||||
@@ -692,6 +717,10 @@ EditorTextArea.prototype = {
|
||||
* @private
|
||||
*/
|
||||
_getEmptyContent: function() {
|
||||
if (!this.enableAppropriateEmptyContent) {
|
||||
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
|
||||
return '';
|
||||
}
|
||||
var alignment;
|
||||
if (this.coreDirection === 'rtl') {
|
||||
alignment = 'style="text-align: right;"';
|
||||
@@ -1793,7 +1822,8 @@ EditorClean.prototype = {
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
|
||||
+5
-5
File diff suppressed because one or more lines are too long
+31
-1
@@ -158,6 +158,14 @@ Y.extend(Editor, Y.Base, {
|
||||
*/
|
||||
coreDirection: null,
|
||||
|
||||
/**
|
||||
* Enable/disable the empty placeholder content.
|
||||
*
|
||||
* @property enableAppropriateEmptyContent
|
||||
* @type Boolean
|
||||
*/
|
||||
enableAppropriateEmptyContent: null,
|
||||
|
||||
/**
|
||||
* Event Handles to clear on editor destruction.
|
||||
*
|
||||
@@ -205,6 +213,9 @@ Y.extend(Editor, Y.Base, {
|
||||
// Set diretcion according to current page language.
|
||||
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
|
||||
|
||||
// Enable the placeholder for empty content.
|
||||
this.enablePlaceholderForEmptyContent();
|
||||
|
||||
// Add everything to the wrapper.
|
||||
this.setupToolbar();
|
||||
|
||||
@@ -417,6 +428,20 @@ Y.extend(Editor, Y.Base, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable the empty placeholder for empty content.
|
||||
*/
|
||||
enablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable the empty placeholder for empty content.
|
||||
*/
|
||||
disablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register an event handle for disposal in the destructor.
|
||||
*
|
||||
@@ -687,6 +712,10 @@ EditorTextArea.prototype = {
|
||||
* @private
|
||||
*/
|
||||
_getEmptyContent: function() {
|
||||
if (!this.enableAppropriateEmptyContent) {
|
||||
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
|
||||
return '';
|
||||
}
|
||||
var alignment;
|
||||
if (this.coreDirection === 'rtl') {
|
||||
alignment = 'style="text-align: right;"';
|
||||
@@ -1780,7 +1809,8 @@ EditorClean.prototype = {
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
|
||||
+2
-1
@@ -498,7 +498,8 @@ EditorClean.prototype = {
|
||||
|
||||
// Use a regular expression to find the next open or close li, ul, or ol tag.
|
||||
// Keep going until there are no more matching tags left.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)[^>]*>/i))) {
|
||||
// This expression looks for whole words by employing the word boundary (\b) metacharacter.
|
||||
while ((match = toProcess.match(/<(\/?)(li|ul|ol)\b[^>]*>/i))) {
|
||||
currentTag = {
|
||||
tag: match[2],
|
||||
tagLowerCase: match[2].toLowerCase(),
|
||||
|
||||
+25
@@ -156,6 +156,14 @@ Y.extend(Editor, Y.Base, {
|
||||
*/
|
||||
coreDirection: null,
|
||||
|
||||
/**
|
||||
* Enable/disable the empty placeholder content.
|
||||
*
|
||||
* @property enableAppropriateEmptyContent
|
||||
* @type Boolean
|
||||
*/
|
||||
enableAppropriateEmptyContent: null,
|
||||
|
||||
/**
|
||||
* Event Handles to clear on editor destruction.
|
||||
*
|
||||
@@ -205,6 +213,9 @@ Y.extend(Editor, Y.Base, {
|
||||
// Set diretcion according to current page language.
|
||||
this.coreDirection = Y.one('body').hasClass('dir-rtl') ? 'rtl' : 'ltr';
|
||||
|
||||
// Enable the placeholder for empty content.
|
||||
this.enablePlaceholderForEmptyContent();
|
||||
|
||||
// Add everything to the wrapper.
|
||||
this.setupToolbar();
|
||||
|
||||
@@ -418,6 +429,20 @@ Y.extend(Editor, Y.Base, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable the empty placeholder for empty content.
|
||||
*/
|
||||
enablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable the empty placeholder for empty content.
|
||||
*/
|
||||
disablePlaceholderForEmptyContent: function() {
|
||||
this.enableAppropriateEmptyContent = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Register an event handle for disposal in the destructor.
|
||||
*
|
||||
|
||||
@@ -45,6 +45,10 @@ EditorTextArea.prototype = {
|
||||
* @private
|
||||
*/
|
||||
_getEmptyContent: function() {
|
||||
if (!this.enableAppropriateEmptyContent) {
|
||||
// Return the empty string if we do not enable the empty placeholder. Ex: HTML mode.
|
||||
return '';
|
||||
}
|
||||
var alignment;
|
||||
if (this.coreDirection === 'rtl') {
|
||||
alignment = 'style="text-align: right;"';
|
||||
|
||||
+4
-4
@@ -670,8 +670,8 @@ function enrol_get_my_courses($fields = null, $sort = null, $limit = 0, $coursei
|
||||
$orderby = "ORDER BY $sort";
|
||||
}
|
||||
|
||||
$wheres = array("c.id <> :siteid");
|
||||
$params = array('siteid'=>SITEID);
|
||||
$wheres = ['c.id <> ' . SITEID];
|
||||
$params = [];
|
||||
|
||||
if (isset($USER->loginascontext) and $USER->loginascontext->contextlevel == CONTEXT_COURSE) {
|
||||
// list _only_ this course - anything else is asking for trouble...
|
||||
@@ -1075,7 +1075,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = nul
|
||||
$orderby = "ORDER BY $sort";
|
||||
}
|
||||
|
||||
$params = array('siteid'=>SITEID);
|
||||
$params = [];
|
||||
|
||||
if ($onlyactive) {
|
||||
$subwhere = "WHERE ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
|
||||
@@ -1101,7 +1101,7 @@ function enrol_get_all_users_courses($userid, $onlyactive = false, $fields = nul
|
||||
$subwhere
|
||||
) en ON (en.courseid = c.id)
|
||||
$ccjoin
|
||||
WHERE c.id <> :siteid
|
||||
WHERE c.id <> " . SITEID . "
|
||||
$orderby";
|
||||
$params['userid'] = $userid;
|
||||
|
||||
|
||||
+2
-2
@@ -4162,14 +4162,14 @@ class curl {
|
||||
$crlf = "\r\n";
|
||||
return preg_replace(
|
||||
// HTTP version and status code (ignore value of code).
|
||||
'~^HTTP/1\..*' . $crlf .
|
||||
'~^HTTP/[1-9](\.[0-9])?.*' . $crlf .
|
||||
// Header name: character between 33 and 126 decimal, except colon.
|
||||
// Colon. Header value: any character except \r and \n. CRLF.
|
||||
'(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
|
||||
// Headers are terminated by another CRLF (blank line).
|
||||
$crlf .
|
||||
// Second HTTP status code, this time must be 200.
|
||||
'(HTTP/1.[01] 200 )~', '$1', $input);
|
||||
'(HTTP/[1-9](\.[0-9])? 200)~', '$2', $input);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-13
@@ -206,7 +206,10 @@ define('PARAM_RAW_TRIMMED', 'raw_trimmed');
|
||||
define('PARAM_SAFEDIR', 'safedir');
|
||||
|
||||
/**
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths, etc.
|
||||
* PARAM_SAFEPATH - several PARAM_SAFEDIR joined by "/", suitable for include() and require(), plugin paths
|
||||
* and other references to Moodle code files.
|
||||
*
|
||||
* This is NOT intended to be used for absolute paths or any user uploaded files.
|
||||
*/
|
||||
define('PARAM_SAFEPATH', 'safepath');
|
||||
|
||||
@@ -983,18 +986,8 @@ function clean_param($param, $type) {
|
||||
return preg_replace('/[^a-zA-Z0-9_-]/i', '', $param);
|
||||
|
||||
case PARAM_SAFEPATH:
|
||||
// Replace MS \ separators.
|
||||
$param = str_replace('\\', '/', $param);
|
||||
// Remove any number of ../ to prevent path traversal.
|
||||
$param = preg_replace('/\.\.+\//', '', $param);
|
||||
// Remove everything not a-zA-Z0-9/:_- .
|
||||
$param = preg_replace('/[^a-zA-Z0-9\/:_-]/i', '', $param);
|
||||
// Remove leading slash.
|
||||
$param = ltrim($param, '/');
|
||||
if ($param === '.') {
|
||||
$param = '';
|
||||
}
|
||||
return $param;
|
||||
// Remove everything not a-zA-Z0-9/_- .
|
||||
return preg_replace('/[^a-zA-Z0-9\/_-]/i', '', $param);
|
||||
|
||||
case PARAM_FILE:
|
||||
// Strip all suspicious characters from filename.
|
||||
|
||||
@@ -17,9 +17,3 @@ Local changes:
|
||||
Note: All this changes need to be reviewed on every upgrade and, if they have
|
||||
been already applied upstream for the release being used, can be removed
|
||||
from the list. If still not available upstream, they will need to be re-applied.
|
||||
|
||||
- MDL-67114: PHP 7.4 compatibility. Array operations on scalar value.
|
||||
This corresponds to upstream https://github.com/bobthecow/mustache.php/pull/352
|
||||
- MDL-73586: PHP 8.0 compatibility. Removed 'mbstring.func_overload' init setting.
|
||||
This corresponds to upstream commit https://github.com/bobthecow/mustache.php/commit/e7165a33b282ab4d20b3863825caadb46313d62b
|
||||
that is availbale for the library versions 2.14.1 and up
|
||||
|
||||
@@ -33,4 +33,11 @@ interface Mustache_Cache
|
||||
* @param string $value
|
||||
*/
|
||||
public function cache($key, $value);
|
||||
|
||||
/**
|
||||
* Set a logger instance.
|
||||
*
|
||||
* @param Mustache_Logger|Psr\Log\LoggerInterface $logger
|
||||
*/
|
||||
public function setLogger($logger = null);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ abstract class Mustache_Cache_AbstractCache implements Mustache_Cache
|
||||
/**
|
||||
* Add a log record if logging is enabled.
|
||||
*
|
||||
* @param int $level The logging level
|
||||
* @param string $level The logging level
|
||||
* @param string $message The log message
|
||||
* @param array $context The log context
|
||||
*/
|
||||
|
||||
@@ -286,7 +286,6 @@ class Mustache_Compiler
|
||||
private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level)
|
||||
{
|
||||
$key = $this->block($nodes);
|
||||
$keystr = var_export($key, true);
|
||||
$id = var_export($id, true);
|
||||
|
||||
return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key);
|
||||
@@ -321,7 +320,6 @@ class Mustache_Compiler
|
||||
}
|
||||
|
||||
const SECTION_CALL = '
|
||||
// %s section
|
||||
$value = $context->%s(%s);%s
|
||||
$buffer .= $this->section%s($context, $indent, $value);
|
||||
';
|
||||
@@ -333,12 +331,12 @@ class Mustache_Compiler
|
||||
|
||||
if (%s) {
|
||||
$source = %s;
|
||||
$result = call_user_func($value, $source, %s);
|
||||
$result = (string) call_user_func($value, $source, %s);
|
||||
if (strpos($result, \'{{\') === false) {
|
||||
$buffer .= $result;
|
||||
} else {
|
||||
$buffer .= $this->mustache
|
||||
->loadLambda((string) $result%s)
|
||||
->loadLambda($result%s)
|
||||
->renderInternal($context);
|
||||
}
|
||||
} elseif (!empty($value)) {
|
||||
@@ -392,11 +390,10 @@ class Mustache_Compiler
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key);
|
||||
return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key);
|
||||
}
|
||||
|
||||
const INVERTED_SECTION = '
|
||||
// %s inverted section
|
||||
$value = $context->%s(%s);%s
|
||||
if (empty($value)) {
|
||||
%s
|
||||
@@ -419,7 +416,7 @@ class Mustache_Compiler
|
||||
$id = var_export($id, true);
|
||||
$filters = $this->getFilters($filters, $level);
|
||||
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level));
|
||||
return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level));
|
||||
}
|
||||
|
||||
const PARTIAL_INDENT = ', $indent . %s';
|
||||
@@ -507,7 +504,7 @@ class Mustache_Compiler
|
||||
|
||||
const VARIABLE = '
|
||||
$value = $this->resolveValue($context->%s(%s), $context);%s
|
||||
$buffer .= %s%s;
|
||||
$buffer .= %s($value === null ? \'\' : %s);
|
||||
';
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
*/
|
||||
class Mustache_Engine
|
||||
{
|
||||
const VERSION = '2.13.0';
|
||||
const SPEC_VERSION = '1.1.2';
|
||||
const VERSION = '2.14.0';
|
||||
const SPEC_VERSION = '1.2.2';
|
||||
|
||||
const PRAGMA_FILTERS = 'FILTERS';
|
||||
const PRAGMA_BLOCKS = 'BLOCKS';
|
||||
|
||||
@@ -38,7 +38,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
/**
|
||||
* Get the Source key (used to generate the compiled class name).
|
||||
*
|
||||
* @throws RuntimeException when a source file cannot be read
|
||||
* @throws Mustache_Exception_RuntimeException when a source file cannot be read
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
@@ -54,7 +54,7 @@ class Mustache_Source_FilesystemSource implements Mustache_Source
|
||||
}
|
||||
|
||||
if ($this->stat === false) {
|
||||
throw new RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName));
|
||||
}
|
||||
|
||||
foreach ($this->statProps as $prop) {
|
||||
|
||||
@@ -88,11 +88,11 @@ class Mustache_Tokenizer
|
||||
* @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid
|
||||
*
|
||||
* @param string $text Mustache template source to tokenize
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
|
||||
* @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string)
|
||||
*
|
||||
* @return array Set of Mustache tokens
|
||||
*/
|
||||
public function scan($text, $delimiters = null)
|
||||
public function scan($text, $delimiters = '')
|
||||
{
|
||||
// Setting mbstring.func_overload makes things *really* slow.
|
||||
// Let's do everyone a favor and scan this string as ASCII instead.
|
||||
@@ -112,7 +112,7 @@ class Mustache_Tokenizer
|
||||
|
||||
$this->reset();
|
||||
|
||||
if ($delimiters = trim($delimiters)) {
|
||||
if (is_string($delimiters) && $delimiters = trim($delimiters)) {
|
||||
$this->setDelimiters($delimiters);
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -943,12 +943,25 @@ class theme_config {
|
||||
|
||||
// First editor plugins.
|
||||
$plugins = core_component::get_plugin_list('editor');
|
||||
foreach ($plugins as $plugin=>$fulldir) {
|
||||
foreach ($plugins as $plugin => $fulldir) {
|
||||
$sheetfile = "$fulldir/editor_styles.css";
|
||||
if (is_readable($sheetfile)) {
|
||||
$files['plugin_'.$plugin] = $sheetfile;
|
||||
}
|
||||
|
||||
$subplugintypes = core_component::get_subplugins("editor_{$plugin}");
|
||||
// Fetch sheets for any editor subplugins.
|
||||
foreach ($subplugintypes as $plugintype => $subplugins) {
|
||||
foreach ($subplugins as $subplugin) {
|
||||
$plugindir = core_component::get_plugin_directory($plugintype, $subplugin);
|
||||
$sheetfile = "{$plugindir}/editor_styles.css";
|
||||
if (is_readable($sheetfile)) {
|
||||
$files["{$plugintype}_{$subplugin}"] = $sheetfile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then parent themes - base first, the immediate parent last.
|
||||
foreach (array_reverse($this->parent_configs) as $parent_config) {
|
||||
if (empty($parent_config->editor_sheets)) {
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<input type="hidden" name="logintoken" value="{{logintoken}}">
|
||||
<input type="hidden" name="username" value="guest" />
|
||||
<input type="hidden" name="password" value="guest" />
|
||||
<button class="btn btn-secondary btn-block" type="submit">{{#str}}loginguest{{/str}}</button>
|
||||
<button class="btn btn-secondary btn-block" type="submit" id="loginguestbtn">{{#str}}loginguest{{/str}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{/canloginasguest}}
|
||||
@@ -262,4 +262,8 @@
|
||||
});
|
||||
{{/autofocusform}}
|
||||
{{/error}}
|
||||
require(['core_form/submit'], function(Submit) {
|
||||
Submit.init("loginbtn");
|
||||
Submit.init("loginguestbtn");
|
||||
});
|
||||
{{/js}}
|
||||
|
||||
@@ -651,7 +651,6 @@ class behat_forms extends behat_base {
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_field_node_value(NodeElement $fieldnode, string $value): void {
|
||||
$this->ensure_node_is_visible($fieldnode);
|
||||
$field = behat_field_manager::get_form_field($fieldnode, $this->getSession());
|
||||
$field->set_value($value);
|
||||
}
|
||||
|
||||
@@ -1922,6 +1922,21 @@ EOF;
|
||||
throw new \Moodle\BehatExtension\Exception\SkippedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given plugin is installed, and skips the current scenario if not.
|
||||
*
|
||||
* @Given the :plugin plugin is installed
|
||||
* @param string $plugin frankenstyle plugin name, e.g. 'filter_embedquestion'.
|
||||
* @throws \Moodle\BehatExtension\Exception\SkippedException
|
||||
*/
|
||||
public function plugin_is_installed(string $plugin): void {
|
||||
$path = core_component::get_component_directory($plugin);
|
||||
if (!is_readable($path . '/version.php')) {
|
||||
throw new \Moodle\BehatExtension\Exception\SkippedException(
|
||||
'Skipping this scenario because the ' . $plugin . ' is not installed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks focus is with the given element.
|
||||
*
|
||||
|
||||
@@ -718,6 +718,7 @@ class behat_navigation extends behat_base {
|
||||
* | Page type | Identifier meaning | description |
|
||||
* | Category | category idnumber | List of courses in that category. |
|
||||
* | Course | course shortname | Main course home pag |
|
||||
* | Course editing | course shortname | Edit settings page for the course |
|
||||
* | Activity | activity idnumber | Start page for that activity |
|
||||
* | Activity editing | activity idnumber | Edit settings page for that activity |
|
||||
* | [modname] Activity | activity name or idnumber | Start page for that activity |
|
||||
@@ -745,6 +746,14 @@ class behat_navigation extends behat_base {
|
||||
}
|
||||
return new moodle_url('/course/index.php', ['categoryid' => $categoryid]);
|
||||
|
||||
case 'course editing':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
if (!$courseid) {
|
||||
throw new Exception('The specified course with shortname, fullname, or idnumber "' .
|
||||
$identifier . '" does not exist');
|
||||
}
|
||||
return new moodle_url('/course/edit.php', ['id' => $courseid]);
|
||||
|
||||
case 'course':
|
||||
$courseid = $this->get_course_id($identifier);
|
||||
if (!$courseid) {
|
||||
|
||||
@@ -954,6 +954,155 @@ EOF;
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
|
||||
// Test it does nothing to the 'plain' data.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
|
||||
|
||||
$httpsexample = <<<EOF
|
||||
HTTP/1.0 200 Connection established
|
||||
|
||||
HTTP/2 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
$httpsexpected = <<<EOF
|
||||
HTTP/2 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
// For HTTP, replace the \n with \r\n.
|
||||
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
|
||||
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
|
||||
|
||||
// Test stripping works OK.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
|
||||
// Test it does nothing to the 'plain' data.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
|
||||
|
||||
$httpsexample = <<<EOF
|
||||
HTTP/1.0 200 Connection established
|
||||
|
||||
HTTP/2.1 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
$httpsexpected = <<<EOF
|
||||
HTTP/2.1 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
// For HTTP, replace the \n with \r\n.
|
||||
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
|
||||
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
|
||||
|
||||
// Test stripping works OK.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
|
||||
// Test it does nothing to the 'plain' data.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
|
||||
|
||||
$httpsexample = <<<EOF
|
||||
HTTP/1.1 200 Connection established
|
||||
|
||||
HTTP/3 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
$httpsexpected = <<<EOF
|
||||
HTTP/3 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
// For HTTP, replace the \n with \r\n.
|
||||
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
|
||||
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
|
||||
|
||||
// Test stripping works OK.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
|
||||
// Test it does nothing to the 'plain' data.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
|
||||
|
||||
$httpsexample = <<<EOF
|
||||
HTTP/2 200 Connection established
|
||||
|
||||
HTTP/4 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
$httpsexpected = <<<EOF
|
||||
HTTP/4 200 OK
|
||||
Date: Fri, 22 Feb 2013 17:14:23 GMT
|
||||
Server: Apache/2
|
||||
X-Powered-By: PHP/5.3.3-7+squeeze14
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Content-Encoding: gzip
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<rss version="2.0">...
|
||||
EOF;
|
||||
// For HTTP, replace the \n with \r\n.
|
||||
$httpsexample = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexample);
|
||||
$httpsexpected = preg_replace("~(?!<\r)\n~", "\r\n", $httpsexpected);
|
||||
|
||||
// Test stripping works OK.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexample));
|
||||
// Test it does nothing to the 'plain' data.
|
||||
$this->assertSame($httpsexpected, curl::strip_double_headers($httpsexpected));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -837,40 +837,6 @@ class core_moodlelib_testcase extends advanced_testcase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide some tested base url and expected results.
|
||||
*
|
||||
* @return array Array of tested base url and expected results.
|
||||
*/
|
||||
public function clean_param_safepath_provider(): array {
|
||||
return [
|
||||
// MS separator test.
|
||||
['c:\temp', 'c:/temp'],
|
||||
|
||||
// Leading slash test.
|
||||
['/tmp/', 'tmp/'],
|
||||
|
||||
// Path traversal test.
|
||||
['../../../../../etc/', 'etc/'],
|
||||
['../', ''],
|
||||
['.../...//', ''],
|
||||
['.', '']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clean_param() method with PARAM_SAFEPATH type.
|
||||
*
|
||||
* @dataProvider clean_param_safepath_provider
|
||||
* @covers ::clean_param
|
||||
* @param string $path
|
||||
* @param string $expected
|
||||
*/
|
||||
public function test_clean_param_safepath(string $path, string $expected) {
|
||||
$result = clean_param($path, PARAM_SAFEPATH);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
public function test_validate_param() {
|
||||
try {
|
||||
$param = validate_param('11a', PARAM_INT);
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
<location>mustache</location>
|
||||
<name>Mustache</name>
|
||||
<license>MIT</license>
|
||||
<version>2.13.0</version>
|
||||
<version>2.14.1</version>
|
||||
</library>
|
||||
<library>
|
||||
<location>amd/src/mustache.js</location>
|
||||
|
||||
@@ -2726,3 +2726,55 @@ function check_admin_dir_usage(environment_results $result): ?environment_result
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the XML-RPC protocol is enabled and warn if so.
|
||||
*
|
||||
* The XML-RPC protocol will be removed in a future version (4.1) as it is no longer supported by PHP.
|
||||
*
|
||||
* See MDL-70889 for further information.
|
||||
*
|
||||
* @param environment_results $result
|
||||
* @return null|environment_results
|
||||
*/
|
||||
function check_xmlrpc_usage(environment_results $result): ?environment_results {
|
||||
global $CFG;
|
||||
|
||||
// Checking Web Service protocols.
|
||||
if (!empty($CFG->webserviceprotocols)) {
|
||||
$plugins = array_flip(explode(',', $CFG->webserviceprotocols));
|
||||
if (array_key_exists('xmlrpc', $plugins)) {
|
||||
$result->setInfo('xmlrpc_webservice_usage');
|
||||
$result->setFeedbackStr('xmlrpcwebserviceenabled');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($CFG->mnet_dispatcher_mode) && $CFG->mnet_dispatcher_mode == 'strict') {
|
||||
// Checking Mnet hosts.
|
||||
$mnethosts = mnet_get_hosts();
|
||||
if ($mnethosts) {
|
||||
$actualhost = 0;
|
||||
foreach ($mnethosts as $mnethost) {
|
||||
if ($mnethost->id != $CFG->mnet_all_hosts_id) {
|
||||
$actualhost++;
|
||||
}
|
||||
}
|
||||
if ($actualhost > 0) {
|
||||
$result->setInfo('xmlrpc_mnet_usage');
|
||||
$result->setFeedbackStr('xmlrpcmnetenabled');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Checking Mahara.
|
||||
$portfolios = \core\plugininfo\portfolio::get_enabled_plugins();
|
||||
if (array_key_exists('mahara', $portfolios)) {
|
||||
$result->setInfo('xmlrpc_mahara_usage');
|
||||
$result->setFeedbackStr('xmlrpcmaharaenabled');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+1
-1
@@ -205,7 +205,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
|
||||
// Handle TinyMCE editor instances
|
||||
// We can't add a listener in the initializer as the editors may not have been created by that point
|
||||
// so we do so here instead
|
||||
if (typeof window.tinyMCE !== 'undefined') {
|
||||
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
|
||||
for (editor in window.tinyMCE.editors) {
|
||||
if (window.tinyMCE.editors[editor].isDirty()) {
|
||||
return 1;
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
YUI.add("moodle-core-formchangechecker",function(r,e){var t=function(){t.superclass.constructor.apply(this,arguments)};r.extend(t,r.Base,{initialvaluelisteners:[],initializer:function(){var e="form#"+this.get("formid"),t=r.one(e);t&&(M.core_formchangechecker.stateinformation.formchanged||(M.core_formchangechecker.stateinformation.formchanged=this.get("initialdirtystate")),r.on(M.core.event.EDITOR_CONTENT_RESTORED,M.core_formchangechecker.reset_form_dirty_state,this),t.delegate("change",M.core_formchangechecker.set_form_changed,"input",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"textarea",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"select",this),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"input",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"textarea",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"select",this)),t.delegate("click",function(){t.setData("ignoreSubmission",!0)},"[data-formchangechecker-ignore-submit]"),r.one(e).on("submit",function(){t.getData("ignoreSubmission")?t.clearData("ignoreSubmission"):M.core_formchangechecker.set_form_submitted()},this),window.onbeforeunload=M.core_formchangechecker.report_form_dirty_state)},store_initial_value:function(e){if(!e.target.ancestor(".ignoredirty",!0)&&!e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))if(M.core_formchangechecker.get_form_dirty_state())for(;this.initialvaluelisteners.length;)this.initialvaluelisteners.shift().detach();else M.core_formchangechecker.stateinformation.focused_element={element:e.target,initial_value:e.target.get("value")}}},{NAME:"core-formchangechecker",ATTRS:{formid:{value:""},initialdirtystate:{value:!1}}}),M.core_formchangechecker=M.core_formchangechecker||{},M.core_formchangechecker.instances=M.core_formchangechecker.instances||[],M.core_formchangechecker.init=function(e){e=new t(e);return M.core_formchangechecker.instances.push(e),e},M.core_formchangechecker.stateinformation=[],M.core_formchangechecker.set_form_changed=function(e){e&&e.target&&(e.target.ancestor(".ignoredirty",!0)||e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))||(M.core_formchangechecker.stateinformation.formchanged=1,delete M.core_formchangechecker.stateinformation.focused_element)},M.core_formchangechecker.set_form_submitted=function(){M.core_formchangechecker.stateinformation.formsubmitted=1},M.core_formchangechecker.get_form_dirty_state=function(){var e,t=M.core_formchangechecker.stateinformation;if(t.formsubmitted)return 0;if(t.formchanged)return 1;if(t.focused_element&&t.focused_element.element.get("value")!==t.focused_element.initial_value)return 1;if("undefined"!=typeof window.tinyMCE)for(e in window.tinyMCE.editors)if(window.tinyMCE.editors[e].isDirty())return 1;return 0},M.core_formchangechecker.reset_form_dirty_state=function(){M.core_formchangechecker.stateinformation.formsubmitted=!1,M.core_formchangechecker.stateinformation.formchanged=!1},M.core_formchangechecker.report_form_dirty_state=function(e){if(M.core_formchangechecker.get_form_dirty_state()){var t=M.util.get_string("changesmadereallygoaway","moodle");if(!M.cfg.behatsiterunning)return e&&(e.returnValue=t),t}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
|
||||
YUI.add("moodle-core-formchangechecker",function(r,e){var t=function(){t.superclass.constructor.apply(this,arguments)};r.extend(t,r.Base,{initialvaluelisteners:[],initializer:function(){var e="form#"+this.get("formid"),t=r.one(e);t&&(M.core_formchangechecker.stateinformation.formchanged||(M.core_formchangechecker.stateinformation.formchanged=this.get("initialdirtystate")),r.on(M.core.event.EDITOR_CONTENT_RESTORED,M.core_formchangechecker.reset_form_dirty_state,this),t.delegate("change",M.core_formchangechecker.set_form_changed,"input",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"textarea",this),t.delegate("change",M.core_formchangechecker.set_form_changed,"select",this),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"input",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"textarea",this)),this.initialvaluelisteners.push(t.delegate("focus",this.store_initial_value,"select",this)),t.delegate("click",function(){t.setData("ignoreSubmission",!0)},"[data-formchangechecker-ignore-submit]"),r.one(e).on("submit",function(){t.getData("ignoreSubmission")?t.clearData("ignoreSubmission"):M.core_formchangechecker.set_form_submitted()},this),window.onbeforeunload=M.core_formchangechecker.report_form_dirty_state)},store_initial_value:function(e){if(!e.target.ancestor(".ignoredirty",!0)&&!e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))if(M.core_formchangechecker.get_form_dirty_state())for(;this.initialvaluelisteners.length;)this.initialvaluelisteners.shift().detach();else M.core_formchangechecker.stateinformation.focused_element={element:e.target,initial_value:e.target.get("value")}}},{NAME:"core-formchangechecker",ATTRS:{formid:{value:""},initialdirtystate:{value:!1}}}),M.core_formchangechecker=M.core_formchangechecker||{},M.core_formchangechecker.instances=M.core_formchangechecker.instances||[],M.core_formchangechecker.init=function(e){e=new t(e);return M.core_formchangechecker.instances.push(e),e},M.core_formchangechecker.stateinformation=[],M.core_formchangechecker.set_form_changed=function(e){e&&e.target&&(e.target.ancestor(".ignoredirty",!0)||e.target.ancestor("[data-formchangechecker-ignore-dirty]",!0))||(M.core_formchangechecker.stateinformation.formchanged=1,delete M.core_formchangechecker.stateinformation.focused_element)},M.core_formchangechecker.set_form_submitted=function(){M.core_formchangechecker.stateinformation.formsubmitted=1},M.core_formchangechecker.get_form_dirty_state=function(){var e,t=M.core_formchangechecker.stateinformation;if(t.formsubmitted)return 0;if(t.formchanged)return 1;if(t.focused_element&&t.focused_element.element.get("value")!==t.focused_element.initial_value)return 1;if("undefined"!=typeof window.tinyMCE&&window.tinyMCE.editors)for(e in window.tinyMCE.editors)if(window.tinyMCE.editors[e].isDirty())return 1;return 0},M.core_formchangechecker.reset_form_dirty_state=function(){M.core_formchangechecker.stateinformation.formsubmitted=!1,M.core_formchangechecker.stateinformation.formchanged=!1},M.core_formchangechecker.report_form_dirty_state=function(e){if(M.core_formchangechecker.get_form_dirty_state()){var t=M.util.get_string("changesmadereallygoaway","moodle");if(!M.cfg.behatsiterunning)return e&&(e.returnValue=t),t}}},"@VERSION@",{requires:["base","event-focus","moodle-core-event"]});
|
||||
+1
-1
@@ -205,7 +205,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
|
||||
// Handle TinyMCE editor instances
|
||||
// We can't add a listener in the initializer as the editors may not have been created by that point
|
||||
// so we do so here instead
|
||||
if (typeof window.tinyMCE !== 'undefined') {
|
||||
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
|
||||
for (editor in window.tinyMCE.editors) {
|
||||
if (window.tinyMCE.editors[editor].isDirty()) {
|
||||
return 1;
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ M.core_formchangechecker.get_form_dirty_state = function() {
|
||||
// Handle TinyMCE editor instances
|
||||
// We can't add a listener in the initializer as the editors may not have been created by that point
|
||||
// so we do so here instead
|
||||
if (typeof window.tinyMCE !== 'undefined') {
|
||||
if (typeof window.tinyMCE !== 'undefined' && window.tinyMCE.editors) {
|
||||
for (editor in window.tinyMCE.editors) {
|
||||
if (window.tinyMCE.editors[editor].isDirty()) {
|
||||
return 1;
|
||||
|
||||
@@ -11,11 +11,14 @@ Feature: Embed videos without the media filter
|
||||
|
||||
@javascript
|
||||
Scenario: Add a video in a URL resource. Make sure media filters work
|
||||
When I add a "URL" to section "1" and I fill the form with:
|
||||
| Name | Video URL |
|
||||
| Description | Example of a video url |
|
||||
| External URL | http://download.moodle.org/mediatest/quicktime_320_180.mov |
|
||||
And I follow "Video URL"
|
||||
Given the following "activity" exists:
|
||||
| activity | url |
|
||||
| course | Acceptance test site |
|
||||
| name | Video URL |
|
||||
| intro | Example of a video url |
|
||||
| externalurl | http://download.moodle.org/mediatest/quicktime_320_180.mov |
|
||||
| section | 1 |
|
||||
When I am on the "Video URL" "url activity" page
|
||||
Then ".video-js" "css_element" should exist
|
||||
And I am on site homepage
|
||||
|
||||
|
||||
@@ -455,6 +455,7 @@ class assign_submission_onlinetext extends assign_submission_plugin {
|
||||
public function view(stdClass $submission) {
|
||||
global $CFG;
|
||||
$result = '';
|
||||
$plagiarismlinks = '';
|
||||
|
||||
$onlinetextsubmission = $this->get_onlinetext_submission($submission->id);
|
||||
|
||||
@@ -467,8 +468,6 @@ class assign_submission_onlinetext extends assign_submission_plugin {
|
||||
'onlinetext',
|
||||
'assignsubmission_onlinetext');
|
||||
|
||||
$plagiarismlinks = '';
|
||||
|
||||
if (!empty($CFG->enableplagiarism)) {
|
||||
require_once($CFG->libdir . '/plagiarismlib.php');
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ abstract class feedback_item_base {
|
||||
* Prepares the value for exporting to Excel
|
||||
*
|
||||
* @param object $item the db-object from feedback_item
|
||||
* @param string $value a item-related value from feedback_values
|
||||
* @param object $value object with item-related value from feedback_values in the 'value' property
|
||||
* @return string
|
||||
*/
|
||||
abstract public function get_printval($item, $value);
|
||||
|
||||
@@ -50,43 +50,28 @@ Feature: Feedbacks in courses with groups
|
||||
| feedback | Site feedback | Acceptance test site | feedback0 | 2 | 1 | 1 | 1 |
|
||||
| feedback | Course feedback | C1 | feedback1 | 2 | 1 | 1 | 0 |
|
||||
| feedback | Course anon feedback | C1 | feedback2 | 1 | 1 | 1 | 0 |
|
||||
And I am on the "Site feedback" "feedback activity" page logged in as manager
|
||||
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
|
||||
And I add a "Multiple choice" question to the feedback with:
|
||||
| Question | Do you like our site? |
|
||||
| Label | multichoice2 |
|
||||
| Multiple choice type | Multiple choice - single answer |
|
||||
| Hide the "Not selected" option | Yes |
|
||||
| Multiple choice values | Yes of course\nNot at all\nI don't know |
|
||||
And I log out
|
||||
|
||||
@javascript
|
||||
Scenario: Non anonymous feedback with groups in a course
|
||||
Given I am on the "Course feedback" "feedback activity" page logged in as teacher
|
||||
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
|
||||
And I add a "Multiple choice" question to the feedback with:
|
||||
| Question | Do you like this course? |
|
||||
| Label | multichoice1 |
|
||||
| Multiple choice type | Multiple choice - single answer |
|
||||
| Hide the "Not selected" option | Yes |
|
||||
| Multiple choice values | Yes of course\nNot at all\nI don't know |
|
||||
And I log out
|
||||
And I log in as "user1" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I log in as "user2" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| I don't know | 1 |
|
||||
And I log in as "user3" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I log in as "user4" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| Yes of course | 1 |
|
||||
And I log in as "user5" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| Yes of course | 1 |
|
||||
And I log in as "user6" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I log in as "user7" and complete feedback "Course feedback" in course "Course 1" with:
|
||||
| I don't know | 1 |
|
||||
Given the following "mod_feedback > question" exists:
|
||||
| activity | feedback1 |
|
||||
| name | Do you like this course? |
|
||||
| questiontype | multichoice |
|
||||
| label | multichoice1 |
|
||||
| subtype | r |
|
||||
| hidenoselect | 1 |
|
||||
| values | Yes of course\nNot at all\nI don't know |
|
||||
And the following "mod_feedback > responses" exist:
|
||||
| activity | user | Do you like this course? |
|
||||
| feedback1 | user1 | Not at all |
|
||||
| feedback1 | user2 | I don't know |
|
||||
| feedback1 | user3 | Not at all |
|
||||
| feedback1 | user4 | Yes of course |
|
||||
| feedback1 | user5 | Yes of course |
|
||||
| feedback1 | user6 | Not at all |
|
||||
| feedback1 | user7 | I don't know |
|
||||
# View analysis, user1 should only see one group - group 1
|
||||
And I am on the "Course feedback" "feedback activity" page logged in as user1
|
||||
When I am on the "Course feedback" "feedback activity" page logged in as user1
|
||||
And I follow "Submitted answers"
|
||||
And I should see "Separate groups: Group 1"
|
||||
And I show chart data for the "multichoice1" feedback
|
||||
@@ -151,37 +136,44 @@ Feature: Feedbacks in courses with groups
|
||||
And I should see "Username 3"
|
||||
|
||||
@javascript
|
||||
Scenario: Anonymous feedback with groups in a course
|
||||
Given I am on the "Course anon feedback" "feedback activity" page logged in as teacher
|
||||
And I click on "Edit questions" "link" in the "[role=main]" "css_element"
|
||||
And I add a "Multiple choice" question to the feedback with:
|
||||
| Question | Do you like this course? |
|
||||
| Label | multichoice1 |
|
||||
| Multiple choice type | Multiple choice - single answer |
|
||||
| Hide the "Not selected" option | Yes |
|
||||
| Multiple choice values | Yes of course\nNot at all\nI don't know |
|
||||
And I log out
|
||||
And I log in as "user1" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I am on the "Course anon feedback" "feedback activity" page logged in as user1
|
||||
Scenario: Anonymous feedback with groups in a course - insufficient responses
|
||||
Given the following "mod_feedback > question" exists:
|
||||
| activity | feedback2 |
|
||||
| name | Do you like this course? |
|
||||
| questiontype | multichoice |
|
||||
| label | multichoice1 |
|
||||
| subtype | r |
|
||||
| hidenoselect | 1 |
|
||||
| values | Yes of course\nNot at all\nI don't know |
|
||||
And the following "mod_feedback > responses" exist:
|
||||
| activity | user | Do you like this course? |
|
||||
| feedback2 | user1 | Not at all |
|
||||
When I am on the "Course anon feedback" "feedback activity" page logged in as user1
|
||||
And I follow "Submitted answers"
|
||||
Then I should not see "Yes of course"
|
||||
And I should see "There are insufficient responses for this group"
|
||||
And I should not see "Yes of course"
|
||||
And I log out
|
||||
And I log in as "user2" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| I don't know | 1 |
|
||||
And I log in as "user3" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I log in as "user4" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| Yes of course | 1 |
|
||||
And I log in as "user5" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| Yes of course | 1 |
|
||||
And I log in as "user6" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| Not at all | 1 |
|
||||
And I log in as "user7" and complete feedback "Course anon feedback" in course "Course 1" with:
|
||||
| I don't know | 1 |
|
||||
|
||||
@javascript
|
||||
Scenario: Anonymous feedback with groups in a course
|
||||
Given the following "mod_feedback > question" exists:
|
||||
| activity | feedback2 |
|
||||
| name | Do you like this course? |
|
||||
| questiontype | multichoice |
|
||||
| label | multichoice1 |
|
||||
| subtype | r |
|
||||
| hidenoselect | 1 |
|
||||
| values | Yes of course\nNot at all\nI don't know |
|
||||
And the following "mod_feedback > responses" exist:
|
||||
| activity | user | Do you like this course? |
|
||||
| feedback2 | user1 | Not at all |
|
||||
| feedback2 | user2 | I don't know |
|
||||
| feedback2 | user3 | Not at all |
|
||||
| feedback2 | user4 | Yes of course |
|
||||
| feedback2 | user5 | Yes of course |
|
||||
| feedback2 | user6 | Not at all |
|
||||
| feedback2 | user7 | I don't know |
|
||||
# View analysis, user1 should only see one group - group 1
|
||||
And I am on the "Course anon feedback" "feedback activity" page logged in as user1
|
||||
When I am on the "Course anon feedback" "feedback activity" page logged in as user1
|
||||
And I follow "Submitted answers"
|
||||
And I should see "Separate groups: Group 1"
|
||||
And I show chart data for the "multichoice1" feedback
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Behat data generator for mod_feedback.
|
||||
*
|
||||
* @package mod_feedback
|
||||
* @category test
|
||||
* @copyright 2022 Noel De Martin
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_mod_feedback_generator extends behat_generator_base {
|
||||
|
||||
/**
|
||||
* Get a list of the entities that Behat can create using the generator step.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function get_creatable_entities(): array {
|
||||
return [
|
||||
'questions' => [
|
||||
'singular' => 'question',
|
||||
'datagenerator' => 'question',
|
||||
'required' => ['activity'],
|
||||
'switchids' => ['activity' => 'cmid'],
|
||||
],
|
||||
'responses' => [
|
||||
'singular' => 'response',
|
||||
'datagenerator' => 'response',
|
||||
'required' => ['activity', 'user'],
|
||||
'switchids' => ['activity' => 'cmid', 'user' => 'userid'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,6 +83,97 @@ class mod_feedback_generator extends testing_module_generator {
|
||||
return parent::create_instance($record, (array)$options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create question.
|
||||
*
|
||||
* @param array $data Question data
|
||||
* @return mixed Question instance
|
||||
*/
|
||||
public function create_question(array $data) {
|
||||
global $DB;
|
||||
|
||||
$questiontype = $data['questiontype'] ?? 'textfield';
|
||||
$cm = get_coursemodule_from_id('feedback', $data['cmid']);
|
||||
$feedback = $DB->get_record('feedback', ['id' => $cm->instance]);
|
||||
|
||||
unset($data['questiontype']);
|
||||
unset($data['cmid']);
|
||||
|
||||
if (isset($data['values'])) {
|
||||
$data['values'] = $this->format_item_values($questiontype, $data['values']);
|
||||
}
|
||||
|
||||
return call_user_func([$this, "create_item_{$questiontype}"], $feedback, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create response.
|
||||
*
|
||||
* @param array $data Response data.
|
||||
* @return stdClass feedback_completed response instance.
|
||||
*/
|
||||
public function create_response(array $data): stdClass {
|
||||
global $DB;
|
||||
|
||||
$userid = $data['userid'];
|
||||
$responsenumber = null;
|
||||
$cm = get_coursemodule_from_id('feedback', $data['cmid']);
|
||||
$feedback = $DB->get_record('feedback', ['id' => $cm->instance]);
|
||||
$answers = [];
|
||||
|
||||
if (isset($data['responsenumber']) && trim($data['responsenumber']) !== '') {
|
||||
$responsenumber = $data['responsenumber'];
|
||||
}
|
||||
|
||||
if (isset($data['anonymous']) && trim($data['anonymous']) !== '') {
|
||||
$anonymous = filter_var(trim($data['anonymous']), FILTER_VALIDATE_BOOLEAN);
|
||||
$feedback->anonymous = $anonymous ? FEEDBACK_ANONYMOUS_YES : FEEDBACK_ANONYMOUS_NO;
|
||||
}
|
||||
|
||||
unset($data['cmid']);
|
||||
unset($data['userid']);
|
||||
unset($data['anonymous']);
|
||||
unset($data['responsenumber']);
|
||||
|
||||
foreach ($data as $question => $response) {
|
||||
$item = $DB->get_record('feedback_item', ['name' => trim($question)], '*', MUST_EXIST);
|
||||
|
||||
$answers["{$item->typ}_{$item->id}"] = $this->get_item_response_value($item, $response);
|
||||
}
|
||||
|
||||
$feedbackcompletion = new mod_feedback_completion(
|
||||
$feedback,
|
||||
$cm,
|
||||
$cm->course,
|
||||
false,
|
||||
null,
|
||||
$feedback->anonymous === FEEDBACK_ANONYMOUS_YES ? null : $userid,
|
||||
$userid
|
||||
);
|
||||
|
||||
if (!$feedbackcompletion->can_complete()) {
|
||||
throw new coding_exception("User {$userid} cannot complete this feedback activity.");
|
||||
}
|
||||
|
||||
if (!$feedbackcompletion->is_open()) {
|
||||
throw new coding_exception("This activity is not open.");
|
||||
}
|
||||
|
||||
$feedbackcompletion->set_module_viewed();
|
||||
$feedbackcompletion->save_response_tmp((object) $answers);
|
||||
$feedbackcompletion->save_response();
|
||||
$completed = $feedbackcompletion->get_completed();
|
||||
|
||||
if (!is_null($responsenumber)) {
|
||||
$DB->update_record('feedback_completed', [
|
||||
'id' => $completed->id,
|
||||
'random_response' => $responsenumber,
|
||||
]);
|
||||
}
|
||||
|
||||
return $completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create info question item.
|
||||
*
|
||||
@@ -392,5 +483,71 @@ class mod_feedback_generator extends testing_module_generator {
|
||||
|
||||
return feedback_create_pagebreak($feedback->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format feedback item values.
|
||||
*
|
||||
* This method will replace newline characters with the proper line separator for each question type.
|
||||
*
|
||||
* @param string $questiontype Question types
|
||||
* @param string $values Values
|
||||
* @return string Formatted values
|
||||
*/
|
||||
protected function format_item_values(string $questiontype, string $values): string {
|
||||
global $CFG;
|
||||
|
||||
if (!file_exists($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php')) {
|
||||
throw new coding_exception("Question type '$questiontype' not found");
|
||||
}
|
||||
|
||||
require_once($CFG->dirroot.'/mod/feedback/item/'.$questiontype.'/lib.php');
|
||||
|
||||
$questiontype = strtoupper($questiontype);
|
||||
|
||||
if (defined("FEEDBACK_{$questiontype}_LINE_SEP")) {
|
||||
return implode(constant("FEEDBACK_{$questiontype}_LINE_SEP"), explode('\n', $values));
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a response to a feedback item, return its corresponding value.
|
||||
*
|
||||
* @param mixed $record Item record
|
||||
* @param string $response Response name
|
||||
* @return int|string Response value
|
||||
*/
|
||||
protected function get_item_response_value($record, string $response) {
|
||||
if (strpos($record->typ, 'multichoice') === 0) {
|
||||
$item = feedback_get_item_class($record->typ);
|
||||
|
||||
return $this->get_choice_item_response_value($item, $record, $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a response to a feedback choice item, return its corresponding value.
|
||||
*
|
||||
* @param feedback_item_base $item Feedback item
|
||||
* @param mixed $record Item record
|
||||
* @param string $response Response
|
||||
* @param int $offset Choice to start looking from
|
||||
* @return int Response choice index
|
||||
*/
|
||||
protected function get_choice_item_response_value(feedback_item_base $item, $record, string $response, int $offset = 1): int {
|
||||
$printval = $item->get_printval($record, (object) ['value' => $offset]);
|
||||
|
||||
if (empty($printval)) {
|
||||
throw new coding_exception("Value '$offset' not found");
|
||||
}
|
||||
|
||||
if ($printval === $response) {
|
||||
return $offset;
|
||||
}
|
||||
|
||||
return $this->get_choice_item_response_value($item, $record, $response, $offset + 1);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1722,7 +1722,7 @@ function forum_get_discussions($cm, $forumsort="", $fullpost=true, $unused=-1, $
|
||||
|
||||
$allnames = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
|
||||
$sql = "SELECT $postdata, $discussionfields,
|
||||
$allnames, u.email, u.picture, u.imagealt $umfields
|
||||
$allnames, u.email, u.picture, u.imagealt, u.deleted AS userdeleted $umfields
|
||||
FROM {forum_discussions} d
|
||||
JOIN {forum_posts} p ON p.discussion = d.id
|
||||
JOIN {user} u ON p.userid = u.id
|
||||
|
||||
@@ -114,7 +114,7 @@ class mod_lti_edit_types_form extends moodleform {
|
||||
$mform->setForceLtr('lti_resourcekey');
|
||||
|
||||
$mform->addElement('passwordunmask', 'lti_password', get_string('password_admin', 'lti'));
|
||||
$mform->setType('lti_password', PARAM_TEXT);
|
||||
$mform->setType('lti_password', PARAM_RAW);
|
||||
$mform->addHelpButton('lti_password', 'password_admin', 'lti');
|
||||
$mform->hideIf('lti_password', 'lti_ltiversion', 'eq', LTI_VERSION_1P3);
|
||||
|
||||
|
||||
+16
-10
@@ -1837,7 +1837,7 @@ class quiz_attempt {
|
||||
*/
|
||||
public function render_question_for_commenting($slot) {
|
||||
$options = $this->get_display_options(true);
|
||||
$options->hide_all_feedback();
|
||||
$options->generalfeedback = question_display_options::HIDDEN;
|
||||
$options->manualcomment = question_display_options::EDITABLE;
|
||||
return $this->quba->render_question($slot, $options,
|
||||
$this->get_question_number($slot));
|
||||
@@ -2494,19 +2494,25 @@ class quiz_attempt {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a page access to see if is an out of sequence access.
|
||||
* Check a page read access to see if is an out of sequence access.
|
||||
*
|
||||
* @param int $page page number.
|
||||
* @return boolean false is is an out of sequence access, true otherwise.
|
||||
* If allownext is set then we also check whether access to the page
|
||||
* after the current one should be permitted.
|
||||
*
|
||||
* @param int $page page number.
|
||||
* @param bool $allownext in case of a sequential navigation, can we go to next page ?
|
||||
* @return boolean false is an out of sequence access, true otherwise.
|
||||
* @since Moodle 3.1
|
||||
*/
|
||||
public function check_page_access($page) {
|
||||
if ($this->get_currentpage() != $page) {
|
||||
if ($this->get_navigation_method() == QUIZ_NAVMETHOD_SEQ && $this->get_currentpage() > $page) {
|
||||
return false;
|
||||
}
|
||||
public function check_page_access($page, $allownext = true) {
|
||||
if ($this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
// Sequential access: allow access to the summary, current page or next page.
|
||||
// Or if the user review his/her attempt, see MDLQA-1523.
|
||||
return $page == -1
|
||||
|| $page == $this->get_currentpage()
|
||||
|| $allownext && ($page == $this->get_currentpage() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1021,8 +1021,9 @@ class mod_quiz_external extends external_api {
|
||||
if ($displayoptions->marks >= question_display_options::MARK_AND_MAX) {
|
||||
$question['mark'] = $attemptobj->get_question_mark($slot);
|
||||
}
|
||||
|
||||
$questions[] = $question;
|
||||
if ($attemptobj->check_page_access($attemptobj->get_question_page($slot), false)) {
|
||||
$questions[] = $question;
|
||||
}
|
||||
}
|
||||
return $questions;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,10 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
|
||||
* Create quiz and attempt data with layout.
|
||||
*
|
||||
* @param string $layout layout to set. Like quiz attempt.layout. E.g. '1,2,0,3,4,0,'.
|
||||
* @param string $navmethod quiz navigation method (defaults to free)
|
||||
* @return quiz_attempt the new quiz_attempt object
|
||||
*/
|
||||
protected function create_quiz_and_attempt_with_layout($layout) {
|
||||
protected function create_quiz_and_attempt_with_layout($layout, $navmethod = QUIZ_NAVMETHOD_FREE) {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
// Make a user to do the quiz.
|
||||
@@ -51,7 +52,7 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
|
||||
// Make a quiz.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$quiz = $quizgenerator->create_instance(['course' => $course->id,
|
||||
'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout]);
|
||||
'grade' => 100.0, 'sumgrades' => 2, 'layout' => $layout, 'navmethod' => $navmethod]);
|
||||
|
||||
$quizobj = quiz::create($quiz->id, $user->id);
|
||||
|
||||
@@ -368,4 +369,58 @@ class mod_quiz_attempt_testcase extends advanced_testcase {
|
||||
$step = $quba->get_question_attempt(1)->get_step(0);
|
||||
$this->assertEquals($student1->id, $step->get_user_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test check_page_access function
|
||||
* @covers \quiz_attempt::check_page_access
|
||||
*/
|
||||
public function test_check_page_access() {
|
||||
$timenow = time();
|
||||
|
||||
// Free navigation.
|
||||
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_FREE);
|
||||
|
||||
// Check access.
|
||||
$this->assertTrue($attempt->check_page_access(4));
|
||||
$this->assertTrue($attempt->check_page_access(3));
|
||||
$this->assertTrue($attempt->check_page_access(2));
|
||||
$this->assertTrue($attempt->check_page_access(1));
|
||||
$this->assertTrue($attempt->check_page_access(0));
|
||||
$this->assertTrue($attempt->check_page_access(2));
|
||||
|
||||
// Access page 2.
|
||||
$attempt->set_currentpage(2);
|
||||
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
|
||||
|
||||
// Check access.
|
||||
$this->assertTrue($attempt->check_page_access(0));
|
||||
$this->assertTrue($attempt->check_page_access(1));
|
||||
$this->assertTrue($attempt->check_page_access(2));
|
||||
$this->assertTrue($attempt->check_page_access(3));
|
||||
$this->assertTrue($attempt->check_page_access(4));
|
||||
|
||||
// Sequential navigation.
|
||||
$attempt = $this->create_quiz_and_attempt_with_layout('1,0,2,0,3,0,4,0,5,0', QUIZ_NAVMETHOD_SEQ);
|
||||
|
||||
// Check access.
|
||||
$this->assertTrue($attempt->check_page_access(0));
|
||||
$this->assertTrue($attempt->check_page_access(1));
|
||||
$this->assertFalse($attempt->check_page_access(2));
|
||||
$this->assertFalse($attempt->check_page_access(3));
|
||||
$this->assertFalse($attempt->check_page_access(4));
|
||||
|
||||
// Access page 1.
|
||||
$attempt->set_currentpage(1);
|
||||
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
|
||||
$this->assertTrue($attempt->check_page_access(1));
|
||||
|
||||
// Access page 2.
|
||||
$attempt->set_currentpage(2);
|
||||
$attempt = quiz_attempt::create($attempt->get_attempt()->id);
|
||||
$this->assertTrue($attempt->check_page_access(2));
|
||||
|
||||
$this->assertTrue($attempt->check_page_access(3));
|
||||
$this->assertFalse($attempt->check_page_access(4));
|
||||
$this->assertFalse($attempt->check_page_access(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
@mod @mod_quiz
|
||||
Feature: Attempt a quiz in a sequential mode
|
||||
As a student I should not be able to see the previous questions
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
| student | Student | One | student@example.com |
|
||||
| teacher | Teacher | One | teacher@example.com |
|
||||
And the following "courses" exist:
|
||||
| fullname | shortname | category |
|
||||
| Course 1 | C1 | 0 |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| student | C1 | student |
|
||||
| teacher | C1 | teacher |
|
||||
And the following "question categories" exist:
|
||||
| contextlevel | reference | name |
|
||||
| Course | C1 | Test questions |
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | TF1 | First question |
|
||||
| Test questions | truefalse | TF2 | Second question |
|
||||
| Test questions | truefalse | TF3 | Third question |
|
||||
| Test questions | truefalse | TF4 | Fourth question |
|
||||
And the following "activities" exist:
|
||||
| activity | name | intro | course | idnumber | preferredbehaviour | navmethod |
|
||||
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 | immediatefeedback | sequential |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page | requireprevious |
|
||||
| TF1 | 1 | 1 |
|
||||
| TF2 | 2 | 1 |
|
||||
| TF3 | 3 | 1 |
|
||||
| TF4 | 4 | 1 |
|
||||
|
||||
@javascript
|
||||
Scenario Outline: As a student I should not be able to navigate out of sequence if sequential navigation is on.
|
||||
Given I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I press "Attempt quiz"
|
||||
And I should see "First question"
|
||||
When I am on the "Quiz 1 > student > Attempt 1 > <pagenumber>" "mod_quiz > Attempt view" page
|
||||
And I should see "<canseequestion>"
|
||||
Then I should not see "<cannotseequestion>"
|
||||
Examples:
|
||||
| pagenumber | canseequestion | cannotseequestion |
|
||||
| 1 | First question | Second question |
|
||||
| 2 | Second question | First question |
|
||||
| 4 | First question | Fourth question |
|
||||
|
||||
@javascript
|
||||
Scenario: As a student I should not be able to navigate out of sequence by opening new windows on the same quiz.
|
||||
Given the following config values are set as admin:
|
||||
| config | value | plugin |
|
||||
| autosaveperiod | 60 | quiz |
|
||||
And I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I press "Attempt quiz"
|
||||
And I should see "First question"
|
||||
And I click on "True" "radio" in the "First question" "question"
|
||||
And I click on "Next page" "button"
|
||||
When I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
|
||||
And I click on "True" "radio" in the "Third question" "question"
|
||||
And I should see "Third question"
|
||||
And I click on "Next page" "button"
|
||||
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
|
||||
Then I should see "Fourth question"
|
||||
|
||||
@javascript
|
||||
Scenario: As a student I should not be able to save my data by opening a given page out of sequence.
|
||||
Given the following config values are set as admin:
|
||||
| config | value | plugin |
|
||||
| autosaveperiod | 1 | quiz |
|
||||
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I press "Attempt quiz"
|
||||
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
|
||||
And I should see "Second question"
|
||||
And I click on "True" "radio" in the "Second question" "question"
|
||||
And I wait "2" seconds
|
||||
And I am on the "Quiz 1 > student > Attempt 1 > 1" "mod_quiz > Attempt view" page
|
||||
Then I should see "Second question"
|
||||
|
||||
@javascript
|
||||
Scenario: As a student I can review question I have finished in any order
|
||||
Given user "student" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | True |
|
||||
| 2 | False |
|
||||
| 3 | False |
|
||||
| 4 | False |
|
||||
When I am on the "Quiz 1" "mod_quiz > View" page logged in as "student"
|
||||
And I follow "Review"
|
||||
And I am on the "Quiz 1 > student > Attempt 1 > 3" "mod_quiz > Attempt view" page
|
||||
And I should see "Third question"
|
||||
And I am on the "Quiz 1 > student > Attempt 1 > 2" "mod_quiz > Attempt view" page
|
||||
Then I should see "Second question"
|
||||
@@ -111,7 +111,21 @@ class behat_mod_quiz extends behat_question_base {
|
||||
case 'manual grading report':
|
||||
return new moodle_url('/mod/quiz/report.php',
|
||||
['id' => $this->get_cm_by_quiz_name($identifier)->id, 'mode' => 'grading']);
|
||||
|
||||
case 'attempt view':
|
||||
list($quizname, $username, $attemptno, $pageno) = explode(' > ', $identifier);
|
||||
$pageno = intval($pageno);
|
||||
$pageno = $pageno > 0 ? $pageno - 1 : 0;
|
||||
$attemptno = (int) trim(str_replace ('Attempt', '', $attemptno));
|
||||
$quiz = $this->get_quiz_by_name($quizname);
|
||||
$quizcm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
|
||||
$user = $DB->get_record('user', ['username' => $username], '*', MUST_EXIST);
|
||||
$attempt = $DB->get_record('quiz_attempts',
|
||||
['quiz' => $quiz->id, 'userid' => $user->id, 'attempt' => $attemptno], '*', MUST_EXIST);
|
||||
return new moodle_url('/mod/quiz/attempt.php', [
|
||||
'attempt' => $attempt->id,
|
||||
'cmid' => $quizcm->id,
|
||||
'page' => $pageno
|
||||
]);
|
||||
case 'attempt review':
|
||||
if (substr_count($identifier, ' > ') !== 2) {
|
||||
throw new coding_exception('For "attempt review", name must be ' .
|
||||
|
||||
@@ -105,8 +105,7 @@ Feature: Edit quiz page - adding things
|
||||
in various categories and add them to the question bank.
|
||||
|
||||
# Create a couple of sub categories.
|
||||
When I am on "Course 1" course homepage
|
||||
And I navigate to "Question bank > Categories" in current page administration
|
||||
When I am on the "Course 1" "core_question > course question categories" page
|
||||
Then I should see "Add category"
|
||||
Then I set the field "Parent category" to "Default for C1"
|
||||
And I set the field "Name" to "Subcat 1"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user